From 95a118c8bd17834cf00ff5afc37df6b04a4f0195 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 17 Jul 2026 03:03:30 +0100 Subject: [PATCH] Run resource behavior gates in daemon --- Aetheria.State.Daemon.Smoke/Program.cs | 128 +++++++++++++++++- ...s => AetheriaRuntimeBehaviorSimulation.cs} | 117 +++++++++++++++- .../AetheriaRuntimeConsumableSimulation.cs | 2 +- .../AetheriaRuntimeDaemonSimulation.cs | 5 +- .../AetheriaRuntimeEquippedBehaviorQueries.cs | 2 +- .../AetheriaRuntimeSnapshotDocuments.cs | 3 + .../AetheriaRuntimeThermalSimulation.cs | 8 -- docs/aetheria-current-codebase-model.md | 5 +- docs/aetheria-perfect-machine-map.md | 10 ++ docs/original-game-migration-matrix.md | 2 +- 10 files changed, 257 insertions(+), 25 deletions(-) rename Packages/org.gamecult.aetheria.state/Runtime/{AetheriaRuntimeStatModifierSimulation.cs => AetheriaRuntimeBehaviorSimulation.cs} (68%) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index b8db948fd..b3540f072 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -175,6 +175,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks DockedRefitSurfacePublishesGenericCommands, FossilVelocityBehaviorsRunInDaemonFlightStep, BehaviorChainsGateAuthoritativeStatModifiers, + ResourceBehaviorsStopEquipmentChainsInAuthoredOrder, ReflectorUsesSharedStellarLightFieldWithoutAccumulating, SensorPingIsActorScopedEnergyGatedAndReconnectable, TurretControllerAcquiresAimsAndTriggersExactWeapons, @@ -260,33 +261,137 @@ internal sealed class AetheriaDaemonYmirSmokeChecks { ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 2, Active = true } }; - AetheriaRuntimeStatModifierSimulation.Step(0, [entity], + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [new AetheriaRuntimeDaemonBehaviorIntent { ActorEntityKey = "zone.1.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = true }], - catalog); + catalog, 0.1); var target = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Thruster").Single(); RequireNear(10, target.EvaluateStat(1), 0.000001, "a behavior command for the same entity index in another zone must not cross-activate this ship"); - AetheriaRuntimeStatModifierSimulation.Step(0, [entity], activate, catalog); + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], activate, catalog, 0.1); RequireNear(25, target.EvaluateStat(1), 0.000001, "switch and one-shot trigger groups must multiply then add through the shared daemon stat path"); var modifierStates = entity.BehaviorStates.Where(value => value.BehaviorKind == "StatModifier").ToArray(); Require(modifierStates.All(value => value.StatModifierApplied && value.StatModifierTargetStatCount == 1), "applied modifier state must name the exact live target count"); - AetheriaRuntimeStatModifierSimulation.Step(0, [entity], [], catalog); + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1); RequireNear(20, target.EvaluateStat(1), 0.000001, "trigger modifiers must disappear on the next tick without another pull"); - AetheriaRuntimeStatModifierSimulation.Step(0, [entity], + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [new AetheriaRuntimeDaemonBehaviorIntent { ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = false }], - catalog); + catalog, 0.1); RequireNear(10, target.EvaluateStat(1), 0.000001, "deactivating the switch must restore the unmodified authored stat exactly"); } + private static void ResourceBehaviorsStopEquipmentChainsInAuthoredOrder() + { + var targetItem = CatalogItem("resource-target", new AetheriaRuntimeBehaviorPayload( + 0, "Thruster", 0, [new AetheriaRuntimeBehaviorField(1, PerformanceStat(10))])); + var controlItem = CatalogItem("resource-controls", + new AetheriaRuntimeBehaviorPayload(0, "Switch", 0, []), + new AetheriaRuntimeBehaviorPayload(1, "Cooldown", 0, + [new AetheriaRuntimeBehaviorField(1, PerformanceStat(1))]), + new AetheriaRuntimeBehaviorPayload(2, "EnergyDraw", 0, + [ + new AetheriaRuntimeBehaviorField(1, PerformanceStat(5)), + new AetheriaRuntimeBehaviorField(2, BoolValue(false)) + ]), + new AetheriaRuntimeBehaviorPayload(3, "Heat", 0, + [ + new AetheriaRuntimeBehaviorField(1, PerformanceStat(10)), + new AetheriaRuntimeBehaviorField(2, BoolValue(false)) + ]), + new AetheriaRuntimeBehaviorPayload(4, "ItemUsage", 0, + [new AetheriaRuntimeBehaviorField(1, ItemKeyValue("resource-ammo"))]), + new AetheriaRuntimeBehaviorPayload(5, "Wear", 0, + [new AetheriaRuntimeBehaviorField(1, BoolValue(true))]), + new AetheriaRuntimeBehaviorPayload(6, "StatModifier", 0, + [ + new AetheriaRuntimeBehaviorField(1, StatReference("Thruster", "Thrust")), + new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)), + new AetheriaRuntimeBehaviorField(3, StringValue("Multiplier")) + ])); + var capacitorItem = CatalogItem("resource-capacitor", CapacitorPayload(10, 1)); + var ammoItem = CatalogItem("resource-ammo"); + var catalog = new AetheriaRuntimeCatalogSnapshot( + [targetItem, controlItem, capacitorItem, ammoItem], [], []); + var entity = Entity(0, 0, "player"); + entity.Equipment = + [ + Equipment(targetItem.ItemKey), + Equipment(controlItem.ItemKey), + Equipment(capacitorItem.ItemKey) + ]; + entity.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit + { + Items = [new AetheriaRuntimeLoadoutItemSlotCommit + { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = ammoItem.ItemKey, Quantity = 2 } }] + }]; + AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog); + AetheriaRuntimeThermalSimulation.EnsureState(entity); + AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 0.1); + AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog); + AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog); + var capacitor = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Capacitor").Single(); + capacitor.State.CapacitorCharge = 10; + var controlEquipmentState = entity.EquipmentStates.Single(value => value.EquipmentIndex == 1); + controlEquipmentState.Wear = 2; + var initialTemperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1); + var target = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Thruster").Single(); + + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], + [new AetheriaRuntimeDaemonBehaviorIntent + { ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = true }], + catalog, 0.1); + RequireNear(10, capacitor.State.CapacitorCharge, 0.000001, + "a newly constructed cooldown must stop its group before any resource transaction"); + RequireEqual(2, CargoQuantity(entity, ammoItem.ItemKey), + "cooldown must stop item usage before cargo mutation"); + + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1); + RequireNear(5, capacitor.State.CapacitorCharge, 0.000001, + "energy draw must debit the daemon capacitor exactly once when the cooldown opens"); + RequireEqual(1, CargoQuantity(entity, ammoItem.ItemKey), + "item usage must remove exactly one authored cargo item"); + RequireNear(0.8, entity.Equipment[1].Item.Durability, 0.000001, + "wear must use the thermal owner's pressure only after preceding gates succeed"); + Require(AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1) > initialTemperature, + "heat must enter the source equipment's authoritative thermal cells"); + RequireNear(20, target.EvaluateStat(1), 0.000001, + "downstream modifiers must apply only after every preceding resource gate succeeds"); + RequireNear(0.9, entity.BehaviorProgress.Single(value => + value.OwnerIndex == 1 && value.BehaviorKind == "Cooldown").Progress, 0.000001, + "cooldown state must project a generic Eve progress lever from the authoritative behavior row"); + + var cooldown = entity.BehaviorStates.Single(value => + value.OwnerIndex == 1 && value.BehaviorKind == "Cooldown"); + cooldown.CooldownProgress = -1; + capacitor.State.CapacitorCharge = 0; + var temperatureBeforeRefusal = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1); + var durabilityBeforeRefusal = entity.Equipment[1].Item.Durability; + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1); + RequireEqual(1, CargoQuantity(entity, ammoItem.ItemKey), + "an empty capacitor must stop the group before item usage"); + RequireNear(durabilityBeforeRefusal, entity.Equipment[1].Item.Durability, 0.000001, + "an empty capacitor must stop the group before wear"); + RequireNear(temperatureBeforeRefusal, + AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1), 0.000001, + "an empty capacitor must stop the group before heat"); + RequireNear(10, target.EvaluateStat(1), 0.000001, + "an empty capacitor must remove a downstream modifier instead of leaving stale application state"); + + static AetheriaRuntimeLoadoutItemSlotCommit Equipment(string itemKey) => new() + { + Item = new AetheriaRuntimeLoadoutItemCommit + { ItemKey = itemKey, Enabled = true, Durability = 1, Quality = 1 } + }; + } + private static void DockedRefitSurfacePublishesGenericCommands() { var hull = HullCatalogItem("surface-hull", 9, 7, 100); @@ -3142,6 +3247,16 @@ internal sealed class AetheriaDaemonYmirSmokeChecks Array.Empty(), Array.Empty()); + private static AetheriaRuntimeBehaviorValue BoolValue(bool value) => new( + "bool", "", 0, value, "", "", + Array.Empty(), + Array.Empty()); + + private static AetheriaRuntimeBehaviorValue ItemKeyValue(string value) => new( + "item-key", "", 0, false, "", value, + Array.Empty(), + Array.Empty()); + private static AetheriaRuntimeBehaviorValue StatReference(string target, string stat) => new( "stat-reference", "", 0, false, "", "", [StringValue(""), StringValue(target), StringValue(stat)], @@ -4794,6 +4909,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "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"); + AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 1); RequireNear(10 - state.Wear, item.Durability, 0.000001, "generic Wear behavior must apply the computed thermal wear potential at its authored cadence"); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStatModifierSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeBehaviorSimulation.cs similarity index 68% rename from Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStatModifierSimulation.cs rename to Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeBehaviorSimulation.cs index e8de4b5c8..3f873c764 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStatModifierSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeBehaviorSimulation.cs @@ -6,15 +6,16 @@ using System.Linq; namespace GameCult.Aetheria.State.Verse { - public static class AetheriaRuntimeStatModifierSimulation + public static class AetheriaRuntimeBehaviorSimulation { public static void Step( int zoneIndex, IReadOnlyList entities, IEnumerable? intents, - AetheriaRuntimeCatalogSnapshot? catalog) + AetheriaRuntimeCatalogSnapshot? catalog, + double deltaSeconds) { - if (catalog == null) + if (catalog == null || deltaSeconds <= 0) return; var intentArray = (intents ?? Enumerable.Empty()).ToArray(); @@ -25,7 +26,8 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog); ApplyControlIntents(zoneIndex, entity, intentArray); - ExecuteEquipmentChains(entity, catalog); + ExecuteEquipmentChains(entity, catalog, deltaSeconds); + ProjectCooldownProgress(entity); } } @@ -104,7 +106,8 @@ namespace GameCult.Aetheria.State.Verse private static void ExecuteEquipmentChains( AetheriaRuntimeEntitySnapshotCommit entity, - AetheriaRuntimeCatalogSnapshot catalog) + AetheriaRuntimeCatalogSnapshot catalog, + double deltaSeconds) { var states = (entity.BehaviorStates ?? Array.Empty()) .Where(value => value != null && string.Equals(value.OwnerKind, @@ -141,6 +144,39 @@ namespace GameCult.Aetheria.State.Verse break; state.TriggerPulled = false; } + if (string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal)) + { + if (state.CooldownProgress >= 0) + break; + state.CooldownProgress = 1; + } + if (string.Equals(entry.Payload.Kind, "EnergyDraw", StringComparison.Ordinal)) + { + var demand = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) * + (ReadBool(entry.Payload, 2) ? deltaSeconds : 1); + if (!AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand)) + break; + } + if (string.Equals(entry.Payload.Kind, "Heat", StringComparison.Ordinal)) + { + var heat = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) * + (ReadBool(entry.Payload, 2) ? deltaSeconds : 1); + AetheriaRuntimeThermalSimulation.AddHeatToEquipment(entity, catalog, equipmentIndex, heat); + } + if (string.Equals(entry.Payload.Kind, "ItemUsage", StringComparison.Ordinal)) + { + var itemKey = ReadItemKey(entry.Payload, 1); + if (string.IsNullOrWhiteSpace(itemKey) || + !AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) || + !AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out _)) + break; + } + if (string.Equals(entry.Payload.Kind, "Wear", StringComparison.Ordinal)) + { + var wear = online.TryGetValue(equipmentIndex, out var wearState) ? wearState.Wear : 0; + AetheriaRuntimeThermalSimulation.ApplyWear( + entity, equipmentIndex, wear * (ReadBool(entry.Payload, 1, true) ? deltaSeconds : 1)); + } if (string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal)) state.StatModifierExecuted = true; } @@ -153,9 +189,66 @@ namespace GameCult.Aetheria.State.Verse state.StatModifierApplied = state.StatModifierExecuted; state.StatModifierTargetStatCount = CountTargets(entity, catalog, entry.Payload); } + + foreach (var entry in payloads.Select((payload, index) => (Payload: payload, Index: index))) + { + if (entry.Payload == null || !string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal) || + !states.TryGetValue((equipmentIndex, entry.Index), out var state)) + continue; + var duration = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1); + state.CooldownProgress -= deltaSeconds / Math.Max(double.Epsilon, duration); + } } } + private static double Evaluate( + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeCatalogSnapshot catalog, + int equipmentIndex, + int behaviorIndex, + AetheriaRuntimeBehaviorPayload payload, + AetheriaRuntimeBehaviorStateCommit state, + int fieldKey) + { + var equipment = entity.Equipment ?? Array.Empty(); + if (equipmentIndex < 0 || equipmentIndex >= equipment.Count || equipment[equipmentIndex]?.Item == null) + return 0; + var item = equipment[equipmentIndex].Item; + var typed = catalog.FindItem(item.ItemKey ?? ""); + if (typed == null) + return 0; + var behavior = new AetheriaRuntimeEquippedBehavior( + entity, catalog, equipmentIndex, behaviorIndex, equipment[equipmentIndex], item, typed, payload, state); + var thermal = (entity.EquipmentStates ?? Array.Empty()) + .FirstOrDefault(value => value?.EquipmentIndex == equipmentIndex)?.ThermalPerformance ?? 1; + return behavior.EvaluateStat(fieldKey, thermal); + } + + private static void ProjectCooldownProgress(AetheriaRuntimeEntitySnapshotCommit entity) + { + var retained = (entity.BehaviorProgress ?? Array.Empty()) + .Where(value => value != null && + !(string.Equals(value.OwnerKind, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal) && + string.Equals(value.BehaviorKind, "Cooldown", StringComparison.Ordinal))); + var cooldowns = (entity.BehaviorStates ?? Array.Empty()) + .Where(value => value != null && + string.Equals(value.OwnerKind, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal) && + string.Equals(value.BehaviorKind, "Cooldown", StringComparison.Ordinal)) + .Select(value => new AetheriaRuntimeBehaviorProgressCommit + { + OwnerKind = value.OwnerKind, + OwnerIndex = value.OwnerIndex, + BehaviorIndex = value.BehaviorIndex, + BehaviorKind = value.BehaviorKind, + Progress = Math.Max(0, Math.Min(1, value.CooldownProgress)) + }); + entity.BehaviorProgress = retained.Concat(cooldowns) + .OrderBy(value => value.OwnerKind, StringComparer.Ordinal) + .ThenBy(value => value.OwnerIndex) + .ThenBy(value => value.BehaviorIndex) + .ToArray(); + } + private static IEnumerable ActiveEquippedModifiers( AetheriaRuntimeEntitySnapshotCommit entity, AetheriaRuntimeCatalogSnapshot catalog) @@ -243,6 +336,20 @@ namespace GameCult.Aetheria.State.Verse private static AetheriaRuntimeBehaviorField? Field(AetheriaRuntimeBehaviorPayload payload, int key) => (payload?.Fields ?? Array.Empty()).FirstOrDefault(value => value?.Key == key); + private static bool ReadBool(AetheriaRuntimeBehaviorPayload payload, int key, bool fallback = false) + { + var field = Field(payload, key); + return field == null ? fallback : field.Value?.BoolValue ?? fallback; + } + + private static string ReadItemKey(AetheriaRuntimeBehaviorPayload payload, int key) + { + var value = Field(payload, key)?.Value; + return !string.IsNullOrWhiteSpace(value?.ItemKeyValue) + ? value.ItemKeyValue + : ReadString(value); + } + private static string ChildString(AetheriaRuntimeBehaviorValue? value, int index) => value != null && value.Children != null && value.Children.Count > index ? ReadString(value.Children[index]) : ""; diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeConsumableSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeConsumableSimulation.cs index 9500ea4f6..97d16d1bf 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeConsumableSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeConsumableSimulation.cs @@ -105,7 +105,7 @@ namespace GameCult.Aetheria.State.Verse if (payloads[behaviorIndex] != null && string.Equals(payloads[behaviorIndex].Kind, "StatModifier", StringComparison.Ordinal)) FindBehaviorState(effect, behaviorIndex, payloads[behaviorIndex]).StatModifierTargetStatCount = - AetheriaRuntimeStatModifierSimulation.CountTargets(entity, catalog!, payloads[behaviorIndex]); + AetheriaRuntimeBehaviorSimulation.CountTargets(entity, catalog!, payloads[behaviorIndex]); UpdateAlwaysUpdatedBehaviors(effect, payloads, effectiveness, deltaSeconds); for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++) { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index 71f6bec8c..686eec504 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -68,11 +68,12 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, deltaSeconds); AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog); } - AetheriaRuntimeStatModifierSimulation.Step( + AetheriaRuntimeBehaviorSimulation.Step( zone.ZoneIndex, entities, intents?.Behaviors, - catalog); + catalog, + deltaSeconds); AetheriaRuntimeConsumableSimulation.StepZone( run, zone, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs index cf7fc94d4..6a95136c2 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs @@ -69,7 +69,7 @@ namespace GameCult.Aetheria.State.Verse field?.Value, Item, Math.Max(0, Math.Min(1, thermalPerformance))); - return AetheriaRuntimeStatModifierSimulation.Apply( + return AetheriaRuntimeBehaviorSimulation.Apply( Entity, Catalog, EquipmentIndex, Payload, fieldKey, baseline); } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs index f4c7d2761..342bdc77c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs @@ -1515,6 +1515,9 @@ namespace GameCult.Aetheria.State.Verse [Key(54)] public bool TurretControllerPredictShots { get; set; } + [Key(58)] + public double CooldownProgress { get; set; } + [Key(55)] public string ResourceScannerTargetBodyKey { get; set; } = ""; diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeThermalSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeThermalSimulation.cs index 85e04d7e7..9ecc0142f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeThermalSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeThermalSimulation.cs @@ -214,14 +214,6 @@ namespace GameCult.Aetheria.State.Verse state.PreviousTemperature = temperature; states.Add(state); - var wearPayload = (typed.BehaviorPayloads ?? Array.Empty()) - .FirstOrDefault(value => value != null && value.Kind == "Wear"); - if (state.Online && wearPayload != null) - { - var perSecond = (wearPayload.Fields ?? Array.Empty()) - .FirstOrDefault(field => field != null && field.Key == 1)?.Value?.BoolValue ?? true; - ApplyWear(entity, index, state.Wear * (perSecond ? deltaSeconds : 1)); - } } entity.EquipmentStates = states; } diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 88b313fa3..b4e5c8a39 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -275,11 +275,14 @@ attached to the old `PerformanceStat` object graph rather than a compact blueprint/instance stat set. The daemon translation does not retain those object-owned dictionaries. -`AetheriaRuntimeStatModifierSimulation` persists switch, trigger, and modifier +`AetheriaRuntimeBehaviorSimulation` persists switch, trigger, and modifier lifecycle state in the authoritative snapshot. Runtime stat reads derive the fossil multiplier-product and constant-sum result from those sources, including required-behavior and descendant-kind targeting. This makes reconnect replay idempotent and keeps the Unity fossil out of the gameplay authority path. +The same daemon organ executes common resource gates in group order. Energy, +thermal cells, cargo, and durability remain owned by their narrow transaction +subsystems; behavior composition decides only whether and when to call them. Migration target: diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 700beae20..96b2bf12d 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -305,6 +305,16 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. values, sums all constants, and returns `base * product + sum`. There is no client modifier owner and no reconnect-time mutable modifier cache to double apply. Offline, disabled, or broken source equipment contributes nothing. + The same executor owns the fossil common resource behaviors in authored group + order. `Cooldown` persists its normalized scalar and publishes generic + behavior progress; `EnergyDraw` transacts against the capacitor/reactor + owner; `Heat` writes the source equipment's thermal cells; `ItemUsage` + removes exactly one cargo item through the atomic cargo transaction; and + `Wear` applies thermal simulation's computed wear pressure at its authored + cadence. A refused cooldown, energy draw, or item use stops the group before + downstream heat, wear, or modifiers. Thermal simulation computes wear but is + no longer allowed to apply it merely because an item contains a `Wear` + payload. - Sensor contact information is daemon-owned. Each operational installed `Sensor` behavior executes the fossil information equation against target visibility, distance, observer-relative angle, its authored sensitivity diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 39dad2cf2..f10c59d4d 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -43,7 +43,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | 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 | 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; the released full-session profile starts with no pickup, destroys a deterministic target, observes its canonical cargo drop, tractors it into a Ymir Begin contact, and proves exactly one `ymir-fact:` collection receipt plus cargo `0` to `1` | 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; `Switch` and one-shot `Trigger` gate ordered equipment groups; equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining behavior families, full resource-gated chain ordering, and canonical consumable-content proof | +| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; cooldown projects generic progress. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining specialized behavior return-value composition and canonical consumable-content proof | | Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data | | AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Operational orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; non-player identity no longer grants implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing | cancellation, reconnect, multi-client command transfer, and model-local barrel-direction metadata | | Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, `ItemUsage`, and stateful `Cooldown` payloads after energy tick initialization; every effect and payload state has stable authored identity; cooldowns update before the ordered chain, require state strictly below zero, reset to one on success, and continue aging behind earlier failures; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity reconciles presentation by effect identity and cannot restore active behavior state by list index | catalog revision pinning, stat modifiers, remaining behavior families, and reconnect presentation proof |