Complete daemon-owned Terminus service loop
This commit is contained in:
parent
1fbf238adb
commit
6c2bcd6afb
18 changed files with 559 additions and 53 deletions
|
|
@ -81,6 +81,10 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
|
||||
var cargo = PackCargo(cargoBay, availabilityFactionKey, scenarioCargo,
|
||||
string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase));
|
||||
var sensorArrayCount = string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase) ? 2 : 1;
|
||||
for (var sensorIndex = 0; sensorIndex < sensorArrayCount; sensorIndex++)
|
||||
TryAddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 2, item =>
|
||||
IsGear(item) && HasBehavior(item, "Sensor"));
|
||||
var selectedKeys = new[] { (Role: "hull", Key: hull.ItemKey) }
|
||||
.Concat(slots.Select(value => (Role: "equipment", Key: value.ItemKey)))
|
||||
.Concat(cargo.Select(value => (Role: "cargo", Key: value.Item.ItemKey)));
|
||||
|
|
@ -146,6 +150,28 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
return selected.Item;
|
||||
}
|
||||
|
||||
private AetheriaRuntimeCatalogItem? TryAddFreeSpaceItem(
|
||||
AetheriaRuntimeCatalogItem hull,
|
||||
string factionKey,
|
||||
HashSet<(int X, int Y)> occupied,
|
||||
List<AetheriaEntityItemSlot> slots,
|
||||
double sizeExponent,
|
||||
Func<AetheriaRuntimeCatalogItem, bool> filter)
|
||||
{
|
||||
var candidates = Available(factionKey)
|
||||
.Where(filter)
|
||||
.Select(item => (Item: item, Fit: FindFit(hull, item, occupied)))
|
||||
.Where(value => value.Fit.HasValue)
|
||||
.ToArray();
|
||||
var selected = PickWeighted(candidates, sizeExponent, factionKey, value => value.Item);
|
||||
if (selected.Item == null || !selected.Fit.HasValue)
|
||||
return null;
|
||||
var fit = selected.Fit.Value;
|
||||
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey));
|
||||
Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation);
|
||||
return selected.Item;
|
||||
}
|
||||
|
||||
private AetheriaLoadoutItemSlot[] PackCargo(
|
||||
AetheriaRuntimeCatalogItem cargoBay,
|
||||
string factionKey,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
new AetheriaDroppedPickupSnapshot
|
||||
{
|
||||
PickupIndex = 0,
|
||||
Position = Vec3(-15, 0, -5),
|
||||
Position = Vec3(-40, 0, -29),
|
||||
Velocity = Vec3(0, 0, 0),
|
||||
Item = new AetheriaLoadoutItem { ItemKey = "scrap-metal", Quantity = 1 }
|
||||
}
|
||||
|
|
@ -216,7 +216,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
var entities = new[]
|
||||
{
|
||||
Entity(loadouts, availabilityFactions, "Anchor Station", "station", -50, -30, 0, 0, "player", 760, keys[6], [keys[1], keys[2], keys[3], keys[4], keys[6], keys[7], keys[11]], ["repair-parts", "reactor-fuel", "drone-core"]),
|
||||
Entity(loadouts, availabilityFactions, "Vanguard One", "ship", -40, -30, 10, 4, "player", 540, keys[6], [keys[0], keys[2], keys[4], keys[6], keys[7]], ["coilgun-ammo", "field-rations"]),
|
||||
Entity(loadouts, availabilityFactions, "Vanguard One", "ship", -40, -30, 0, 0, "player", 540, keys[6], [keys[0], keys[2], keys[4], keys[6], keys[7]], ["coilgun-ammo", "field-rations"]),
|
||||
Entity(loadouts, availabilityFactions, "Wing Two", "ship", 145, 125, -5, 7, "player", 450, keys[6], [keys[0], keys[1], keys[6], keys[8]], ["sensor-buoy", "shield-cell"]),
|
||||
Entity(loadouts, availabilityFactions, "Torch Three", "ship", -235, 210, 8, -4, "player", 500, keys[7], [keys[0], keys[1], keys[7]], ["micro-missile", "coolant-pack"]),
|
||||
Entity(loadouts, availabilityFactions, "Foundry Tug", "ship", 330, -155, -2, 3, "player", 390, keys[6], [keys[0], keys[1], keys[6], keys[10]], ["ore-canister", "field-rations"]),
|
||||
|
|
@ -230,6 +230,13 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
};
|
||||
foreach (var index in new[] { 1, 2, 3, 4 })
|
||||
entities[index].HomeEntityKey = keys[0];
|
||||
entities[1].CargoContents =
|
||||
[
|
||||
new AetheriaCargoBayLoadout
|
||||
{
|
||||
Items = Array.Empty<AetheriaLoadoutItemSlot>()
|
||||
}
|
||||
];
|
||||
entities[2].AgentTaskCapabilities = ["attack", "defend", "explore"];
|
||||
entities[3].AgentTaskCapabilities = ["attack", "defend"];
|
||||
entities[4].AgentTaskCapabilities = ["mine", "haul", "tow", "explore"];
|
||||
|
|
|
|||
|
|
@ -199,13 +199,15 @@ static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
|
|||
policyRejectedCommandIds);
|
||||
var terminus = string.Equals(ingressState.GameMode, AetheriaGameSessionState.TerminusMode, StringComparison.Ordinal);
|
||||
var simulationClockCommands = authorizedCommands
|
||||
.Where(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetSimulationRate)
|
||||
.Where(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetSimulationRate ||
|
||||
command.Kind == AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep)
|
||||
.ToArray();
|
||||
if (!terminus && simulationClockCommands.Length > 0)
|
||||
{
|
||||
policyRejectedCommandIds.AddRange(simulationClockCommands.Select(command => command.CommandId));
|
||||
authorizedCommands = authorizedCommands
|
||||
.Where(command => command.Kind != AetheriaRuntimeDaemonCommandKinds.SetSimulationRate)
|
||||
.Where(command => command.Kind != AetheriaRuntimeDaemonCommandKinds.SetSimulationRate &&
|
||||
command.Kind != AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep)
|
||||
.ToArray();
|
||||
}
|
||||
else if (simulationClockCommands.Length > 0)
|
||||
|
|
@ -217,7 +219,7 @@ static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
|
|||
if (terminus && !advanceSimulation)
|
||||
{
|
||||
authorizedCommands = authorizedCommands
|
||||
.Where(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetSimulationRate)
|
||||
.Where(command => !AetheriaRuntimeDaemonOperations.RequiresSimulationStep(command.Kind))
|
||||
.ToArray();
|
||||
}
|
||||
var simulationTimeSeconds = (currentFrame?.SimulationTimeSeconds ?? 0) +
|
||||
|
|
@ -369,6 +371,11 @@ static async Task ApplySimulationClockCommandsAsync(
|
|||
.ReadAsync().ConfigureAwait(false) ?? new AetheriaGameSessionState();
|
||||
foreach (var command in commands.OrderBy(command => command.IssuedAtUtc, StringComparer.Ordinal))
|
||||
{
|
||||
if (command.Kind == AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep)
|
||||
{
|
||||
ingressState.SimulationStepAccumulator += 1;
|
||||
continue;
|
||||
}
|
||||
if (AetheriaRuntimeDaemonOperations.IsSupportedSimulationRate(command.ScalarValue))
|
||||
session.SimulationRate = command.ScalarValue;
|
||||
}
|
||||
|
|
@ -1526,10 +1533,12 @@ static async Task PublishDaemonApiDocumentsAsync(
|
|||
.ConfigureAwait(false);
|
||||
var gameSession = await node.MutableDocument<AetheriaGameSessionState>(AetheriaStateNode.GameSessionStateKey)
|
||||
.ReadAsync().ConfigureAwait(false);
|
||||
var inputCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(node.StatePath);
|
||||
await node.MutableDocument<EveInputCapabilityDocument>(AetheriaRuntimeVerseRecordKeys.PilotInputCapability)
|
||||
.ReplaceAsync(AetheriaRuntimeInputCapabilityDocument.FromFrame(
|
||||
result.Frame,
|
||||
string.Equals(gameSession?.Mode, AetheriaGameSessionState.TerminusMode, StringComparison.Ordinal)).ToEveDocument())
|
||||
string.Equals(gameSession?.Mode, AetheriaGameSessionState.TerminusMode, StringComparison.Ordinal),
|
||||
inputCatalog).ToEveDocument())
|
||||
.ConfigureAwait(false);
|
||||
var mainMenuState = await node.MutableDocument<AetheriaMainMenuState>(AetheriaStateNode.MainMenuStateKey)
|
||||
.ReadAsync()
|
||||
|
|
@ -2687,7 +2696,7 @@ static async Task<AetheriaRuntimeRunCheckpointCommit?> ReadRuntimeRunCheckpointA
|
|||
.ToArray(),
|
||||
GenerationSeed = run.GenerationSeed,
|
||||
CurrentEntityKey = run.CurrentEntityKey ?? "",
|
||||
Credits = 1000
|
||||
Credits = 1000000
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11054,7 +11054,7 @@ static void RequireTypedDaemonCommandPayloads(string root)
|
|||
"return Submit((client, frame) => client.SetTarget(frame, targetEntityKey));",
|
||||
"return Send((client, frame) => client.TransferCargoItem(",
|
||||
"command.CargoTransfer.OriginEntityKey",
|
||||
"command.TradePurchase.TotalPrice",
|
||||
"ReadPayloadString(request, \"itemKey\", \"\")",
|
||||
"command.LootPickup.ItemKey",
|
||||
"command.LoadoutRestore.TemplateName",
|
||||
"command.EquipmentTransfer.SourceKind",
|
||||
|
|
|
|||
|
|
@ -2036,6 +2036,8 @@ public class DaemonRuntimeDocumentTests
|
|||
FactionKey = "player",
|
||||
IsActive = true,
|
||||
TargetEntityIndex = 1,
|
||||
Visibility = 0.37,
|
||||
VisibilitySourceCount = 2,
|
||||
PositionX = 0,
|
||||
PositionZ = 0
|
||||
},
|
||||
|
|
@ -2178,7 +2180,9 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(1.25, weapon.BurstInterval, 0.0001);
|
||||
Assert.AreEqual(88, zone.Entities[0].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001);
|
||||
Assert.AreEqual(66, zone.Entities[1].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001);
|
||||
Assert.AreEqual(222, zone.Entities[0].Visibility, 0.0001);
|
||||
Assert.AreEqual(0.37, zone.Entities[0].Visibility, 0.0001);
|
||||
Assert.AreEqual(2, zone.Entities[0].VisibilitySourceCount);
|
||||
Assert.IsTrue(zone.Entities[0].Contacts.Single(contact => contact.TargetEntityIndex == 1).Visible);
|
||||
|
||||
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
||||
run,
|
||||
|
|
@ -2192,6 +2196,84 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(120, frame.SimulationSettings.AttackRange, 0.0001);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SensorReachDerivesFromInstalledEquipmentRatherThanEntityKind()
|
||||
{
|
||||
var unitStat = new AetheriaRuntimeBehaviorValue(
|
||||
"performance-stat", "", 0, false, "", "",
|
||||
new[]
|
||||
{
|
||||
NumberValue(1), NumberValue(1), NumberValue(1),
|
||||
NumberValue(0), NumberValue(1), EmptyBehaviorValue("stat-recipe")
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[]
|
||||
{
|
||||
CatalogItem("sensor", new[]
|
||||
{
|
||||
new AetheriaRuntimeBehaviorPayload(
|
||||
0, "Sensor", 1,
|
||||
new[] { new AetheriaRuntimeBehaviorField(3, unitStat) })
|
||||
})
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
AetheriaRuntimeLoadoutItemSlotCommit Sensor() => new()
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = "sensor", Quality = 1, Durability = 1, Enabled = true
|
||||
}
|
||||
};
|
||||
var ship = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 0, Kind = "ship", FactionKey = "player", IsActive = true,
|
||||
Equipment = new[] { Sensor() }, Visibility = 0.4
|
||||
};
|
||||
var station = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 1, Kind = "station", FactionKey = "player", IsActive = true,
|
||||
Equipment = new[] { Sensor() }, PositionZ = 10, Visibility = 0.6
|
||||
};
|
||||
var arrayShip = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 2, Kind = "ship", FactionKey = "player", IsActive = true,
|
||||
Equipment = new[] { Sensor(), Sensor() }, PositionZ = 20, Visibility = 0.8
|
||||
};
|
||||
var target = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 3, Kind = "ship", FactionKey = "neutral", IsActive = true,
|
||||
PositionX = 250
|
||||
};
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
CurrentZoneIndex = 0,
|
||||
Zones = new[] { new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = new[] { ship, station, arrayShip, target } } }
|
||||
};
|
||||
var defaults = AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
|
||||
var settings = new AetheriaRuntimeDaemonSimulationSettings(
|
||||
defaults.PawnSpeed, defaults.RaiderSpeed, defaults.AttackRange, defaults.AttackHoldRatio,
|
||||
defaults.PawnProjectileDamage, defaults.RaiderProjectileDamage, defaults.WeaponCooldownSeconds,
|
||||
defaults.ProjectileSpeed, defaults.ProjectileRadius, defaults.ProjectileLifetimeSeconds,
|
||||
defaults.ProjectileSpawnOffset, defaults.ProjectileHeatScale, defaults.HeatDissipationPerSecond,
|
||||
999, 200, defaults.PlayerStationHull, defaults.HostileStationHull, defaults.PlayerEntityHull,
|
||||
defaults.RaiderEntityHull, defaults.StationShield, defaults.EntityShield);
|
||||
|
||||
AetheriaRuntimeDaemonSimulation.Step(
|
||||
run, new AetheriaRuntimeDaemonIntentState(), 0.1, settings,
|
||||
new PassthroughPhysicalPayloadPhysics(), new PassthroughWorldPhysics(), catalog);
|
||||
|
||||
Assert.IsFalse(ship.Contacts.Single(contact => contact.TargetEntityIndex == 3).Visible);
|
||||
Assert.IsFalse(station.Contacts.Single(contact => contact.TargetEntityIndex == 3).Visible,
|
||||
"Entity kind must not grant a station privileged sensor reach.");
|
||||
Assert.IsTrue(arrayShip.Contacts.Single(contact => contact.TargetEntityIndex == 3).Visible,
|
||||
"Additional installed arrays should increase reach regardless of entity kind.");
|
||||
Assert.AreEqual(0.4, ship.Visibility, 0.0001);
|
||||
Assert.AreEqual(0.6, station.Visibility, 0.0001);
|
||||
Assert.AreEqual(0.8, arrayShip.Visibility, 0.0001);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TerminusPilotTractorScoopsCargoFromTargetedWreck()
|
||||
{
|
||||
|
|
@ -4190,10 +4272,36 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(0.25, run.Zones[0].Entities[0].ShutdownPerformance, 0.0001);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsClassifiesPausedCommandsBySimulationDependency()
|
||||
{
|
||||
Assert.IsFalse(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase));
|
||||
Assert.IsFalse(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.SetTarget));
|
||||
Assert.IsFalse(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.TransferCargoItem));
|
||||
Assert.IsFalse(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.SetSimulationRate));
|
||||
Assert.IsFalse(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep));
|
||||
Assert.IsTrue(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.SetMoveVector));
|
||||
Assert.IsTrue(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup));
|
||||
Assert.IsTrue(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.DockNearest));
|
||||
Assert.IsTrue(AetheriaRuntimeDaemonOperations.RequiresSimulationStep(
|
||||
AetheriaRuntimeDaemonCommandKinds.Undock));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsOwnsTradePurchaseCreditsAndCargoInDaemonState()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
run.CurrentEntityKey = "zone.0.entity.1";
|
||||
run.Zones[0].Entities[0].DockingBayAssignments = new[] { 1 };
|
||||
run.Zones[0].Entities[0].ChildEntityIndices = new[] { 1 };
|
||||
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase,
|
||||
"codex",
|
||||
|
|
@ -4206,8 +4314,8 @@ public class DaemonRuntimeDocumentTests
|
|||
command.TradePurchase.PurchaseKind = "commodity";
|
||||
command.TradePurchase.ItemKey = "ore";
|
||||
command.TradePurchase.Quantity = 5;
|
||||
command.TradePurchase.UnitPrice = 40;
|
||||
command.TradePurchase.TotalPrice = 200;
|
||||
command.TradePurchase.UnitPrice = 0;
|
||||
command.TradePurchase.TotalPrice = 0;
|
||||
command.TradePurchase.StationEntityKey = "zone.0.entity.0";
|
||||
command.TradePurchase.StationCargoIndex = 0;
|
||||
command.TradePurchase.TargetEntityKey = "zone.0.entity.1";
|
||||
|
|
@ -4216,7 +4324,16 @@ public class DaemonRuntimeDocumentTests
|
|||
command.TradePurchase.SourceY = 3;
|
||||
command.TradePurchase.CreatesDockedShip = false;
|
||||
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(run, new[] { command });
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(
|
||||
run,
|
||||
new[] { command },
|
||||
new AetheriaRuntimeDaemonOperationContext
|
||||
{
|
||||
Catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { CatalogItem("ore", Array.Empty<AetheriaRuntimeBehaviorPayload>(), price: 40) },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>())
|
||||
});
|
||||
|
||||
var stationCargo = run.Zones[0].Entities[0].CargoContents[0].Items;
|
||||
var targetCargo = run.Zones[0].Entities[1].CargoContents[0].Items;
|
||||
|
|
@ -4229,10 +4346,58 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(5, targetCargo[0].Item.Quantity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsRejectsTradeWhenCurrentEntityIsNotDockedWithoutMutatingState()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase,
|
||||
"codex",
|
||||
"session-trade-undocked",
|
||||
30,
|
||||
"zone.0.entity.1");
|
||||
command.TradePurchase.ItemKey = "ore";
|
||||
command.TradePurchase.Quantity = 1;
|
||||
command.TradePurchase.StationEntityKey = "zone.0.entity.0";
|
||||
command.TradePurchase.StationCargoIndex = 0;
|
||||
command.TradePurchase.TargetEntityKey = "zone.0.entity.1";
|
||||
command.TradePurchase.TargetCargoIndex = 0;
|
||||
command.TradePurchase.SourceX = 2;
|
||||
command.TradePurchase.SourceY = 3;
|
||||
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(
|
||||
run,
|
||||
new[] { command },
|
||||
new AetheriaRuntimeDaemonOperationContext
|
||||
{
|
||||
Catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { CatalogItem("ore", Array.Empty<AetheriaRuntimeBehaviorPayload>(), price: 40) },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>())
|
||||
});
|
||||
|
||||
Assert.AreEqual(0, result.AppliedCommandIds.Count);
|
||||
Assert.AreEqual(1, result.RejectedCommandIds.Count);
|
||||
Assert.AreEqual(1000, run.Credits);
|
||||
Assert.AreEqual(12, run.Zones[0].Entities[0].CargoContents[0].Items[0].Item.Quantity);
|
||||
Assert.AreEqual(0, run.Zones[0].Entities[1].CargoContents[0].Items.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsCreatesPurchasedDockedShipInDaemonState()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
run.Zones[0].Entities[1].DockingBayAssignments = new[] { 0, -1 };
|
||||
run.Zones[0].Entities[1].ChildEntityIndices = new[] { 0 };
|
||||
run.Zones[0].Entities[1].CargoContents[0].Items = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
X = 4,
|
||||
Y = 5,
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "starter-hull", Quantity = 1 }
|
||||
}
|
||||
};
|
||||
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase,
|
||||
"codex",
|
||||
|
|
@ -4247,10 +4412,23 @@ public class DaemonRuntimeDocumentTests
|
|||
command.TradePurchase.Quantity = 1;
|
||||
command.TradePurchase.UnitPrice = 300;
|
||||
command.TradePurchase.TotalPrice = 300;
|
||||
command.TradePurchase.StationEntityKey = "zone.0.entity.1";
|
||||
command.TradePurchase.StationCargoIndex = 0;
|
||||
command.TradePurchase.TargetEntityKey = "zone.0.entity.1";
|
||||
command.TradePurchase.SourceX = 4;
|
||||
command.TradePurchase.SourceY = 5;
|
||||
command.TradePurchase.CreatesDockedShip = true;
|
||||
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(run, new[] { command });
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(
|
||||
run,
|
||||
new[] { command },
|
||||
new AetheriaRuntimeDaemonOperationContext
|
||||
{
|
||||
Catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { CatalogItem("starter-hull", Array.Empty<AetheriaRuntimeBehaviorPayload>(), price: 300, hullType: "ship") },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>())
|
||||
});
|
||||
|
||||
var zone = run.Zones[0];
|
||||
Assert.AreEqual(1, result.AppliedCommandIds.Count);
|
||||
|
|
@ -5437,7 +5615,9 @@ public class DaemonRuntimeDocumentTests
|
|||
|
||||
private static AetheriaRuntimeCatalogItem CatalogItem(
|
||||
string itemKey,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> behaviorPayloads)
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> behaviorPayloads,
|
||||
int price = 0,
|
||||
string hullType = "")
|
||||
{
|
||||
return new AetheriaRuntimeCatalogItem(
|
||||
itemKey,
|
||||
|
|
@ -5445,7 +5625,7 @@ public class DaemonRuntimeDocumentTests
|
|||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
price,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
|
@ -5461,7 +5641,7 @@ public class DaemonRuntimeDocumentTests
|
|||
Array.Empty<AetheriaRuntimeHardpoint>(),
|
||||
behaviorPayloads,
|
||||
"",
|
||||
"",
|
||||
hullType,
|
||||
behaviorPayloads.Select(payload => payload.Kind).ToArray(),
|
||||
1,
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -36,9 +36,31 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Math.Max(0, Capacity(entity, catalog) - Occupied(entity, catalog));
|
||||
|
||||
public static int UnitsThatFit(AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog, string itemKey)
|
||||
{
|
||||
return UnitsThatFit(entity, catalog, itemKey, 0);
|
||||
}
|
||||
|
||||
public static int UnitsThatFit(
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
string itemKey,
|
||||
int cargoIndex)
|
||||
{
|
||||
var volume = catalog?.FindItem(itemKey ?? "")?.Volume ?? 0;
|
||||
return volume <= 0 ? int.MaxValue : Math.Max(0, (int)Math.Floor(Available(entity, catalog) / volume));
|
||||
if (volume <= 0)
|
||||
return int.MaxValue;
|
||||
var cargoBays = (entity?.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Select(slot => catalog?.FindItem(slot?.Item?.ItemKey ?? ""))
|
||||
.Where(item => item != null && item.InteriorOccupiedCells > 0)
|
||||
.ToArray();
|
||||
if (cargoIndex < 0 || cargoIndex >= cargoBays.Length)
|
||||
return 0;
|
||||
var capacity = cargoBays[cargoIndex]!.InteriorOccupiedCells;
|
||||
var bay = (entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.ElementAtOrDefault(cargoIndex);
|
||||
var occupied = (bay?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Sum(slot => Volume(slot?.Item, catalog!));
|
||||
return Math.Max(0, (int)Math.Floor((capacity - occupied) / volume));
|
||||
}
|
||||
|
||||
private static double Volume(AetheriaRuntimeLoadoutItemCommit? item, AetheriaRuntimeCatalogSnapshot catalog) =>
|
||||
|
|
|
|||
|
|
@ -195,7 +195,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
ToggleShieldEnabled,
|
||||
IssueAgentTask,
|
||||
CancelAgentTask,
|
||||
SetSimulationRate
|
||||
SetSimulationRate,
|
||||
AdvanceSimulationStep
|
||||
}
|
||||
|
||||
[CultDocument("gamecult.aetheria.daemon_provider_advertisement", "gamecult.aetheria.daemon_provider_advertisement.v1")]
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return Send(command);
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope AdvanceSimulationStep(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame)
|
||||
{
|
||||
return Send(Create(AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep, frame));
|
||||
}
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SetLookDirection(
|
||||
AetheriaRuntimeDaemonFrameDocument? frame,
|
||||
double directionX,
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
case AetheriaRuntimeDaemonCommandKinds.ToggleHullConductivity:
|
||||
return ApplyToggleHullConductivity(run, command);
|
||||
case AetheriaRuntimeDaemonCommandKinds.TradePurchase:
|
||||
return ApplyTradePurchase(run, command);
|
||||
return ApplyTradePurchase(run, command, context.Catalog);
|
||||
case AetheriaRuntimeDaemonCommandKinds.RestoreLoadout:
|
||||
return ApplyRestoreLoadout(run, command, context);
|
||||
case AetheriaRuntimeDaemonCommandKinds.SetMoveVector:
|
||||
|
|
@ -181,6 +181,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return ApplyCancelAgentTask(run, command);
|
||||
case AetheriaRuntimeDaemonCommandKinds.SetSimulationRate:
|
||||
return IsSupportedSimulationRate(command.ScalarValue);
|
||||
case AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
@ -191,6 +193,27 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public static bool IsSupportedSimulationRate(double value) =>
|
||||
SupportedSimulationRates.Any(rate => Math.Abs(value - rate) < 0.000001);
|
||||
|
||||
public static bool RequiresSimulationStep(AetheriaRuntimeDaemonCommandKinds kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AetheriaRuntimeDaemonCommandKinds.SetMoveVector:
|
||||
case AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup:
|
||||
case AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive:
|
||||
case AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive:
|
||||
case AetheriaRuntimeDaemonCommandKinds.ActivateConsumable:
|
||||
case AetheriaRuntimeDaemonCommandKinds.SensorPing:
|
||||
case AetheriaRuntimeDaemonCommandKinds.Dock:
|
||||
case AetheriaRuntimeDaemonCommandKinds.DockNearest:
|
||||
case AetheriaRuntimeDaemonCommandKinds.Undock:
|
||||
case AetheriaRuntimeDaemonCommandKinds.Interact:
|
||||
case AetheriaRuntimeDaemonCommandKinds.EnterWormhole:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ApplyIssueAgentTask(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeDaemonCommandDocument command)
|
||||
|
|
@ -748,7 +771,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var quantity = transfer.Quantity > 0
|
||||
? transfer.Quantity
|
||||
: (int)Math.Round(command.ScalarValue);
|
||||
if (quantity <= 0 || quantity > AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(destinationEntity, catalog, command.TextValue))
|
||||
if (quantity <= 0 || quantity > AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(destinationEntity, catalog, command.TextValue, destinationCargoIndex))
|
||||
return false;
|
||||
if (!TryRemoveCargoItemQuantity(
|
||||
originEntity,
|
||||
|
|
@ -917,18 +940,58 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static bool ApplyTradePurchase(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeDaemonCommandDocument command)
|
||||
AetheriaRuntimeDaemonCommandDocument command,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
var purchase = command.TradePurchase ?? new AetheriaRuntimeTradePurchaseCommand();
|
||||
var totalPrice = purchase.TotalPrice;
|
||||
if (totalPrice < 0 || run.Credits < totalPrice)
|
||||
var itemKey = purchase.ItemKey ?? "";
|
||||
var typedItem = catalog?.FindItem(itemKey);
|
||||
var quantity = Math.Max(1, purchase.Quantity);
|
||||
if (typedItem == null ||
|
||||
!TryResolveDockParent(run, run.CurrentEntityKey, out var dockParentKey, out var dockParent))
|
||||
return false;
|
||||
|
||||
if (purchase.CreatesDockedShip)
|
||||
if (!TryFindStationStock(
|
||||
dockParent,
|
||||
itemKey,
|
||||
purchase.StationCargoIndex,
|
||||
purchase.SourceX,
|
||||
purchase.SourceY,
|
||||
out var stationCargoIndex,
|
||||
out var stockSlot))
|
||||
{
|
||||
if (!ApplyCreateDockedShipPurchase(run, purchase, out var purchasedShipKey))
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
var stationEntity = dockParent;
|
||||
|
||||
var unitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
|
||||
typedItem,
|
||||
stockSlot.Item,
|
||||
catalog.TradeValueSettings).Price;
|
||||
var totalPrice = checked(unitPrice * quantity);
|
||||
if (unitPrice < 0 || run.Credits < totalPrice)
|
||||
return false;
|
||||
|
||||
var createsDockedShip = !string.IsNullOrWhiteSpace(typedItem.HullType);
|
||||
if (createsDockedShip)
|
||||
{
|
||||
if (quantity != 1 || !HasAvailableDockingBay(dockParent))
|
||||
return false;
|
||||
purchase.TargetEntityKey = dockParentKey;
|
||||
if (!TryRemoveCargoItemQuantity(
|
||||
stationEntity,
|
||||
stationCargoIndex,
|
||||
itemKey,
|
||||
purchase.SourceX,
|
||||
purchase.SourceY,
|
||||
1,
|
||||
out var purchasedHull))
|
||||
return false;
|
||||
if (!ApplyCreateDockedShipPurchase(run, purchase, out var purchasedShipKey))
|
||||
{
|
||||
AddCargoItem(stationEntity, stationCargoIndex, purchasedHull);
|
||||
return false;
|
||||
}
|
||||
run.CurrentEntityKey = purchasedShipKey;
|
||||
run.Credits -= totalPrice;
|
||||
return true;
|
||||
|
|
@ -936,24 +999,17 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
if (!TryResolveCargoBay(
|
||||
run,
|
||||
purchase.StationEntityKey,
|
||||
purchase.StationCargoIndex,
|
||||
out var stationEntity,
|
||||
out var stationCargoIndex,
|
||||
out _) ||
|
||||
!TryResolveCargoBay(
|
||||
run,
|
||||
purchase.TargetEntityKey,
|
||||
purchase.TargetCargoIndex,
|
||||
run.CurrentEntityKey,
|
||||
Math.Max(0, purchase.TargetCargoIndex),
|
||||
out var targetEntity,
|
||||
out var targetCargoIndex,
|
||||
out _))
|
||||
out _) ||
|
||||
!IsEntityDockedAt(run, run.CurrentEntityKey, dockParent) ||
|
||||
quantity > AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(targetEntity, catalog, itemKey, targetCargoIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemKey = purchase.ItemKey ?? "";
|
||||
var quantity = Math.Max(1, purchase.Quantity);
|
||||
if (!TryRemoveCargoItemQuantity(
|
||||
stationEntity,
|
||||
stationCargoIndex,
|
||||
|
|
@ -973,6 +1029,79 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveDockParent(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
string childEntityKey,
|
||||
out string parentEntityKey,
|
||||
out AetheriaRuntimeEntitySnapshotCommit parent)
|
||||
{
|
||||
parentEntityKey = "";
|
||||
parent = null!;
|
||||
if (!TryResolveEntity(run, childEntityKey, out var zoneIndex, out var childIndex, out _))
|
||||
return false;
|
||||
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
|
||||
parent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.FirstOrDefault(entity => entity != null &&
|
||||
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(childIndex))!;
|
||||
if (parent == null)
|
||||
return false;
|
||||
parentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, parent.EntityIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsEntityDockedAt(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
string entityKey,
|
||||
AetheriaRuntimeEntitySnapshotCommit parent)
|
||||
{
|
||||
return TryResolveEntity(run, entityKey, out _, out var entityIndex, out _) &&
|
||||
(parent.DockingBayAssignments ?? Array.Empty<int>()).Contains(entityIndex);
|
||||
}
|
||||
|
||||
private static bool HasAvailableDockingBay(AetheriaRuntimeEntitySnapshotCommit parent) =>
|
||||
(parent.DockingBayAssignments ?? Array.Empty<int>()).Any(index => index < 0);
|
||||
|
||||
private static bool TryFindCargoItem(
|
||||
AetheriaRuntimeCargoBayLoadoutCommit cargo,
|
||||
string itemKey,
|
||||
int x,
|
||||
int y,
|
||||
out AetheriaRuntimeLoadoutItemSlotCommit slot)
|
||||
{
|
||||
slot = (cargo?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.FirstOrDefault(candidate => candidate?.Item != null &&
|
||||
string.Equals(candidate.Item.ItemKey ?? "", itemKey, StringComparison.Ordinal) &&
|
||||
candidate.X == x && candidate.Y == y)!;
|
||||
return slot != null;
|
||||
}
|
||||
|
||||
private static bool TryFindStationStock(
|
||||
AetheriaRuntimeEntitySnapshotCommit station,
|
||||
string itemKey,
|
||||
int requestedCargoIndex,
|
||||
int requestedX,
|
||||
int requestedY,
|
||||
out int cargoIndex,
|
||||
out AetheriaRuntimeLoadoutItemSlotCommit slot)
|
||||
{
|
||||
cargoIndex = -1;
|
||||
slot = null!;
|
||||
var cargo = station.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
||||
IEnumerable<int> indices = requestedCargoIndex >= 0 && requestedCargoIndex < cargo.Count
|
||||
? new[] { requestedCargoIndex }
|
||||
: Enumerable.Range(0, cargo.Count);
|
||||
foreach (var index in indices)
|
||||
{
|
||||
if (TryFindCargoItem(cargo[index], itemKey, requestedX, requestedY, out slot))
|
||||
{
|
||||
cargoIndex = index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ApplyCreateDockedShipPurchase(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeTradePurchaseCommand purchase,
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@ public sealed class AetheriaRuntimeDaemonOperationsClient
|
|||
AetheriaRuntimeDaemonCommandKinds.SetSimulationRate => client.SetSimulationRate(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "scalarValue", 0.0)),
|
||||
AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep => client.AdvanceSimulationStep(frame),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetLookDirection => client.SetLookDirection(
|
||||
frame,
|
||||
ReadPayloadDouble(request, "directionX", 0.0),
|
||||
|
|
@ -528,6 +529,20 @@ public sealed class AetheriaRuntimeDaemonOperationsClient
|
|||
AetheriaRuntimeDaemonCommandKinds.CancelAgentTask => client.CancelAgentTask(
|
||||
frame,
|
||||
ReadPayloadString(request, "taskId", "")),
|
||||
AetheriaRuntimeDaemonCommandKinds.TradePurchase => client.TradePurchase(
|
||||
frame,
|
||||
"",
|
||||
ReadPayloadString(request, "itemKey", ""),
|
||||
(int)ReadPayloadDouble(request, "quantity", 1),
|
||||
0,
|
||||
0,
|
||||
ReadPayloadString(request, "stationEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "stationCargoIndex", -1),
|
||||
ReadPayloadString(request, "targetEntityKey", ""),
|
||||
(int)ReadPayloadDouble(request, "targetCargoIndex", -1),
|
||||
(int)ReadPayloadDouble(request, "sourceX", int.MinValue),
|
||||
(int)ReadPayloadDouble(request, "sourceY", int.MinValue),
|
||||
false),
|
||||
_ => AetheriaRuntimeDaemonSurfaceCommandCatalog.TrySubmitArgumentless(
|
||||
client,
|
||||
frame,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
frameId, simulationTimeSeconds);
|
||||
AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
|
||||
AetheriaRuntimeSurveySimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
|
||||
RefreshContacts(entities, settings);
|
||||
RefreshContacts(entities, settings, catalog);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1724,10 +1724,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static void RefreshContacts(
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
foreach (var observer in entities)
|
||||
{
|
||||
var sensorRange = ResolveSensorRange(observer, settings, catalog);
|
||||
var contacts = new List<AetheriaRuntimeEntityContactCommit>();
|
||||
foreach (var target in entities)
|
||||
{
|
||||
|
|
@ -1735,7 +1737,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
continue;
|
||||
|
||||
var distance = Math.Sqrt(DistanceSq(observer, target));
|
||||
var visible = distance <= ResolveSensorRange(observer, settings);
|
||||
var visible = distance <= sensorRange;
|
||||
if (!visible && !Hostile(observer, target))
|
||||
continue;
|
||||
|
||||
|
|
@ -1749,8 +1751,6 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
observer.Contacts = contacts;
|
||||
observer.Visibility = observer.IsActive ? ResolveSensorRange(observer, settings) : 0;
|
||||
observer.VisibilitySourceCount = observer.IsActive ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2066,10 +2066,27 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static double ResolveSensorRange(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase)
|
||||
? settings.StationSensorRange
|
||||
if (!entity.IsActive)
|
||||
return 0;
|
||||
|
||||
var equipmentStates = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(state => state != null)
|
||||
.ToDictionary(state => state.EquipmentIndex);
|
||||
var sensitivitySquared = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Sensor")
|
||||
.Where(sensor => sensor.Item.Enabled && sensor.Item.Durability > 0 &&
|
||||
(!equipmentStates.TryGetValue(sensor.EquipmentIndex, out var state) || state.Online))
|
||||
.Select(sensor => Math.Max(0, sensor.EvaluateStat(
|
||||
3,
|
||||
equipmentStates.TryGetValue(sensor.EquipmentIndex, out var state)
|
||||
? state.ThermalPerformance
|
||||
: 1)))
|
||||
.Select(sensitivity => sensitivity * sensitivity)
|
||||
.Sum();
|
||||
return sensitivitySquared > 0
|
||||
? settings.EntitySensorRange * Math.Sqrt(sensitivitySquared)
|
||||
: settings.EntitySensorRange;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double ProjectileSpawnOffset { get; }
|
||||
public double ProjectileHeatScale { get; }
|
||||
public double HeatDissipationPerSecond { get; }
|
||||
[System.Obsolete("Sensor reach derives from installed equipment. Retained only for snapshot compatibility.")]
|
||||
public double StationSensorRange { get; }
|
||||
public double FallbackSensorRange => EntitySensorRange;
|
||||
public double EntitySensorRange { get; }
|
||||
public double PlayerStationHull { get; }
|
||||
public double HostileStationHull { get; }
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
public static AetheriaRuntimeInputCapabilityDocument FromFrame(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
bool includeSimulationClock = false)
|
||||
bool includeSimulationClock = false,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null)
|
||||
{
|
||||
var run = frame?.Run ?? new AetheriaRuntimeRunCheckpointCommit();
|
||||
var entity = run.Zones.SelectMany(zone => zone.Entities).FirstOrDefault(candidate =>
|
||||
|
|
@ -68,6 +69,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
if (includeSimulationClock)
|
||||
{
|
||||
actions.Add(Action("simulation.pause", "Pause", "SetSimulationRate", "simulation", "terminus-clock", ("scalarValue", "0")));
|
||||
actions.Add(Action("simulation.step", "Advance One Step", "AdvanceSimulationStep", "simulation", "terminus-clock"));
|
||||
actions.Add(Action("simulation.rate.quarter", "Quarter Speed", "SetSimulationRate", "simulation", "terminus-clock", ("scalarValue", "0.25")));
|
||||
actions.Add(Action("simulation.rate.half", "Half Speed", "SetSimulationRate", "simulation", "terminus-clock", ("scalarValue", "0.5")));
|
||||
actions.Add(Action("simulation.rate.realtime", "Real Time", "SetSimulationRate", "simulation", "terminus-clock", ("scalarValue", "1")));
|
||||
|
|
@ -84,10 +86,72 @@ namespace GameCult.Aetheria.State.Verse
|
|||
actions.AddRange((entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>()).Select((_, index) => Action($"weapon-group.{index}.fire", $"Fire Weapon Group {index + 1}", "FireWeaponGroup", "weapon-group", $"{run.CurrentEntityKey}#weapon-group/{index}", ("weaponGroup", index.ToString()))));
|
||||
actions.AddRange((entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).Where(slot => slot?.Item != null).Select((slot, index) => Action($"equipment.{index}.activate", $"Activate {slot.Item.ItemKey}", "SetBehaviorActive", "equipment", $"{run.CurrentEntityKey}#equipment/{index}", ("equipmentIndex", index.ToString()), ("behaviorIndex", "0"), ("active", "true"))));
|
||||
actions.AddRange((entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).SelectMany(bay => bay.Items).Where(slot => slot?.Item != null).Select((slot, index) => Action($"cargo.{slot.Item.ItemKey}.{index}.use", $"Use {slot.Item.ItemKey}", "ActivateConsumable", "consumable", $"{run.CurrentEntityKey}#cargo/{index}", ("itemKey", slot.Item.ItemKey))));
|
||||
actions.AddRange(TradeActions(run, entity, catalog));
|
||||
}
|
||||
return new AetheriaRuntimeInputCapabilityDocument { Version = frame?.FrameId ?? 0, Actions = actions.ToArray(), DefaultProfiles = BuildDefaultProfiles(actions) };
|
||||
}
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeInputActionDocument> TradeActions(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (catalog == null)
|
||||
yield break;
|
||||
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == run.CurrentZoneIndex);
|
||||
var parent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null &&
|
||||
(candidate.DockingBayAssignments ?? Array.Empty<int>()).Contains(entity.EntityIndex));
|
||||
if (parent == null)
|
||||
yield break;
|
||||
|
||||
var stationKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
|
||||
run.RunId,
|
||||
run.CurrentZoneIndex,
|
||||
parent.EntityIndex);
|
||||
var cargo = parent.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
||||
for (var bayIndex = 0; bayIndex < cargo.Count; bayIndex++)
|
||||
{
|
||||
foreach (var slot in cargo[bayIndex]?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
{
|
||||
var itemKey = slot?.Item?.ItemKey ?? "";
|
||||
var typedItem = catalog.FindItem(itemKey);
|
||||
if (typedItem == null || slot?.Item == null || slot.Item.Quantity <= 0)
|
||||
continue;
|
||||
var value = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
|
||||
typedItem,
|
||||
slot.Item,
|
||||
catalog.TradeValueSettings).Price;
|
||||
var createsShip = !string.IsNullOrWhiteSpace(typedItem.HullType);
|
||||
var canReceive = createsShip
|
||||
? (parent.DockingBayAssignments ?? Array.Empty<int>()).Any(index => index < 0)
|
||||
: AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(entity, catalog, itemKey, 0) > 0;
|
||||
var action = Action(
|
||||
$"trade.buy.{StableToken(itemKey)}.{bayIndex}.{slot.X}.{slot.Y}",
|
||||
$"Buy {typedItem.Name} ({value})",
|
||||
"TradePurchase",
|
||||
"trade",
|
||||
$"{stationKey}#cargo/{bayIndex}/{slot.X}/{slot.Y}",
|
||||
("itemKey", itemKey),
|
||||
("quantity", "1"),
|
||||
("stationEntityKey", stationKey),
|
||||
("stationCargoIndex", bayIndex.ToString()),
|
||||
("targetEntityKey", run.CurrentEntityKey),
|
||||
("targetCargoIndex", "0"),
|
||||
("sourceX", slot.X.ToString()),
|
||||
("sourceY", slot.Y.ToString()));
|
||||
action.Availability = value >= 0 && run.Credits >= value && canReceive
|
||||
? "available"
|
||||
: "unavailable";
|
||||
yield return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string StableToken(string value) =>
|
||||
new string((value ?? "").Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '-').ToArray()).Trim('-');
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeInputActionDocument> CoreActions()
|
||||
{
|
||||
yield return Action("pilot.scoop", "Scoop", "SetTractorPower", "ship", "pilot", ("scalarValue", "1"));
|
||||
|
|
|
|||
|
|
@ -255,6 +255,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
equipped-item and active-consumable behavior instances through narrow
|
||||
behavior-owned restore methods; construction/loadout blueprints do not own
|
||||
behavior replay.
|
||||
- Passive contact reach is daemon-owned and derived from enabled installed
|
||||
`Sensor` behavior sensitivity. Arrays combine by root-sum-square, so larger
|
||||
hulls can buy reach by devoting real equipment volume, mass, power, cooling,
|
||||
and computation to sensing. Entity kind is not an input. Cargo is not
|
||||
installed equipment. Contact refresh consumes this derived reach but does
|
||||
not write `Visibility`; emitted signature remains owned by thermal,
|
||||
propulsion, weapons, reflectors, explicit visibility sources, and pings.
|
||||
`EntityConstructionBlueprint` still exists for construction/loadout projection,
|
||||
but those live hull grids and session scalars are not blueprint fields and
|
||||
cannot be restored by the old blueprint projector path.
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Thermal/energy | cell conduction, capacitor/reactor/radiator network, and cockpit thermal exposure | daemon | ship/equipment power, temperature, medical state, risk and death feedback | schematic/post effects | owner-exists; wired; exclusive; projected; generic timing and native asset delivery lowered: catalog hull and installed-item shapes derive cell mass/conductivity/topology; local temperature derives thermal performance, wear potential and online state; all consumers share capacitor-first/reactor-residual transactions; powered radiators pump before reactor settlement; reactor heat then enters the same cell network before conduction/radiation; cockpit-cell temperature drives the fossil nonlinear heatstroke/hypothermia accumulation, linear recovery, severe-risk crossings, and ordinary exactly-once destruction with a typed cause; Eve exposes cockpit, risk, heatstroke source-weight, phasing and death-transition facts; EveUnity generically resolves the original severe pulse plus live/reconnect-safe cause-specific death crossfade; Aetheria deterministically translates the five broken legacy profiles into clean URP profiles, advertises them by semantic role through CultMesh CDN, and binds them in the canonical client through a weight-only native sink; rotated equipment topology, parent/child radiation, schematic composition, legacy SSR/AO/roundness equivalents, canonical render-pipeline activation and rendered parity remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, generic schematic meters, renderer-feature parity, active URP capture and live/reconnect death-fade screenshots |
|
||||
| Weapons | ServerShared behaviors + Unity effects | daemon plus Ymir geometry/kinematics | action capability, weapon state, shot/damage receipts | effects/audio/HUD | instant and constant resource/timing lifecycles are daemon-owned; charged weapons accept one semantic request, spool without requiring a solution, hold at full charge, commit automatically when a solution appears, and perform deterministic once-per-second malfunction hazard checks after the authored safe hold interval with increasing persisted risk; generic Eve `weapon.state` exposes charge hold/risk and feedback exposes readiness, commit, refusal, and malfunction chronology; committed direct damage now enters the shared daemon armor-cell transaction | guided persistent payloads, visibility/wear, remaining impact-distribution parity, and Unity effect reconciliation |
|
||||
| Destruction/drops | hull death, 25% equipment rolls, unconditional cargo drops, 25-unit launch, 30-second pickups | daemon | destruction identity, death/drop events, typed pickups | provider-advertised destruction effect and pickups | owner-exists; wired; exclusive; projected: direct and deployable lethal damage commit one deterministic destruction transaction, clear live references, retain only a non-rendered identity tombstone, and emit exact pickups/events; the reserved client `DestroyEntity` command is rejected; Aetheria advertises its original big explosion as `effect.feedback.entity.destroyed`, lowered generically by EveUnity | equipment-destruction-before-hull parity and multiplayer reconnect chronology |
|
||||
| Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | pickup world entity plus beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; shield contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; 30s daemon lifetime; instant cargo teleport deleted; current-zone pickups now occupy ordinary rows in the shared Eve entity SoA body with stable logical pickup identity and the provider-owned pickup asset; the generic Unity witness sees a paused deterministic salvage pickup, flies through it, and observes daemon/Ymir removal | add generic tractor beam plus rejected-capacity feedback and cargo-delta assertion |
|
||||
| Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | live generic Unity witness now proves advertised dock/undock actions, daemon bay assignment, dock-parent inclusion in the SoA view, station sensor-contact sharing, generic camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock; the starter station and local raider form a deterministic Terminus departure proof | restore fossil component-prerequisite rejection reasons, exact undock placement, wormhole priority, reconnect, and docked inventory/trade surfaces |
|
||||
| Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | pickup world entity plus beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; shield contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; 30s daemon lifetime; instant cargo teleport deleted; current-zone pickups occupy ordinary rows in the shared Eve entity SoA body with stable logical identity and provider-owned assets; the generic Unity witness activates scoop while paused, advances fixed daemon steps, and observes Ymir contact remove the starter salvage | add generic tractor beam plus rejected-capacity feedback and explicit cargo-delta witness fact |
|
||||
| Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | live generic Unity witness now proves advertised dock/undock actions, daemon bay assignment, dock-parent inclusion in the SoA view, sharing of the dock parent's equipment-derived contact picture, generic camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock; the starter station and local raider form a deterministic Terminus departure proof | restore fossil component-prerequisite rejection reasons, exact undock placement, wormhole priority, reconnect, and docked inventory/trade surfaces |
|
||||
| Inventory/equipment | inventory panels + ServerShared | daemon | inventory/equipment surfaces | generic UI | owner-exists; wired/projected in fragments; not proven | all transfer directions and rejection |
|
||||
| Trade | trade UI/economy | daemon | stock, quote, purchase commands | generic UI | purchase owner wired/projected; not proven | atomic purchase and capacity |
|
||||
| Trade | trade UI/economy | daemon | stock, quote, purchase commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; Eve advertises only currently available stock actions; generic Unity live witness receives an authoritative purchase receipt while paused | sell flow, quote expiry, contested stock, and full trade surface parity |
|
||||
| Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; dynamic original behavior source absent | dynamic equipment/cargo actions |
|
||||
| Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | basic follow lowered; dock/view parity absent | authored framing and dock transition |
|
||||
| Render channels | Unity layers/culling masks | provider variant + runtime | semantic view id | native layer policy | specification work; incomplete uncommitted prototype | no map glyphs in pilot view |
|
||||
|
|
@ -39,10 +39,10 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, authoritative weapon reload chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; refusal identity, authored weapon, target, reason, and remaining magazine state are runtime-independent cockpit inputs | add lock transitions, thermal and docking event families, negative transaction fixtures, and runtime effect consumption |
|
||||
| Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | wired; basic lowering; not live-proven remotely | cold/warm remote-only live load and cache proof |
|
||||
| Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | owner-exists; not parity-proven | mid-loop restart |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | partial live scenario proof: generic Unity discovers the provider, lowers the pilot world and assets, flies through Ymir, sees and collects daemon-authored salvage, pauses, queues targeting/fire, resumes, and presents the authoritative shot and receipts | destruction-created loot, dock, and trade remain before the minimum loop is proven |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome | destruction-created loot pickup, richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; shared evaluated-stat query proven; execution families remain partial | every behavior family, ordering, resource transaction and modifier lifecycle |
|
||||
| Sensors/visibility | decaying sources and observer information thresholds | daemon | contacts and ping events | indicators/maps | reference | threshold, decay, ping and classification timelines |
|
||||
| Sensors/visibility | installed sensor sensitivity, decaying signature sources, and observer information thresholds | daemon | contacts and ping events | indicators/maps | passive reach now derives from enabled installed Sensor behaviors with diminishing-return array composition regardless of entity kind; contact refresh no longer overwrites emitted visibility; the old station range survives only as obsolete snapshot compatibility | author station/ship sensor loadout budgets, then prove power/thermal shutdown, threshold decay, ping, and classification timelines |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines | daemon | commander task board, corporation survey ledger, and shared semantic controls | Eve commander/pilot clients | point exploration, attack with optimum-range control, same-zone haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home wired through shared commands; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing; capability/priority assignment and rejection behavior proven; generic Electron commander lowering proves the worker roster | cross-zone logistics execution, cancellation, reconnect, and multi-client command transfer |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions and active effects | action bar/schematic | reference | consume, stacking, duration and cleanup |
|
||||
|
||||
|
|
|
|||
|
|
@ -103,11 +103,19 @@ consume another configured cargo item.
|
|||
## Sensors And Visibility
|
||||
|
||||
Visibility sources are independently identified and decayed. Passive sensors
|
||||
accumulate observer-relative information from sensitivity, signature, and
|
||||
range. Threshold transitions create/remove classified contacts. Ping consumes
|
||||
accumulate observer-relative information from installed sensor sensitivity,
|
||||
target signature, and distance. Passive reach is derived from enabled installed
|
||||
sensor arrays; multiple arrays combine with diminishing returns. Stations have
|
||||
no class bonus: their advantage comes from spending hull volume, mass, power,
|
||||
cooling, and computation on sensor equipment ships cannot afford. Cargo-held
|
||||
sensors contribute nothing. Threshold transitions create/remove classified contacts. Ping consumes
|
||||
energy, raises source visibility, expands in space, and grants information on
|
||||
crossing. Hostility uses faction ownership, security, and presence rules.
|
||||
|
||||
Sensor reach and emitted visibility are separate quantities. Contact refresh
|
||||
must never overwrite the thermal, propulsion, weapon, reflector, and ping
|
||||
sources that make an entity detectable.
|
||||
|
||||
## AI
|
||||
|
||||
Agents own root/current state, task, controls, acceleration, and transitions.
|
||||
|
|
|
|||
|
|
@ -176,6 +176,13 @@ Visibility is simulated and observer-relative. Sensors accumulate information,
|
|||
contacts distinguish visible/known/hostile state, pings alter visibility, and
|
||||
target selection is constrained by available contacts and game rules.
|
||||
|
||||
Passive sensor capability belongs to installed equipment, not to the `station`
|
||||
or `ship` entity kind. A station generally sees farther because its size and
|
||||
mass budget supports more and larger arrays together with the required power,
|
||||
cooling, and computation. An equivalently equipped ship and station have the
|
||||
same capability. Docked craft may consume the station's contact picture, but
|
||||
do not inherit a magical range value.
|
||||
|
||||
The pilot can select explicitly, cycle or choose nearest targets, and obtain a
|
||||
lock where required by equipment. The UI distinguishes target existence,
|
||||
visibility to target, target visibility, hostility, lock progress, shields, and
|
||||
|
|
|
|||
|
|
@ -99,6 +99,12 @@ compression so a physically larger world does not require the player to watch
|
|||
uneventful coasting in real time. Supported requested rates currently range from
|
||||
pause through `128x`; Starbridge remains real-time-owned.
|
||||
|
||||
While paused, `AdvanceSimulationStep` advances exactly one fixed daemon step
|
||||
without changing the persistent requested rate. Eve advertises this as
|
||||
`simulation.step`, giving players, bots, and conformance fixtures a precise
|
||||
tactical inspection primitive instead of briefly unpausing and racing the
|
||||
publication cadence.
|
||||
|
||||
The requested simulation rate is not a renderer time scale. The daemon advances
|
||||
the deterministic fixed-step simulation repeatedly and publishes one coherent
|
||||
sample of the resulting state. Unity, Electron, bots, and other Eve clients may
|
||||
|
|
|
|||
Loading…
Reference in a new issue