Restore daemon energy and thermal network
This commit is contained in:
parent
22dae9adbe
commit
7fdfdff99a
11 changed files with 905 additions and 77 deletions
|
|
@ -28,6 +28,10 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
PickupIsCapacityCheckedExactlyOnceAndExpires();
|
||||
PickupShieldContactCollectsOrBounces();
|
||||
ThermalCellsUseFossilConductionAndRadiation();
|
||||
EnergyNetworkSettlesReactorAfterConsumers();
|
||||
RadiatorPumpsHeatBeforeReactorSettlement();
|
||||
EquipmentThermalPerformanceOwnsShutdownAndWear();
|
||||
ThermalTopologyComesFromHullAndEquipment();
|
||||
MultipleActorsUseTheSameMovementLever();
|
||||
AgentClaimsAndCompletesExploreTaskThroughCommands();
|
||||
SchedulerAssignsHighestPriorityCompatibleTask();
|
||||
|
|
@ -58,14 +62,16 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
source.DirectionY = 0;
|
||||
source.TargetEntityIndex = 2;
|
||||
source.WeaponGroups = [new[] { 0 }];
|
||||
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-beam", Quality = 1, Durability = 1, Enabled = true }
|
||||
}];
|
||||
source.Equipment =
|
||||
[
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-beam", Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-beam-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
source.BehaviorStates = [new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = "fixture", OwnerIndex = 0, BehaviorIndex = 0,
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
||||
BehaviorKind = "Capacitor", CapacitorCharge = 10, CapacitorCapacity = 10, CapacitorEfficiency = 1
|
||||
}];
|
||||
source.CargoContents = [Cargo(("beam-ammo", 1, 0, 0))];
|
||||
|
|
@ -89,7 +95,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
|
||||
new AetheriaRuntimeBehaviorField(17, Number(0.15))
|
||||
]);
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([CatalogItem("test-beam", payload)], [], []);
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[CatalogItem("test-beam", payload), CatalogItem("test-beam-capacitor", CapacitorPayload(10, 1))], [], []);
|
||||
var intents = new AetheriaRuntimeDaemonIntentState();
|
||||
intents.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
||||
{
|
||||
|
|
@ -382,14 +389,16 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
{
|
||||
var source = Entity(0, -50, "player");
|
||||
var target = Entity(1, 0, "neutral");
|
||||
target.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-energy-shield", Quality = 1, Durability = 1, Enabled = true }
|
||||
}];
|
||||
target.Equipment =
|
||||
[
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-energy-shield", Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-shield-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
target.BehaviorStates = [new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = "fixture", OwnerIndex = 0, BehaviorIndex = 0,
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
||||
BehaviorKind = "Capacitor", CapacitorCharge = 100, CapacitorCapacity = 100,
|
||||
CapacitorEfficiency = 1
|
||||
}];
|
||||
|
|
@ -399,7 +408,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(3))
|
||||
]);
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[CatalogItem("test-energy-shield", shield)], [], []);
|
||||
[CatalogItem("test-energy-shield", shield), CatalogItem("test-shield-capacitor", CapacitorPayload(100, 1))], [], []);
|
||||
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] };
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
|
|
@ -959,6 +968,13 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
|
||||
private static AetheriaRuntimeBehaviorPayload CapacitorPayload(double capacity, double efficiency) => new(
|
||||
0, "Capacitor", 0,
|
||||
[
|
||||
new AetheriaRuntimeBehaviorField(1, PerformanceStat(capacity)),
|
||||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(efficiency))
|
||||
]);
|
||||
|
||||
private static AetheriaRuntimeCatalogItem CatalogItem(string itemKey, params AetheriaRuntimeBehaviorPayload[] payloads) => new(
|
||||
itemKey, itemKey, "equipment", "", "", 0, 1, 1, 1, 1,
|
||||
1, 1, 1, Array.Empty<AetheriaRuntimeShapeCell>(),
|
||||
|
|
@ -1216,13 +1232,15 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
{
|
||||
ItemKey = "test-lock-cannon", Quality = 1, Durability = 1, Enabled = true
|
||||
}
|
||||
}
|
||||
},
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-attack-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
agent.BehaviorStates =
|
||||
[
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = "fixture", OwnerIndex = 0, BehaviorIndex = 0,
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
||||
BehaviorKind = "Capacitor", CapacitorCharge = 50, CapacitorCapacity = 50, CapacitorEfficiency = 1
|
||||
}
|
||||
];
|
||||
|
|
@ -1255,7 +1273,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
||||
]);
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[CatalogItem("test-lock-cannon", weaponPayload)],
|
||||
[CatalogItem("test-lock-cannon", weaponPayload), CatalogItem("test-attack-capacitor", CapacitorPayload(50, 1))],
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var target = Entity(1, 105, "raider");
|
||||
|
|
@ -1875,6 +1893,223 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
return command;
|
||||
}
|
||||
|
||||
private static void EnergyNetworkSettlesReactorAfterConsumers()
|
||||
{
|
||||
var entity = Entity(0, 0, "player");
|
||||
entity.Equipment =
|
||||
[
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "ledger-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "ledger-reactor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
entity.BehaviorStates =
|
||||
[
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = "Capacitor",
|
||||
CapacitorCharge = 2, CapacitorCapacity = 10, CapacitorEfficiency = 0.5
|
||||
},
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 1, BehaviorIndex = 0, BehaviorKind = "Reactor"
|
||||
}
|
||||
];
|
||||
entity.StatGrids =
|
||||
[
|
||||
Grid("hull", 100), Grid("shield", 0), Grid("heat", 280),
|
||||
Grid("temperature", 280), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
||||
];
|
||||
var capacitorItem = CatalogItem("ledger-capacitor", CapacitorPayload(10, 0.5));
|
||||
capacitorItem.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
var reactorPayload = new AetheriaRuntimeBehaviorPayload(0, "Reactor", 100,
|
||||
[
|
||||
new AetheriaRuntimeBehaviorField(1, PerformanceStat(4)),
|
||||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)),
|
||||
new AetheriaRuntimeBehaviorField(3, PerformanceStat(0.5)),
|
||||
new AetheriaRuntimeBehaviorField(4, PerformanceStat(2))
|
||||
]);
|
||||
var reactorItem = CatalogItem("ledger-reactor", reactorPayload);
|
||||
reactorItem.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([capacitorItem, reactorItem], [], []);
|
||||
|
||||
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
||||
Require(AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, 5),
|
||||
"online reactor must accept residual demand after capacitors drain");
|
||||
var capacitor = entity.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor");
|
||||
var reactor = entity.BehaviorStates.Single(value => value.BehaviorKind == "Reactor");
|
||||
RequireNear(0, capacitor.CapacitorCharge, 0.000001,
|
||||
"consumers must drain capacitors before adding reactor draw");
|
||||
RequireNear(3, reactor.ReactorDraw, 0.000001,
|
||||
"residual demand must accumulate on the last-running reactor");
|
||||
|
||||
AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, 1);
|
||||
RequireNear(1, capacitor.CapacitorCharge, 0.000001,
|
||||
"baseline reactor surplus must refill non-full capacitors after demand settles");
|
||||
RequireNear(0, reactor.ReactorDraw, 0.000001,
|
||||
"reactor settlement must consume and reset the per-tick demand ledger");
|
||||
RequireNear(283.5, GridValue(entity, "temperature", 0), 0.000001,
|
||||
"capacitor discharge/refill losses and reactor baseline heat must reach canonical item cells");
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "energy-ledger-smoke", CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0",
|
||||
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
||||
};
|
||||
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
||||
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
||||
new AetheriaRuntimeDaemonHealthDocument(),
|
||||
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
||||
var worldEntity = Flatten(surface.Surface.Root).Single(node =>
|
||||
node.Id == "aetheria.daemon.game.world.entity.0");
|
||||
RequireEqual("1", worldEntity.Props["capacitorCharge"],
|
||||
"Eve must expose the settled canonical capacitor charge");
|
||||
RequireEqual("0", worldEntity.Props["reactorDraw"],
|
||||
"Eve must expose the reset post-settlement reactor ledger");
|
||||
}
|
||||
|
||||
private static void RadiatorPumpsHeatBeforeReactorSettlement()
|
||||
{
|
||||
var entity = Entity(0, 0, "player");
|
||||
entity.HeatsinksEnabled = true;
|
||||
entity.Equipment =
|
||||
[
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "radiator-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "radiator", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
entity.BehaviorStates =
|
||||
[
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = "Capacitor", CapacitorCharge = 10
|
||||
},
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 1, BehaviorIndex = 0, BehaviorKind = "Radiator", RadiatorTemperature = 280
|
||||
}
|
||||
];
|
||||
entity.StatGrids =
|
||||
[
|
||||
Grid("hull", 100), Grid("shield", 0), Grid("heat", 400),
|
||||
Grid("temperature", 400), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
||||
];
|
||||
var capacitor = CatalogItem("radiator-capacitor", CapacitorPayload(10, 1));
|
||||
capacitor.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
var radiatorPayload = new AetheriaRuntimeBehaviorPayload(0, "Radiator", 0,
|
||||
[
|
||||
new AetheriaRuntimeBehaviorField(1, PerformanceStat(0)),
|
||||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(1)),
|
||||
new AetheriaRuntimeBehaviorField(3, Number(300)),
|
||||
new AetheriaRuntimeBehaviorField(4, PerformanceStat(0.5)),
|
||||
new AetheriaRuntimeBehaviorField(5, PerformanceStat(1)),
|
||||
new AetheriaRuntimeBehaviorField(6, PerformanceStat(10))
|
||||
]);
|
||||
var radiator = CatalogItem("radiator", radiatorPayload);
|
||||
radiator.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([capacitor, radiator], [], []);
|
||||
|
||||
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 1);
|
||||
|
||||
RequireNear(9, entity.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
|
||||
0.000001, "radiator pump must consume authored energy before reactor settlement");
|
||||
RequireNear(300.5, GridValue(entity, "temperature", 0), 0.000001,
|
||||
"radiator must remove pumped heat and return authored waste heat to its item cells");
|
||||
RequireNear(290, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").RadiatorTemperature,
|
||||
0.000001, "pumped heat must accumulate in radiator thermal mass before radiation");
|
||||
}
|
||||
|
||||
private static void EquipmentThermalPerformanceOwnsShutdownAndWear()
|
||||
{
|
||||
var entity = Entity(0, 0, "player");
|
||||
entity.ShutdownPerformance = 0.25;
|
||||
var item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "thermal-wear-item", Quality = 0, Durability = 10, Enabled = true };
|
||||
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = item }];
|
||||
entity.StatGrids =
|
||||
[
|
||||
Grid("hull", 100), Grid("shield", 0), Grid("heat", 300),
|
||||
Grid("temperature", 300), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
||||
];
|
||||
var wearPayload = new AetheriaRuntimeBehaviorPayload(0, "Wear", 0,
|
||||
[new AetheriaRuntimeBehaviorField(1, new AetheriaRuntimeBehaviorValue(
|
||||
"bool", "", 0, true, "", "", [], []))]);
|
||||
var typed = CatalogItem("thermal-wear-item", wearPayload);
|
||||
typed.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
typed.Durability = 10;
|
||||
typed.ThermalResilience = 2;
|
||||
typed.MinimumTemperature = 200;
|
||||
typed.MaximumTemperature = 400;
|
||||
typed.ThermalPerformanceCurveKeys =
|
||||
[
|
||||
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
||||
new AetheriaRuntimeCurveKey(1, 0, 0, 0)
|
||||
];
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([typed], [], []);
|
||||
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
||||
var state = entity.EquipmentStates.Single();
|
||||
RequireNear(0.5, state.ThermalPerformance, 0.000001,
|
||||
"item thermal performance must come from mean occupied-cell temperature and authored curve");
|
||||
Require(state.ThermalOnline && state.Online,
|
||||
"item above shutdown performance with positive durability must remain online");
|
||||
RequireNear(10 - state.Wear, item.Durability, 0.000001,
|
||||
"generic Wear behavior must apply the computed thermal wear potential at its authored cadence");
|
||||
|
||||
var temperature = entity.StatGrids.Single(value => value.Name == "temperature");
|
||||
temperature.Values = [380];
|
||||
item.OverrideShutdown = false;
|
||||
entity.OverrideShutdown = true;
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
||||
state = entity.EquipmentStates.Single();
|
||||
Require(!state.ThermalOnline && !state.Online,
|
||||
"entity override alone must not bypass item thermal shutdown");
|
||||
item.OverrideShutdown = true;
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
||||
Require(entity.EquipmentStates.Single().ThermalOnline,
|
||||
"thermal shutdown bypass must require both entity-wide and item-local override");
|
||||
}
|
||||
|
||||
private static void ThermalTopologyComesFromHullAndEquipment()
|
||||
{
|
||||
var entity = Entity(0, 0, "player");
|
||||
entity.HullItemKey = "thermal-hull";
|
||||
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
X = 0, Y = 0, Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "thermal-gear", Quality = 1, Durability = 1, Enabled = true }
|
||||
}];
|
||||
var hull = HullCatalogItem("thermal-hull", 2, 1, 0);
|
||||
hull.Mass = 10;
|
||||
hull.SpecificHeat = 2;
|
||||
hull.Conductivity = 4;
|
||||
hull.InteriorShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
var gear = CatalogItem("thermal-gear");
|
||||
gear.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
||||
gear.Mass = 4;
|
||||
gear.SpecificHeat = 5;
|
||||
gear.Conductivity = 3;
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, gear], [], []);
|
||||
|
||||
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
||||
|
||||
var mass = entity.StatGrids.Single(value => value.Name == "thermal-mass");
|
||||
var conductivity = entity.StatGrids.Single(value => value.Name == "conductivity");
|
||||
RequireNear(30, mass.Values[0], 0.000001,
|
||||
"occupied cell thermal mass must include its proportional equipment mass");
|
||||
RequireNear(10, mass.Values[1], 0.000001,
|
||||
"hull thermal mass must be divided across authored hull cells");
|
||||
RequireNear(3, conductivity.Values[0], 0.000001,
|
||||
"occupied cell conductivity must come from installed equipment");
|
||||
RequireNear(280, entity.StatGrids.Single(value => value.Name == "temperature").Values[1], 0.000001,
|
||||
"authored hull topology must initialize each occupied cell at the fossil temperature");
|
||||
}
|
||||
|
||||
private static void ThermalCellsUseFossilConductionAndRadiation()
|
||||
{
|
||||
var entity = Entity(7, 0, "player");
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ RequireMainMenuVerseHostDocumentAccess(root);
|
|||
RequireMainMenuContinueRunState(root);
|
||||
RequireUnityObserverDoesNotTickLocalSimulation(root);
|
||||
RequireUnityDoesNotCallSharedSimulationTicks(root);
|
||||
RequireDaemonEnergyAndThermalAuthority(root);
|
||||
RequireUnityPhysicsIsNotGameplayAuthority(root);
|
||||
RequireDeadPropertiesPanelShellDeleted(root);
|
||||
RequireTypedBehaviorMetadataCoverage(root);
|
||||
|
|
@ -16327,6 +16328,104 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root)
|
|||
}
|
||||
}
|
||||
|
||||
static void RequireDaemonEnergyAndThermalAuthority(string root)
|
||||
{
|
||||
var runtime = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime");
|
||||
var energyPath = Path.Combine(runtime, "AetheriaRuntimeEnergySimulation.cs");
|
||||
var thermalPath = Path.Combine(runtime, "AetheriaRuntimeThermalSimulation.cs");
|
||||
var simulationPath = Path.Combine(runtime, "AetheriaRuntimeDaemonSimulation.cs");
|
||||
var snapshotPath = Path.Combine(runtime, "AetheriaRuntimeSnapshotDocuments.cs");
|
||||
var surfacePath = Path.Combine(runtime, "AetheriaRuntimeDaemonGameSurfaceBuilder.cs");
|
||||
var operationsPath = Path.Combine(runtime, "AetheriaRuntimeDaemonOperations.cs");
|
||||
|
||||
var sources = new Dictionary<string, string>
|
||||
{
|
||||
[energyPath] = File.Exists(energyPath) ? File.ReadAllText(energyPath) : "",
|
||||
[thermalPath] = File.Exists(thermalPath) ? File.ReadAllText(thermalPath) : "",
|
||||
[simulationPath] = File.Exists(simulationPath) ? File.ReadAllText(simulationPath) : "",
|
||||
[snapshotPath] = File.Exists(snapshotPath) ? File.ReadAllText(snapshotPath) : "",
|
||||
[surfacePath] = File.Exists(surfacePath) ? File.ReadAllText(surfacePath) : ""
|
||||
};
|
||||
var required = new Dictionary<string, string[]>
|
||||
{
|
||||
[energyPath] = new[]
|
||||
{
|
||||
"public static class AetheriaRuntimeEnergySimulation",
|
||||
"public static bool TryConsume(",
|
||||
"public static void StepRadiators(",
|
||||
"public static void SettleReactors(",
|
||||
"AddCapacitorCharge("
|
||||
},
|
||||
[thermalPath] = new[]
|
||||
{
|
||||
"public static void EnsureTopology(",
|
||||
"public static void UpdateEquipmentStates(",
|
||||
"public static void AddHeatToEquipment(",
|
||||
"public static void ApplyWear("
|
||||
},
|
||||
[simulationPath] = new[]
|
||||
{
|
||||
"AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog)",
|
||||
"AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, deltaSeconds)",
|
||||
"AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog)",
|
||||
"AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds)",
|
||||
"AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds)",
|
||||
"AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog)",
|
||||
"AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand)"
|
||||
},
|
||||
[snapshotPath] = new[]
|
||||
{
|
||||
"public IReadOnlyList<AetheriaRuntimeEquipmentStateCommit> EquipmentStates",
|
||||
"public sealed class AetheriaRuntimeEquipmentStateCommit",
|
||||
"public double ThermalPerformance",
|
||||
"public double Wear",
|
||||
"public bool Online"
|
||||
},
|
||||
[surfacePath] = new[]
|
||||
{
|
||||
"[\"capacitorCharge\"]",
|
||||
"[\"reactorDraw\"]",
|
||||
"[\"radiatorTemperature\"]",
|
||||
"[\"minimumEquipmentThermalPerformance\"]",
|
||||
"[\"offlineEquipmentCount\"]"
|
||||
}
|
||||
};
|
||||
var missing = required.SelectMany(pair => pair.Value
|
||||
.Where(symbol => !sources[pair.Key].Contains(symbol, StringComparison.Ordinal))
|
||||
.Select(symbol => $"{Path.GetRelativePath(root, pair.Key)}: missing {symbol}"))
|
||||
.ToArray();
|
||||
if (missing.Length > 0)
|
||||
throw new InvalidOperationException(
|
||||
"Daemon energy and thermal authority must execute one catalog-derived network and publish its facts through Eve: " +
|
||||
string.Join("; ", missing));
|
||||
|
||||
var operations = File.Exists(operationsPath) ? File.ReadAllText(operationsPath) : "";
|
||||
var forbiddenOperations = new[]
|
||||
{
|
||||
"ApplyCommitEnergy(",
|
||||
"ApplySetHeat(",
|
||||
"ApplySetEquipmentOnline(",
|
||||
"ApplyEquipmentWear("
|
||||
};
|
||||
var surviving = forbiddenOperations.Where(symbol => operations.Contains(symbol, StringComparison.Ordinal)).ToArray();
|
||||
if (surviving.Length > 0)
|
||||
throw new InvalidOperationException(
|
||||
"Client commands may request semantic behavior but may not directly write daemon energy, heat, wear, or equipment online state: " +
|
||||
string.Join(", ", surviving));
|
||||
|
||||
var simulation = sources[simulationPath];
|
||||
var ordered = new[]
|
||||
{
|
||||
"AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds)",
|
||||
"AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds)",
|
||||
"AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog)"
|
||||
};
|
||||
if (!(simulation.IndexOf(ordered[0], StringComparison.Ordinal) < simulation.IndexOf(ordered[1], StringComparison.Ordinal) &&
|
||||
simulation.IndexOf(ordered[1], StringComparison.Ordinal) < simulation.IndexOf(ordered[2], StringComparison.Ordinal)))
|
||||
throw new InvalidOperationException(
|
||||
"Daemon tick order must pump radiators before reactor settlement, then conduct and radiate the resulting cell heat.");
|
||||
}
|
||||
|
||||
static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
|
||||
{
|
||||
var checkedRoots = new[]
|
||||
|
|
|
|||
|
|
@ -653,6 +653,23 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var thermalVisibility = EntityStat(entity, "thermal-visibility");
|
||||
var maximumHull = MaximumHull(entity, simulationSettings);
|
||||
var maximumShield = MaximumShield(entity, simulationSettings);
|
||||
var behaviorStates = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
var capacitorCharge = behaviorStates.Where(value => value != null && value.BehaviorKind == "Capacitor")
|
||||
.Sum(value => Math.Max(0, value.CapacitorCharge));
|
||||
var capacitorCapacity = behaviorStates.Where(value => value != null && value.BehaviorKind == "Capacitor")
|
||||
.Sum(value => Math.Max(0, value.CapacitorCapacity));
|
||||
var reactorDraw = behaviorStates.Where(value => value != null && value.BehaviorKind == "Reactor")
|
||||
.Sum(value => value.ReactorDraw);
|
||||
var reactorLoad = behaviorStates.Where(value => value != null && value.BehaviorKind == "Reactor")
|
||||
.Select(value => value.ReactorLoadRatio).DefaultIfEmpty(0).Max();
|
||||
var radiatorTemperature = behaviorStates.Where(value => value != null && value.BehaviorKind == "Radiator")
|
||||
.Select(value => value.RadiatorTemperature).DefaultIfEmpty(0).Max();
|
||||
var equipmentStates = entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>();
|
||||
var minimumThermalPerformance = equipmentStates.Where(value => value != null)
|
||||
.Select(value => value.ThermalPerformance).DefaultIfEmpty(1).Min();
|
||||
var maximumWear = equipmentStates.Where(value => value != null)
|
||||
.Select(value => value.Wear).DefaultIfEmpty(0).Max();
|
||||
var offlineEquipmentCount = equipmentStates.Count(value => value != null && !value.Online);
|
||||
var weaponCooldown = (entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
|
||||
.Select(state => state?.CooldownProgress ?? 0)
|
||||
.DefaultIfEmpty(0)
|
||||
|
|
@ -683,6 +700,16 @@ namespace GameCult.Aetheria.State.Verse
|
|||
["shield"] = FormatNumber(shield),
|
||||
["maximumShield"] = FormatNumber(maximumShield),
|
||||
["shieldRatio"] = FormatRatio(shield, maximumShield),
|
||||
["capacitorCharge"] = FormatNumber(capacitorCharge),
|
||||
["capacitorCapacity"] = FormatNumber(capacitorCapacity),
|
||||
["capacitorRatio"] = FormatRatio(capacitorCharge, capacitorCapacity),
|
||||
["reactorDraw"] = FormatNumber(reactorDraw),
|
||||
["reactorLoadRatio"] = FormatNumber(reactorLoad),
|
||||
["radiatorTemperature"] = FormatNumber(radiatorTemperature),
|
||||
["radiatorVisibility"] = FormatNumber(EntityStat(entity, "radiator-visibility")),
|
||||
["minimumEquipmentThermalPerformance"] = FormatNumber(minimumThermalPerformance),
|
||||
["maximumEquipmentWear"] = FormatNumber(maximumWear),
|
||||
["offlineEquipmentCount"] = offlineEquipmentCount.ToString(CultureInfo.InvariantCulture),
|
||||
["heat"] = FormatNumber(heat),
|
||||
["meanTemperature"] = FormatNumber(heat),
|
||||
["minimumTemperature"] = FormatNumber(minimumTemperature),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
EnsureStats(entities, settings, catalog);
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
||||
AetheriaRuntimeThermalSimulation.EnsureState(entity);
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
||||
}
|
||||
foreach (var movement in intents?.Movements ?? Enumerable.Empty<AetheriaRuntimeDaemonMovementIntent>())
|
||||
ApplyMovementIntent(run, entities, movement, settings);
|
||||
StepTractorPower(entities, deltaSeconds);
|
||||
|
|
@ -300,7 +305,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
continue;
|
||||
if (weapon.SingleAmmoBurst)
|
||||
{
|
||||
var triggerResult = CommitWeaponRound(attacker, weapon);
|
||||
var triggerResult = CommitWeaponRound(attacker, weapon, catalog);
|
||||
if (triggerResult == WeaponRoundResult.ReloadStarted)
|
||||
AppendWeaponEvent(run, zone, attacker, weapon, frameId, "weapon.reload.started");
|
||||
PublishWeaponRoundResult(run, zone, attacker, weapon, frameId, triggerResult);
|
||||
|
|
@ -318,7 +323,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
if (!weapon.SingleAmmoBurst)
|
||||
{
|
||||
var roundResult = CommitWeaponRound(attacker, weapon);
|
||||
var roundResult = CommitWeaponRound(attacker, weapon, catalog);
|
||||
if (roundResult == WeaponRoundResult.ReloadStarted)
|
||||
AppendWeaponEvent(run, zone, attacker, weapon, frameId, "weapon.reload.started");
|
||||
PublishWeaponRoundResult(run, zone, attacker, weapon, frameId, roundResult);
|
||||
|
|
@ -334,7 +339,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
CommitShotResolution(run, zone, attacker, target, weapon, shotId, frameId, catalog, settings);
|
||||
AppendShotCommittedEvent(run, zone, attacker, target, weapon, shotId, frameId);
|
||||
weapon.State.Firing = true;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.Heat);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
weapon.State.OwnerIndex, weapon.Heat);
|
||||
ApplyWeaponWear(attacker, weapon.State, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,7 +373,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds);
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog);
|
||||
entity.IsActive = IsAlive(entity);
|
||||
if (!entity.IsActive)
|
||||
{
|
||||
|
|
@ -395,7 +404,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
weapon.State.CoolingDown = weapon.State.CooldownProgress > 0;
|
||||
if (!IsAlive(attacker) || weaponGroup < 0 || weapon.State.CooldownProgress > 0) continue;
|
||||
|
||||
var result = CommitWeaponRound(attacker, weapon);
|
||||
var result = CommitWeaponRound(attacker, weapon, catalog);
|
||||
PublishWeaponRoundResult(run, zone, attacker, weapon, frameId, result);
|
||||
if (result != WeaponRoundResult.Fired) continue;
|
||||
|
||||
|
|
@ -434,7 +443,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
weapon.State.Firing = true;
|
||||
weapon.State.CoolingDown = true;
|
||||
weapon.State.CooldownProgress = weapon.Cooldown;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.Heat);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
weapon.State.OwnerIndex, weapon.Heat);
|
||||
ApplyWeaponWear(attacker, weapon.State, 1);
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit { EventId = $"physical-payload:{payloadId}:deployed", Kind = "deployable.deployed", FrameId = frameId, ZoneIndex = zone.ZoneIndex, SourceEntityIndex = attacker.EntityIndex, SubjectKey = payloadId, ItemKey = weapon.ItemKey, PositionX = attacker.PositionX, PositionZ = attacker.PositionZ });
|
||||
}
|
||||
}
|
||||
|
|
@ -552,15 +563,16 @@ namespace GameCult.Aetheria.State.Verse
|
|||
if (!state.Charged)
|
||||
{
|
||||
var energy = weapon.ChargeEnergy * step;
|
||||
if (!CanSupplyEnergy(attacker, energy))
|
||||
if (!CanSupplyEnergy(attacker, catalog, energy))
|
||||
{
|
||||
state.Charging = false;
|
||||
state.Charge = 0;
|
||||
PublishChargedRefusal(run, zone, attacker, weapon, frameId, "insufficient-charge-energy");
|
||||
continue;
|
||||
}
|
||||
CommitEnergy(attacker, energy);
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.ChargeHeat * step);
|
||||
CommitEnergy(attacker, catalog, energy);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
weapon.Base.State.OwnerIndex, weapon.ChargeHeat * step);
|
||||
}
|
||||
state.Charge += step;
|
||||
if (!state.Charged && state.Charge >= 1)
|
||||
|
|
@ -576,7 +588,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
state.ChargeHoldSeconds = 0;
|
||||
state.ChargeRiskChecks = 0;
|
||||
state.ChargeMalfunctionRisk = 0;
|
||||
TriggerChargedShot(run, zone, attacker, weapon, frameId);
|
||||
TriggerChargedShot(run, zone, attacker, weapon, frameId, catalog);
|
||||
}
|
||||
else if (state.Charged)
|
||||
{
|
||||
|
|
@ -614,7 +626,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
if (!shot.SingleAmmoBurst)
|
||||
{
|
||||
var result = CommitWeaponRound(attacker, shot);
|
||||
var result = CommitWeaponRound(attacker, shot, catalog);
|
||||
PublishWeaponRoundResult(run, zone, attacker, shot, frameId, result);
|
||||
if (result == WeaponRoundResult.ReloadStarted)
|
||||
AppendWeaponEvent(run, zone, attacker, shot, frameId, "weapon.reload.started");
|
||||
|
|
@ -626,7 +638,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
CommitShotResolution(run, zone, attacker, solutionTarget, shot, shotId, frameId, catalog, settings);
|
||||
AppendShotCommittedEvent(run, zone, attacker, solutionTarget, shot, shotId, frameId);
|
||||
state.Firing = true;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, shot.Heat);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
shot.State.OwnerIndex, shot.Heat);
|
||||
ApplyWeaponWear(attacker, shot.State, 1);
|
||||
}
|
||||
if (state.BurstRemaining <= 0)
|
||||
{
|
||||
|
|
@ -638,12 +652,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static void TriggerChargedShot(AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone, AetheriaRuntimeEntitySnapshotCommit attacker,
|
||||
ResolvedChargedWeapon weapon, long frameId)
|
||||
ResolvedChargedWeapon weapon, long frameId, AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
var shot = weapon.CommittedShot();
|
||||
if (shot.SingleAmmoBurst)
|
||||
{
|
||||
var result = CommitWeaponRound(attacker, shot);
|
||||
var result = CommitWeaponRound(attacker, shot, catalog);
|
||||
PublishWeaponRoundResult(run, zone, attacker, shot, frameId, result);
|
||||
if (result == WeaponRoundResult.ReloadStarted)
|
||||
AppendWeaponEvent(run, zone, attacker, shot, frameId, "weapon.reload.started");
|
||||
|
|
@ -736,13 +750,13 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
var energy = weapon.Energy * deltaSeconds;
|
||||
if (!CanSupplyEnergy(attacker, energy))
|
||||
if (!CanSupplyEnergy(attacker, catalog, energy))
|
||||
{
|
||||
PublishConstantWeaponRefusal(run, zone, attacker, weapon, frameId, "insufficient-energy");
|
||||
StopConstantWeapon(run, zone, attacker, weapon, frameId, "insufficient-energy");
|
||||
continue;
|
||||
}
|
||||
CommitEnergy(attacker, energy);
|
||||
CommitEnergy(attacker, catalog, energy);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(weapon.AmmoItemKey))
|
||||
{
|
||||
|
|
@ -774,7 +788,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
weapon.State.LastRefusalReason = "";
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.Heat * deltaSeconds);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
weapon.State.OwnerIndex, weapon.Heat * deltaSeconds);
|
||||
ApplyWeaponWear(attacker, weapon.State, deltaSeconds);
|
||||
var shot = weapon.ResolutionShot(deltaSeconds);
|
||||
var shotId = NextShotId(zone, attacker, shot);
|
||||
CommitShotResolution(run, zone, attacker, selectedTarget!, shot, shotId, frameId, catalog, settings);
|
||||
|
|
@ -1538,7 +1554,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static WeaponRoundResult CommitWeaponRound(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
ResolvedWeapon weapon)
|
||||
ResolvedWeapon weapon,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (weapon.MagazineSize > 1 && weapon.State.Ammo <= 0)
|
||||
{
|
||||
|
|
@ -1555,46 +1572,29 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return WeaponRoundResult.ReloadStarted;
|
||||
}
|
||||
|
||||
if (!CanSupplyEnergy(entity, weapon.Energy))
|
||||
if (!CanSupplyEnergy(entity, catalog, weapon.Energy))
|
||||
return WeaponRoundResult.InsufficientEnergy;
|
||||
CommitEnergy(entity, weapon.Energy);
|
||||
CommitEnergy(entity, catalog, weapon.Energy);
|
||||
if (weapon.MagazineSize > 1)
|
||||
weapon.State.Ammo--;
|
||||
return WeaponRoundResult.Fired;
|
||||
}
|
||||
|
||||
private static bool CanSupplyEnergy(AetheriaRuntimeEntitySnapshotCommit entity, double demand)
|
||||
{
|
||||
if (demand < 0.01) return true;
|
||||
var states = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
var charge = states.Where(value => value != null && value.BehaviorKind == "Capacitor")
|
||||
.Sum(value => Math.Max(0, value.CapacitorCharge));
|
||||
return charge >= demand || states.Any(value => value != null && value.BehaviorKind == "Reactor");
|
||||
}
|
||||
private static bool CanSupplyEnergy(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double demand) =>
|
||||
AetheriaRuntimeEnergySimulation.CanSupply(entity, catalog, demand);
|
||||
|
||||
private static void CommitEnergy(AetheriaRuntimeEntitySnapshotCommit entity, double demand)
|
||||
private static void CommitEnergy(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double demand) =>
|
||||
AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand);
|
||||
|
||||
private static void ApplyWeaponWear(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeWeaponStateCommit state, double multiplier)
|
||||
{
|
||||
if (demand < 0.01) return;
|
||||
var capacitors = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && value.BehaviorKind == "Capacitor" && value.CapacitorCharge > 0)
|
||||
.ToList();
|
||||
var remaining = demand;
|
||||
while (remaining > 0.000001 && capacitors.Count > 0)
|
||||
{
|
||||
var share = remaining / capacitors.Count;
|
||||
foreach (var capacitor in capacitors.ToArray())
|
||||
{
|
||||
var drained = Math.Min(share, capacitor.CapacitorCharge);
|
||||
capacitor.CapacitorCharge -= drained;
|
||||
remaining -= drained;
|
||||
if (capacitor.CapacitorCharge <= 0.000001) capacitors.Remove(capacitor);
|
||||
}
|
||||
}
|
||||
var reactors = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && value.BehaviorKind == "Reactor")
|
||||
.ToArray();
|
||||
if (remaining > 0.000001 && reactors.Length > 0)
|
||||
foreach (var reactor in reactors) reactor.ReactorDraw += remaining / reactors.Length;
|
||||
if (state == null || state.OwnerKind != AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind) return;
|
||||
var wear = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(value => value != null && value.EquipmentIndex == state.OwnerIndex)?.Wear ?? 0;
|
||||
AetheriaRuntimeThermalSimulation.ApplyWear(entity, state.OwnerIndex, wear * multiplier);
|
||||
}
|
||||
|
||||
private enum WeaponRoundResult { Fired, ReloadStarted, InsufficientEnergy, NoAmmo }
|
||||
|
|
@ -1815,10 +1815,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var energyDemand = damage * energyUsage;
|
||||
shield.State.ShieldEfficiency = efficiency;
|
||||
shield.State.ShieldEnergyUsage = energyUsage;
|
||||
if (!CanSupplyEnergy(target, energyDemand))
|
||||
if (!CanSupplyEnergy(target, catalog, energyDemand))
|
||||
continue;
|
||||
|
||||
CommitEnergy(target, energyDemand);
|
||||
CommitEnergy(target, catalog, energyDemand);
|
||||
var heat = damage / efficiency;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(target, heat);
|
||||
RefreshShieldProjection(target, catalog);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeEnergySimulation
|
||||
{
|
||||
public static void BeginTick(AetheriaRuntimeEntitySnapshotCommit entity, AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null) return;
|
||||
var previousRadiatorVisibility = RadiatorVisibility(entity);
|
||||
entity.Visibility = Math.Max(0, entity.Visibility - previousRadiatorVisibility);
|
||||
SetRadiatorVisibility(entity, 0);
|
||||
foreach (var capacitor in AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Capacitor"))
|
||||
{
|
||||
capacitor.State.CapacitorCapacity = Math.Max(0, capacitor.EvaluateStat(1));
|
||||
capacitor.State.CapacitorEfficiency = Clamp01(capacitor.EvaluateStat(2));
|
||||
capacitor.State.CapacitorCharge = Math.Max(0,
|
||||
Math.Min(capacitor.State.CapacitorCapacity, capacitor.State.CapacitorCharge));
|
||||
}
|
||||
foreach (var reactor in AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Reactor"))
|
||||
reactor.State.ReactorDraw = 0;
|
||||
}
|
||||
|
||||
public static bool CanSupply(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double demand)
|
||||
{
|
||||
if (demand < 0.01) return true;
|
||||
var capacitors = Online(entity, catalog, "Capacitor");
|
||||
var charge = capacitors.Sum(value => Math.Max(0, value.State.CapacitorCharge));
|
||||
return charge > demand || Online(entity, catalog, "Reactor").Count > 0;
|
||||
}
|
||||
|
||||
public static bool TryConsume(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double demand)
|
||||
{
|
||||
if (demand < 0.01) return true;
|
||||
if (!CanSupply(entity, catalog, demand)) return false;
|
||||
var capacitors = Online(entity, catalog, "Capacitor");
|
||||
while (demand > 0.01)
|
||||
{
|
||||
var charged = capacitors.Where(value => value.State.CapacitorCharge > 0.01).ToArray();
|
||||
if (charged.Length == 0) break;
|
||||
var chargeToRemove = demand;
|
||||
foreach (var capacitor in charged)
|
||||
{
|
||||
var removed = Math.Min(chargeToRemove / charged.Length, capacitor.State.CapacitorCharge);
|
||||
AddCapacitorCharge(entity, catalog, capacitor, -removed);
|
||||
demand -= removed;
|
||||
}
|
||||
}
|
||||
if (demand < 0.01) return true;
|
||||
var reactors = Online(entity, catalog, "Reactor");
|
||||
if (reactors.Count == 0) return false;
|
||||
foreach (var reactor in reactors) reactor.State.ReactorDraw += demand / reactors.Count;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void StepRadiators(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0 || !entity.HeatsinksEnabled) return;
|
||||
foreach (var radiator in Online(entity, catalog, "Radiator"))
|
||||
{
|
||||
var itemTemperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(
|
||||
entity, catalog, radiator.EquipmentIndex);
|
||||
if (radiator.State.RadiatorTemperature <= 0)
|
||||
radiator.State.RadiatorTemperature = itemTemperature;
|
||||
radiator.State.PumpedHeat = Math.Max(0, radiator.EvaluateStat(2));
|
||||
radiator.State.WasteHeat = Math.Max(0, radiator.EvaluateStat(4));
|
||||
radiator.State.EnergyUsage = Math.Max(0, radiator.EvaluateStat(5));
|
||||
var tempRatio = Math.Max(radiator.State.RadiatorTemperature / Math.Max(0.000001, itemTemperature), 1);
|
||||
if (radiator.State.WasteHeat > 0 &&
|
||||
tempRatio > radiator.State.PumpedHeat / radiator.State.WasteHeat)
|
||||
continue;
|
||||
if (!TryConsume(entity, catalog, radiator.State.EnergyUsage * tempRatio * deltaSeconds))
|
||||
continue;
|
||||
var temperatureFloor = ReadNumber(radiator.Payload, 3);
|
||||
var pumped = radiator.State.PumpedHeat * Math.Max(itemTemperature - temperatureFloor, 0);
|
||||
if (pumped >= 0.01)
|
||||
{
|
||||
var waste = radiator.State.WasteHeat * tempRatio;
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(
|
||||
entity, catalog, radiator.EquipmentIndex, (waste - pumped) * deltaSeconds);
|
||||
var thermalMass = Math.Max(0.000001, radiator.EvaluateStat(6));
|
||||
radiator.State.RadiatorTemperature += pumped / thermalMass * deltaSeconds;
|
||||
}
|
||||
radiator.State.Emissivity = Math.Max(0, radiator.EvaluateStat(1));
|
||||
var radiation = Math.Pow(Math.Max(0, radiator.State.RadiatorTemperature),
|
||||
AetheriaRuntimeThermalSimulation.RadiationExponent) *
|
||||
AetheriaRuntimeThermalSimulation.RadiationMultiplier * radiator.State.Emissivity;
|
||||
radiator.State.RadiatorTemperature = Math.Max(0,
|
||||
radiator.State.RadiatorTemperature - radiation * deltaSeconds);
|
||||
AddRadiatorVisibility(entity, radiation);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SettleReactors(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0) return;
|
||||
var capacitors = Online(entity, catalog, "Capacitor");
|
||||
foreach (var reactor in Online(entity, catalog, "Reactor"))
|
||||
{
|
||||
var charge = Math.Max(0, reactor.EvaluateStat(1)) * deltaSeconds;
|
||||
var efficiency = Math.Max(0.000001, reactor.EvaluateStat(2));
|
||||
reactor.State.ReactorDraw -= charge;
|
||||
var heat = charge / efficiency;
|
||||
if (reactor.State.ReactorDraw > 0.01)
|
||||
{
|
||||
reactor.State.ReactorLoadRatio = (reactor.State.ReactorDraw + charge) / Math.Max(charge, 0.01);
|
||||
heat += reactor.State.ReactorDraw / Math.Max(0.000001, reactor.EvaluateStat(3));
|
||||
reactor.State.ReactorDraw = 0;
|
||||
}
|
||||
if (reactor.State.ReactorDraw < -0.01)
|
||||
{
|
||||
while (reactor.State.ReactorDraw < -0.01)
|
||||
{
|
||||
var available = capacitors.Where(value =>
|
||||
value.State.CapacitorCharge < value.State.CapacitorCapacity - 0.01).ToArray();
|
||||
if (available.Length == 0) break;
|
||||
var chargeToAdd = -reactor.State.ReactorDraw;
|
||||
foreach (var capacitor in available)
|
||||
{
|
||||
var added = Math.Min(chargeToAdd / available.Length,
|
||||
capacitor.State.CapacitorCapacity - capacitor.State.CapacitorCharge);
|
||||
AddCapacitorCharge(entity, catalog, capacitor, added);
|
||||
reactor.State.ReactorDraw += added;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reactor.State.ReactorDraw < -0.01)
|
||||
{
|
||||
reactor.State.ReactorLoadRatio = (reactor.State.ReactorDraw + charge) / Math.Max(charge, 0.01);
|
||||
heat -= reactor.State.ReactorDraw / efficiency *
|
||||
(1 - 1 / Math.Max(0.000001, reactor.EvaluateStat(4)));
|
||||
reactor.State.ReactorDraw = 0;
|
||||
}
|
||||
else reactor.State.ReactorLoadRatio = 1;
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(
|
||||
entity, catalog, reactor.EquipmentIndex, heat);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeEquippedBehavior> Online(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity, AetheriaRuntimeCatalogSnapshot? catalog, string kind) =>
|
||||
AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, kind)
|
||||
.Where(value => (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(state => state != null && state.EquipmentIndex == value.EquipmentIndex)?.Online ??
|
||||
(value.Item.Enabled && value.Item.Durability > 0.01))
|
||||
.ToArray();
|
||||
|
||||
private static void AddCapacitorCharge(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeEquippedBehavior capacitor, double delta)
|
||||
{
|
||||
capacitor.State.CapacitorCharge = Math.Max(0,
|
||||
Math.Min(capacitor.State.CapacitorCapacity, capacitor.State.CapacitorCharge + delta));
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(entity, catalog, capacitor.EquipmentIndex,
|
||||
Math.Abs(delta) * (1 - capacitor.State.CapacitorEfficiency));
|
||||
}
|
||||
|
||||
private static void AddRadiatorVisibility(AetheriaRuntimeEntitySnapshotCommit entity, double radiation)
|
||||
{
|
||||
radiation = Math.Max(0, radiation);
|
||||
SetRadiatorVisibility(entity, RadiatorVisibility(entity) + radiation);
|
||||
entity.Visibility += radiation;
|
||||
}
|
||||
|
||||
private static double RadiatorVisibility(AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
(entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>())
|
||||
.FirstOrDefault(value => string.Equals(value.Name, "radiator-visibility", StringComparison.Ordinal))?
|
||||
.Values.FirstOrDefault() ?? 0;
|
||||
|
||||
private static void SetRadiatorVisibility(AetheriaRuntimeEntitySnapshotCommit entity, double radiation)
|
||||
{
|
||||
var grids = (entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>()).ToList();
|
||||
var grid = grids.FirstOrDefault(value => string.Equals(value.Name, "radiator-visibility", StringComparison.Ordinal));
|
||||
if (grid == null)
|
||||
{
|
||||
grid = new AetheriaRuntimeEntityStatGridCommit { Name = "radiator-visibility", Width = 1, Height = 1 };
|
||||
grids.Add(grid);
|
||||
}
|
||||
grid.Values = new[] { Math.Max(0, radiation) };
|
||||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
private static double ReadNumber(AetheriaRuntimeBehaviorPayload payload, int key) =>
|
||||
(payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value?.NumberValue ?? 0;
|
||||
|
||||
private static double Clamp01(double value) => value < 0 ? 0 : value > 1 ? 1 : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ec02276eb02546a2868da179c5f0be7c
|
||||
|
|
@ -1045,6 +1045,24 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(46)]
|
||||
public long DestroyedFrameId { get; set; } = -1;
|
||||
|
||||
[Key(47)]
|
||||
public IReadOnlyList<AetheriaRuntimeEquipmentStateCommit> EquipmentStates { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeEquipmentStateCommit>();
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeEquipmentStateCommit
|
||||
{
|
||||
[Key(0)] public int EquipmentIndex { get; set; } = -1;
|
||||
[Key(1)] public double Temperature { get; set; }
|
||||
[Key(2)] public double PreviousTemperature { get; set; }
|
||||
[Key(3)] public double ThermalPerformance { get; set; } = 1;
|
||||
[Key(4)] public double DurabilityPerformance { get; set; } = 1;
|
||||
[Key(5)] public double Wear { get; set; }
|
||||
[Key(6)] public bool ThermalOnline { get; set; } = true;
|
||||
[Key(7)] public bool DurabilityOnline { get; set; } = true;
|
||||
[Key(8)] public bool Online { get; set; } = true;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public const double RadiationExponent = 3.0;
|
||||
public const double RadiationMultiplier = 0.00000001;
|
||||
public const double InitialTemperature = 280.0;
|
||||
public const double ThermalWearExponent = 0.01;
|
||||
public const double DeltaTemperatureWearExponent = 0.01;
|
||||
public const double QualityWearExponent = 2.0;
|
||||
|
||||
public static void EnsureState(AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
|
|
@ -42,6 +45,64 @@ namespace GameCult.Aetheria.State.Verse
|
|||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
public static void EnsureTopology(AetheriaRuntimeEntitySnapshotCommit entity, AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null || catalog == null) return;
|
||||
var hull = catalog.FindItem(entity.HullItemKey ?? "");
|
||||
if (hull == null || hull.ShapeWidth <= 0 || hull.ShapeHeight <= 0 ||
|
||||
hull.ShapeCells == null || hull.ShapeCells.Count == 0) return;
|
||||
var grids = (entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>()).ToList();
|
||||
var occupied = new HashSet<int>(hull.ShapeCells.Select(cell => Index(cell.X, cell.Y, hull.ShapeWidth)));
|
||||
var temperature = Find(grids, TemperatureGrid);
|
||||
if (temperature == null)
|
||||
{
|
||||
temperature = Grid(TemperatureGrid, hull.ShapeWidth, hull.ShapeHeight, 0);
|
||||
grids.Add(temperature);
|
||||
}
|
||||
if (temperature.Width != hull.ShapeWidth || temperature.Height != hull.ShapeHeight ||
|
||||
temperature.Values.Count != hull.ShapeWidth * hull.ShapeHeight)
|
||||
{
|
||||
temperature.Width = hull.ShapeWidth;
|
||||
temperature.Height = hull.ShapeHeight;
|
||||
temperature.Values = Enumerable.Range(0, hull.ShapeWidth * hull.ShapeHeight)
|
||||
.Select(index => occupied.Contains(index) ? InitialTemperature : 0).ToArray();
|
||||
}
|
||||
else if (temperature.Values.All(value => value <= 0))
|
||||
temperature.Values = Enumerable.Range(0, hull.ShapeWidth * hull.ShapeHeight)
|
||||
.Select(index => occupied.Contains(index) ? InitialTemperature : 0).ToArray();
|
||||
|
||||
var cellCount = Math.Max(1, hull.ShapeCells.Count);
|
||||
var baseMass = Math.Max(0.000001, hull.Mass * hull.SpecificHeat / cellCount);
|
||||
var mass = Enumerable.Range(0, hull.ShapeWidth * hull.ShapeHeight)
|
||||
.Select(index => occupied.Contains(index) ? baseMass : 0).ToArray();
|
||||
var conductivity = Enumerable.Range(0, mass.Length)
|
||||
.Select(index => occupied.Contains(index) ? 1.0 : 0).ToArray();
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
var slot = equipment[equipmentIndex];
|
||||
var item = catalog.FindItem(slot?.Item?.ItemKey ?? "");
|
||||
var cells = item?.ShapeCells ?? Array.Empty<AetheriaRuntimeShapeCell>();
|
||||
var itemCellCount = Math.Max(1, cells.Count);
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
var x = slot.X + cell.X;
|
||||
var y = slot.Y + cell.Y;
|
||||
if (x < 0 || y < 0 || x >= hull.ShapeWidth || y >= hull.ShapeHeight) continue;
|
||||
var index = Index(x, y, hull.ShapeWidth);
|
||||
if (!occupied.Contains(index)) continue;
|
||||
mass[index] += Math.Max(0, item!.Mass * item.SpecificHeat / itemCellCount);
|
||||
conductivity[index] = Math.Max(0.000001, item.Conductivity);
|
||||
}
|
||||
}
|
||||
SetGrid(grids, ThermalMassGrid, hull.ShapeWidth, hull.ShapeHeight, mass);
|
||||
SetGrid(grids, ConductivityGrid, hull.ShapeWidth, hull.ShapeHeight, conductivity);
|
||||
EnsureScalar(grids, MeanTemperatureGrid, Mean(temperature.Values.Where(value => value > 0).ToArray()));
|
||||
EnsureScalar(grids, MinimumTemperatureGrid, temperature.Values.Where(value => value > 0).DefaultIfEmpty(0).Min());
|
||||
EnsureScalar(grids, MaximumTemperatureGrid, temperature.Values.DefaultIfEmpty(0).Max());
|
||||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
public static void AddHeat(AetheriaRuntimeEntitySnapshotCommit entity, double energy)
|
||||
{
|
||||
if (entity == null || !double.IsFinite(energy) || Math.Abs(energy) < 0.0000001)
|
||||
|
|
@ -65,7 +126,100 @@ namespace GameCult.Aetheria.State.Verse
|
|||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
public static void Step(AetheriaRuntimeEntitySnapshotCommit entity, double deltaSeconds)
|
||||
public static void AddHeatToEquipment(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, int equipmentIndex, double energy)
|
||||
{
|
||||
if (entity == null || !double.IsFinite(energy) || Math.Abs(energy) < 0.0000001) return;
|
||||
EnsureState(entity);
|
||||
var cells = EquipmentCellIndices(entity, catalog, equipmentIndex).ToArray();
|
||||
if (cells.Length == 0) { AddHeat(entity, energy); return; }
|
||||
var grids = entity.StatGrids.ToList();
|
||||
var temperature = Find(grids, TemperatureGrid)!;
|
||||
var thermalMass = Find(grids, ThermalMassGrid)!;
|
||||
var values = temperature.Values.ToArray();
|
||||
var energyPerCell = energy / cells.Length;
|
||||
foreach (var index in cells)
|
||||
if (index >= 0 && index < values.Length && thermalMass.Values[index] > 0)
|
||||
values[index] = Math.Max(0, values[index] + energyPerCell / thermalMass.Values[index]);
|
||||
temperature.Values = values;
|
||||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
public static double EquipmentTemperature(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, int equipmentIndex)
|
||||
{
|
||||
EnsureState(entity);
|
||||
var temperature = Find(entity.StatGrids, TemperatureGrid)!;
|
||||
var cells = EquipmentCellIndices(entity, catalog, equipmentIndex).ToArray();
|
||||
return cells.Length == 0 ? Mean(temperature.Values) : cells.Average(index => temperature.Values[index]);
|
||||
}
|
||||
|
||||
public static void UpdateEquipmentStates(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null) return;
|
||||
var existing = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(value => value != null).ToDictionary(value => value.EquipmentIndex);
|
||||
var states = new List<AetheriaRuntimeEquipmentStateCommit>();
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var index = 0; index < equipment.Count; index++)
|
||||
{
|
||||
var item = equipment[index]?.Item;
|
||||
var typed = catalog.FindItem(item?.ItemKey ?? "");
|
||||
if (item == null || typed == null) continue;
|
||||
if (!existing.TryGetValue(index, out var state))
|
||||
state = new AetheriaRuntimeEquipmentStateCommit
|
||||
{
|
||||
EquipmentIndex = index,
|
||||
PreviousTemperature = EquipmentTemperature(entity, catalog, index)
|
||||
};
|
||||
var temperature = EquipmentTemperature(entity, catalog, index);
|
||||
var thermalPerformance = typed.MaximumTemperature <= typed.MinimumTemperature ||
|
||||
typed.ThermalPerformanceCurveKeys == null || typed.ThermalPerformanceCurveKeys.Count == 0
|
||||
? 1
|
||||
: Clamp01(SampleCurve(typed.ThermalPerformanceCurveKeys,
|
||||
(temperature - typed.MinimumTemperature) /
|
||||
(typed.MaximumTemperature - typed.MinimumTemperature)));
|
||||
var maxDurability = Math.Max(0.000001, typed.Durability);
|
||||
var durabilityPerformance = Math.Max(0, item.Durability / maxDurability);
|
||||
var quality = Clamp01(item.Quality);
|
||||
var wear = (1 - Math.Pow(thermalPerformance,
|
||||
(1 - Math.Pow(quality, QualityWearExponent)) * ThermalWearExponent) +
|
||||
Math.Abs(temperature - state.PreviousTemperature) * DeltaTemperatureWearExponent) *
|
||||
maxDurability / Math.Max(0.000001, typed.ThermalResilience);
|
||||
state.Temperature = temperature;
|
||||
state.ThermalPerformance = thermalPerformance;
|
||||
state.DurabilityPerformance = durabilityPerformance;
|
||||
state.Wear = Math.Max(0, wear);
|
||||
state.ThermalOnline = thermalPerformance > entity.ShutdownPerformance ||
|
||||
entity.OverrideShutdown && item.OverrideShutdown;
|
||||
state.DurabilityOnline = item.Durability > 0.01;
|
||||
state.Online = item.Enabled && state.ThermalOnline && state.DurabilityOnline;
|
||||
state.PreviousTemperature = temperature;
|
||||
states.Add(state);
|
||||
|
||||
var wearPayload = (typed.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
|
||||
.FirstOrDefault(value => value != null && value.Kind == "Wear");
|
||||
if (state.Online && wearPayload != null)
|
||||
{
|
||||
var perSecond = (wearPayload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == 1)?.Value?.BoolValue ?? true;
|
||||
ApplyWear(entity, index, state.Wear * (perSecond ? deltaSeconds : 1));
|
||||
}
|
||||
}
|
||||
entity.EquipmentStates = states;
|
||||
}
|
||||
|
||||
public static void ApplyWear(AetheriaRuntimeEntitySnapshotCommit entity, int equipmentIndex, double damage)
|
||||
{
|
||||
var equipment = entity?.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
if (equipmentIndex < 0 || equipmentIndex >= equipment.Count || damage <= 0) return;
|
||||
var item = equipment[equipmentIndex]?.Item;
|
||||
if (item != null) item.Durability = Math.Max(0, item.Durability - damage);
|
||||
}
|
||||
|
||||
public static void Step(AetheriaRuntimeEntitySnapshotCommit entity, double deltaSeconds,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null)
|
||||
{
|
||||
if (entity == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
|
@ -75,6 +229,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var temperature = Find(grids, TemperatureGrid)!;
|
||||
var thermalMass = Find(grids, ThermalMassGrid)!;
|
||||
var conductivity = Find(grids, ConductivityGrid)!;
|
||||
var hull = catalog?.FindItem(entity.HullItemKey ?? "");
|
||||
var hullConductivity = Math.Max(0.000001, hull?.Conductivity ?? 1);
|
||||
var facetX = Find(grids, "hull_conductivity_x");
|
||||
var facetY = Find(grids, "hull_conductivity_y");
|
||||
var interior = new HashSet<int>((hull?.InteriorShapeCells ?? Array.Empty<AetheriaRuntimeShapeCell>())
|
||||
.Select(cell => Index(cell.X, cell.Y, temperature.Width)));
|
||||
var current = temperature.Values.ToArray();
|
||||
var next = current.ToArray();
|
||||
var radiation = 0.0;
|
||||
|
|
@ -88,20 +248,22 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
var weightedTemperature = current[index] / ConductionMultiplier;
|
||||
var totalConductivity = 1.0 / ConductionMultiplier;
|
||||
Accumulate(x - 1, y);
|
||||
Accumulate(x + 1, y);
|
||||
Accumulate(x, y - 1);
|
||||
Accumulate(x, y + 1);
|
||||
Accumulate(x - 1, y, facetX, x - 1, y);
|
||||
Accumulate(x + 1, y, facetX, x, y);
|
||||
Accumulate(x, y - 1, facetY, x, y - 1);
|
||||
Accumulate(x, y + 1, facetY, x, y);
|
||||
next[index] = weightedTemperature / totalConductivity;
|
||||
|
||||
if (IsBorderCell(x, y, temperature.Width, temperature.Height, thermalMass.Values))
|
||||
if (hull != null ? !interior.Contains(index) :
|
||||
IsBorderCell(x, y, temperature.Width, temperature.Height, thermalMass.Values))
|
||||
{
|
||||
var emitted = Math.Pow(Math.Max(0, next[index]), RadiationExponent) * RadiationMultiplier;
|
||||
next[index] = Math.Max(0, next[index] - emitted * deltaSeconds);
|
||||
radiation += emitted;
|
||||
}
|
||||
|
||||
void Accumulate(int neighborX, int neighborY)
|
||||
void Accumulate(int neighborX, int neighborY,
|
||||
AetheriaRuntimeEntityStatGridCommit? facet, int facetXIndex, int facetYIndex)
|
||||
{
|
||||
if (neighborX < 0 || neighborY < 0 || neighborX >= temperature.Width || neighborY >= temperature.Height)
|
||||
return;
|
||||
|
|
@ -109,6 +271,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
if (thermalMass.Values[neighbor] <= 0)
|
||||
return;
|
||||
var transfer = conductivity.Values[index] * conductivity.Values[neighbor] *
|
||||
FacetFactor(facet, facetXIndex, facetYIndex, hullConductivity) *
|
||||
thermalMass.Values[neighbor] / thermalMass.Values[index];
|
||||
totalConductivity += transfer;
|
||||
weightedTemperature += current[neighbor] * transfer;
|
||||
|
|
@ -124,9 +287,35 @@ namespace GameCult.Aetheria.State.Verse
|
|||
entity.StatGrids = grids;
|
||||
}
|
||||
|
||||
private static double FacetFactor(AetheriaRuntimeEntityStatGridCommit? grid,
|
||||
int x, int y, double hullConductivity)
|
||||
{
|
||||
if (grid == null || x < 0 || y < 0 || x >= grid.Width || y >= grid.Height) return 1 / hullConductivity;
|
||||
var index = Index(x, y, grid.Width);
|
||||
return index < grid.Values.Count && grid.Values[index] > 0.5 ? hullConductivity : 1 / hullConductivity;
|
||||
}
|
||||
|
||||
private static double PreviousThermalVisibility(AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
Find(entity.StatGrids, "thermal-visibility")?.Values.FirstOrDefault() ?? 0;
|
||||
|
||||
private static IEnumerable<int> EquipmentCellIndices(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, int equipmentIndex)
|
||||
{
|
||||
var temperature = Find(entity.StatGrids, TemperatureGrid);
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
if (temperature == null || catalog == null || equipmentIndex < 0 || equipmentIndex >= equipment.Count)
|
||||
yield break;
|
||||
var slot = equipment[equipmentIndex];
|
||||
var item = catalog.FindItem(slot?.Item?.ItemKey ?? "");
|
||||
foreach (var cell in item?.ShapeCells ?? Array.Empty<AetheriaRuntimeShapeCell>())
|
||||
{
|
||||
var x = slot.X + cell.X;
|
||||
var y = slot.Y + cell.Y;
|
||||
if (x >= 0 && y >= 0 && x < temperature.Width && y < temperature.Height)
|
||||
yield return Index(x, y, temperature.Width);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBorderCell(int x, int y, int width, int height, IReadOnlyList<double> mass) =>
|
||||
x == 0 || y == 0 || x == width - 1 || y == height - 1 ||
|
||||
mass[Index(x - 1, y, width)] <= 0 || mass[Index(x + 1, y, width)] <= 0 ||
|
||||
|
|
@ -134,6 +323,29 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static int Index(int x, int y, int width) => y * width + x;
|
||||
private static double Mean(IReadOnlyList<double> values) => values.Count == 0 ? 0 : values.Average();
|
||||
private static double Clamp01(double value) => value < 0 ? 0 : value > 1 ? 1 : value;
|
||||
|
||||
private static double SampleCurve(IReadOnlyList<AetheriaRuntimeCurveKey> keys, double value)
|
||||
{
|
||||
var ordered = keys.OrderBy(key => key.Time).ToArray();
|
||||
if (value <= ordered[0].Time) return ordered[0].Value;
|
||||
for (var index = 1; index < ordered.Length; index++)
|
||||
{
|
||||
var next = ordered[index];
|
||||
var previous = ordered[index - 1];
|
||||
if (value > next.Time) continue;
|
||||
var span = next.Time - previous.Time;
|
||||
if (span <= double.Epsilon) return next.Value;
|
||||
var t = Clamp01((value - previous.Time) / span);
|
||||
var t2 = t * t;
|
||||
var t3 = t2 * t;
|
||||
return (2 * t3 - 3 * t2 + 1) * previous.Value +
|
||||
(t3 - 2 * t2 + t) * previous.OutTangent * span +
|
||||
(-2 * t3 + 3 * t2) * next.Value +
|
||||
(t3 - t2) * next.InTangent * span;
|
||||
}
|
||||
return ordered[ordered.Length - 1].Value;
|
||||
}
|
||||
private static bool Valid(AetheriaRuntimeEntityStatGridCommit? grid) =>
|
||||
grid != null && grid.Width > 0 && grid.Height > 0 && grid.Values.Count == grid.Width * grid.Height;
|
||||
|
||||
|
|
@ -166,6 +378,20 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Find(grids, name)!.Values = new[] { value };
|
||||
}
|
||||
|
||||
private static void SetGrid(List<AetheriaRuntimeEntityStatGridCommit> grids,
|
||||
string name, int width, int height, IReadOnlyList<double> values)
|
||||
{
|
||||
var grid = Find(grids, name);
|
||||
if (grid == null)
|
||||
{
|
||||
grid = Grid(name, width, height, 0);
|
||||
grids.Add(grid);
|
||||
}
|
||||
grid.Width = width;
|
||||
grid.Height = height;
|
||||
grid.Values = values.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeEntityStatGridCommit Grid(string name, int width, int height, double value) => new()
|
||||
{
|
||||
Name = name,
|
||||
|
|
|
|||
|
|
@ -1937,6 +1937,15 @@ First Aetheria surfaces to publish:
|
|||
|
||||
- `rg "Newtonsoft|JsonObject|JsonProperty|JsonConvert|JsonKnownTypes|RethinkDb|RethinkTable|DatabaseCache"` is zero outside migration quarantine and docs.
|
||||
- `rg "DatabaseEntry|DatabaseLink|InspectableDatabaseLink|ServerShared\\\\CultCache" Assets Economy.*` shows no live runtime ownership path.
|
||||
- `AetheriaRuntimeEnergySimulation` owns the equipped capacitor/reactor/radiator
|
||||
network. `AetheriaRuntimeThermalSimulation` owns catalog-derived cell topology,
|
||||
equipment-local temperature/performance, shutdown, wear, conduction, and
|
||||
radiation. The daemon orders radiator pumping before reactor settlement and
|
||||
thermal conduction; client operations have no direct energy, heat, wear, or
|
||||
equipment-online writer. The Eve game surface publishes the resulting power,
|
||||
radiator, temperature, wear, and offline-equipment facts. Cockpit medical
|
||||
consequences, rotated equipment footprints, and parent/child radiation remain
|
||||
migration work rather than presentation authority.
|
||||
- `dotnet list package --vulnerable --include-transitive` is clean for active
|
||||
maintained projects.
|
||||
- CultCache smoke proves write, flush, reopen, query, and typed reference
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; daemon weapon lock now acquires and decays from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock | target-switch reset, angular decay, visibility/nearest/explicit cases, and runtime lock feedback |
|
||||
| Projectile flight | weapon/projectile classes | Eve presentation for ordinary tracers; Ymir for persistent physical payloads | receipt trajectory/effect facts or persistent payload state | native projectile visual | ordinary instant, charged, and constant execution creates no projectile body; each weapon state owns a stable shot sequence and receipts carry origin, endpoint, nominal duration, and effect identity for Eve presentation; `SpawnProjectile` and its alternate writer are deleted; Ymir projectile contacts are feedback-only for explicitly persisted payload rows and cannot write damage; Ymir.Core remains embedded for world integration, radial fields, circle contacts, pickups, and genuinely persistent/interceptable payloads | split/rename the remaining physical-payload interface and remove ordinary projectile terminology from runtime contracts |
|
||||
| Shield/armor/equipment/hull damage | energy-funded shield, schematic armor cells, equipment and scalar hull | daemon canonical damage transaction | aggregate/exact armor grid, entity status, and layer-aware shot/damage receipts | schematic, meters, impacts, damage VFX | owner-exists; wired; exclusive; projected: daemon initializes armor/maximum-armor grids from hull plus hardpoint catalog topology; direct and deployable damage share one `shield -> armor cell -> equipment -> scalar hull` transaction; direct impacts select a cell or complete hardpoint footprint before orthogonal spread and 0.5-cell penetration, while splash payloads select the source-facing hull half; the fossil penetration infinite-loop defect is fixed; damage type is typed/pass-through without resistances because the fossil never applied them; Eve exposes aggregate and exact armor state and receipts identify reached/applied layers; fake Ymir contacts and presentation cannot decide damage | fossil parity fixtures for topology, edge impacts, orthogonal spread, multi-cell penetration, equipment interception, and generic shield/equipment effect lifecycle |
|
||||
| Thermal/energy | cell conduction and capacitor/reactor/radiator network | daemon | ship/equipment status and warnings | schematic/post effects | cell conduction/radiation wired; energy network and medical thresholds remain reference | capacitor/reactor/radiator ordering, shared energy and medical timelines |
|
||||
| Thermal/energy | cell conduction and capacitor/reactor/radiator network | daemon | ship/equipment state, power/temperature facts and warnings | schematic/post effects | owner-exists; wired; exclusive; projected: 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; Eve exposes capacitor, reactor, radiator, temperature, wear and offline-equipment facts; cockpit medical thresholds, rotated equipment topology and parent/child radiation remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, cockpit injury/death timelines and generic schematic/post-effect lowering |
|
||||
| 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 | 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 | prove generic runtime beam, contact, success and rejection feedback |
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ performance and online state derive from local temperature.
|
|||
Cockpit temperature advances heatstroke/hypothermia accumulation and recovery.
|
||||
Threshold crossings and death are authoritative events.
|
||||
|
||||
The daemon derives the grid from the equipped catalog, rather than accepting a
|
||||
renderer-authored heat scalar. Hull mass and specific heat are distributed over
|
||||
the hull shape; installed equipment contributes thermal mass and replaces the
|
||||
conductivity of its occupied cells. Equipment temperature is the mean of those
|
||||
cells. Authored thermal-performance curves and durability derive each item's
|
||||
current performance and online state. Generic wear and weapon-triggered wear
|
||||
consume the same temperature-derived wear potential.
|
||||
|
||||
## Energy Network
|
||||
|
||||
Capacitors store charge. A demand drains charged capacitors evenly first, then
|
||||
|
|
@ -49,6 +57,14 @@ divides unmet demand among online reactors. Behaviors succeed or fail according
|
|||
to the actual transaction. Radiators consume energy to pump heat and produce
|
||||
waste heat. Shields consume energy per intercepted damage.
|
||||
|
||||
Per daemon tick, equipment performance is derived before behavior execution and
|
||||
all energy consumers transact against the same equipped network. Radiators then
|
||||
pump heat and add their demand, reactors settle baseline output, overload,
|
||||
capacitor refill, throttling and equipment heat, and finally the thermal grid
|
||||
conducts and radiates. Thermal and radiator radiation are independently replaced
|
||||
visibility contributions; they are not cumulative scalar drift. A client may
|
||||
request semantic controls, but cannot set charge, heat, wear, or online state.
|
||||
|
||||
## Behavior Execution
|
||||
|
||||
Required behavior families:
|
||||
|
|
|
|||
Loading…
Reference in a new issue