Restore patrol combat preemption

This commit is contained in:
Meta Vens 2026-07-17 09:38:21 +01:00
commit c78fdb0361
8 changed files with 284 additions and 11 deletions

View file

@ -103,7 +103,12 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
public void RunTutorialMaterialization() => RunCheck(TutorialPopulationPreservesFossilEntityMechanics);
public void RunTutorialRun() => RunCheck(TutorialRunPersistsCanonicalWorldTruth);
public void RunOrbits() => RunCheck(GeneratedOrbitsAdvanceOrbitalWorldTruth);
public void RunPatrol() => RunCheck(AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands);
public void RunPatrol()
{
RunCheck(SecurityZonesOwnFossilTrespasserHostility);
RunCheck(AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands);
RunCheck(PatrolCombatPreemptsAndResumesTheSameCircuit);
}
private static void TutorialRunPersistsCanonicalWorldTruth()
{
@ -5199,6 +5204,137 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"patrol is persistent work and must remain assigned after one circuit");
}
private static void SecurityZonesOwnFossilTrespasserHostility()
{
var guard = Entity(0, 0, "owners");
guard.SecurityLevel = 2;
guard.SecurityRadius = 100;
var visitor = Entity(1, 10, "visitors");
guard.Contacts = [new AetheriaRuntimeEntityContactCommit { TargetEntityIndex = 1, InfoGathered = 1 }];
visitor.Contacts = [new AetheriaRuntimeEntityContactCommit { TargetEntityIndex = 0, InfoGathered = 1 }];
var zone = new AetheriaRuntimeZoneSnapshotCommit
{
ZoneIndex = 0,
OwnerFactionIndex = 0,
Entities = [guard, visitor]
};
var run = new AetheriaRuntimeRunCheckpointCommit
{
CurrentEntityKey = "zone.0.entity.1",
FactionRelationships =
[
new AetheriaRuntimeFactionRelationshipCommit
{
FactionKey = "owners", Relationship = "Neutral"
}
],
Zones = [zone]
};
var catalog = new AetheriaRuntimeCatalogSnapshot([], [new AetheriaRuntimeCorporation(
"owners", "Owners", "Owners", "", "", "", 4, 0,
Array.Empty<AetheriaRuntimeCorporationAllegiance>())], []);
AetheriaRuntimeSensorSimulation.StepZone(
run, zone, [guard, visitor], null, catalog, 0.1, 0, 0.5, 1);
Require(guard.Contacts.Single().Hostile && visitor.Contacts.Single().Hostile,
"a neutral visitor inside an owner critical-security field must be mutually hostile");
visitor.PositionX = 1000;
AetheriaRuntimeSensorSimulation.StepZone(
run, zone, [guard, visitor], null, catalog, 0.1, 0, 0.5, 2);
Require(!guard.Contacts.Single().Hostile && !visitor.Contacts.Single().Hostile,
"the same neutral visitor outside security fields must cease being hostile");
}
private static void PatrolCombatPreemptsAndResumesTheSameCircuit()
{
var patrol = Entity(0, 0, "owners");
patrol.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Patrol];
patrol.WeaponGroups = [new[] { 0 }];
patrol.Equipment =
[
new AetheriaRuntimeLoadoutItemSlotCommit
{
Item = new AetheriaRuntimeLoadoutItemCommit
{
ItemKey = "patrol-cannon", Quality = 1, Durability = 1, Enabled = true
}
}
];
patrol.Contacts =
[
new AetheriaRuntimeEntityContactCommit
{
TargetEntityIndex = 1, InfoGathered = 1, Visible = true, Hostile = true
}
];
var hostile = Entity(1, 0, "visitors");
hostile.PositionZ = 50;
var weapon = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
[
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
new AetheriaRuntimeBehaviorField(5, PerformanceStat(0)),
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5))
]);
var catalog = new AetheriaRuntimeCatalogSnapshot(
[WearableWeaponCatalogItem("patrol-cannon", weapon)], [], []);
var task = new AetheriaRuntimeAgentTaskCommit
{
TaskId = "patrol-combat",
CorporationKey = "owners",
TaskType = AetheriaRuntimeAgentTaskTypes.Patrol,
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
ZoneIndex = 0,
AssignedEntityIndex = 0,
CompletionRadius = 5,
TargetOrbitKeys = ["orbit:east", "orbit:west"],
CircuitIndex = 1
};
patrol.AssignedAgentTaskId = task.TaskId;
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "patrol-combat-smoke",
CurrentEntityKey = "",
AgentTasks = [task],
Zones =
[
new AetheriaRuntimeZoneSnapshotCommit
{
ZoneIndex = 0,
Entities = [patrol, hostile],
Orbits =
[
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:east", FixedPositionX = 20 },
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:west", FixedPositionX = -20 }
]
}
]
};
var combatCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan(
run, 1, catalog, 0.1, AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
Require(combatCommands.Any(command =>
command.Kind == AetheriaRuntimeDaemonCommandKinds.SetTarget &&
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
command.TargetEntityKey, out var zoneIndex, out var entityIndex) &&
zoneIndex == 0 && entityIndex == 1),
"a visible hostile ship must pre-empt patrol movement through the shared target command");
Require(combatCommands.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup),
"an aligned in-range patrol agent must use the shared weapon command");
RequireEqual(1, task.CircuitIndex,
"opportunistic combat must not replace or advance the patrol circuit");
patrol.Contacts = Array.Empty<AetheriaRuntimeEntityContactCommit>();
var resumed = AetheriaRuntimeAgentScheduler.AssignAndPlan(
run, 2, catalog, 0.2, AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
Require(resumed.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetMoveVector),
"loss of hostile visibility must resume the same patrol circuit");
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, task.Status,
"opportunistic combat must never complete or fail the patrol task");
}
private static void RejectedHaulTransferDoesNotAdvanceTask()
{
var origin = Entity(0, 0, "workers");

View file

@ -302,6 +302,16 @@ namespace GameCult.Aetheria.State.Verse
if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Defend, StringComparison.Ordinal) ||
string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Patrol, StringComparison.Ordinal))
{
var hostile = FirstVisibleHostileShip(zone!, agent);
if (hostile != null && ResolveAgentWeapons(agent, catalog).Count > 0)
{
var combatDx = hostile.PositionX - agent.PositionX;
var combatDz = hostile.PositionZ - agent.PositionZ;
commands.AddRange(PlanAttack(
run, task, agent, hostile, frameId, combatDx, combatDz,
Math.Sqrt(combatDx * combatDx + combatDz * combatDz), catalog, simulationSettings));
continue;
}
commands.AddRange(PlanPatrol(zone!, task, agent, frameId));
continue;
}
@ -1123,6 +1133,25 @@ namespace GameCult.Aetheria.State.Verse
return new[] { Movement(task, agent, frameId, dx, dz, 1) };
}
private static AetheriaRuntimeEntitySnapshotCommit? FirstVisibleHostileShip(
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit agent)
{
var hostileIndices = (agent.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
.Where(contact => contact != null && contact.Visible && contact.Hostile)
.Select(contact => contact.TargetEntityIndex)
.ToArray();
foreach (var index in hostileIndices)
{
var target = (zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.FirstOrDefault(entity => entity != null && entity.EntityIndex == index &&
entity.IsActive && string.Equals(entity.Kind, "ship", StringComparison.OrdinalIgnoreCase));
if (target != null)
return target;
}
return null;
}
private static AetheriaRuntimeDaemonCommandDocument Movement(
AetheriaRuntimeAgentTaskCommit task,
AetheriaRuntimeEntitySnapshotCommit agent,

View file

@ -111,7 +111,8 @@ namespace GameCult.Aetheria.State.Verse
deltaSeconds,
settings.TargetInfoDecay,
activeRenderSettings.TargetDetectionInfoThreshold,
frameId);
frameId,
settings.SecureAreaRadiusMultiplier);
AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
AetheriaRuntimeSurveySimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
}

View file

@ -110,7 +110,8 @@ namespace GameCult.Aetheria.State.Verse
double wormholeExitRadius = AetheriaDefaultRaw.WormholeExitRadius,
double wormholeDepth = AetheriaDefaultRaw.WormholeDepth,
double visibilityDecay = AetheriaDefaultRaw.VisibilityDecay,
double targetInfoDecay = AetheriaDefaultRaw.TargetInfoDecay)
double targetInfoDecay = AetheriaDefaultRaw.TargetInfoDecay,
double secureAreaRadiusMultiplier = AetheriaDefaultRaw.SecureAreaRadiusMultiplier)
{
PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed);
RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed);
@ -165,6 +166,8 @@ namespace GameCult.Aetheria.State.Verse
WormholeDepth = PositiveOr(wormholeDepth, AetheriaDefaultRaw.WormholeDepth);
VisibilityDecay = NonNegativeOr(visibilityDecay, AetheriaDefaultRaw.VisibilityDecay);
TargetInfoDecay = NonNegativeOr(targetInfoDecay, AetheriaDefaultRaw.TargetInfoDecay);
SecureAreaRadiusMultiplier = PositiveOr(
secureAreaRadiusMultiplier, AetheriaDefaultRaw.SecureAreaRadiusMultiplier);
}
public double PawnSpeed { get; }
@ -222,6 +225,7 @@ namespace GameCult.Aetheria.State.Verse
public double WormholeDepth { get; }
public double VisibilityDecay { get; }
public double TargetInfoDecay { get; }
public double SecureAreaRadiusMultiplier { get; }
private static double PositiveOr(double value, double fallback)
{
@ -293,6 +297,7 @@ namespace GameCult.Aetheria.State.Verse
public const double WormholeDepth = 1000.0;
public const double VisibilityDecay = 0.5;
public const double TargetInfoDecay = 0.5;
public const double SecureAreaRadiusMultiplier = 0.425;
}
}
}

View file

@ -17,7 +17,8 @@ namespace GameCult.Aetheria.State.Verse
double deltaSeconds,
double targetInfoDecay,
double detectionThreshold,
long frameId)
long frameId,
double secureAreaRadiusMultiplier = 0.425)
{
if (catalog == null || deltaSeconds <= 0)
return;
@ -44,7 +45,7 @@ namespace GameCult.Aetheria.State.Verse
foreach (var sensor in sensors)
StepSensor(run, zone, observer, sensor, entities, contacts, catalog,
deltaSeconds, targetInfoDecay, frameId);
deltaSeconds, targetInfoDecay, secureAreaRadiusMultiplier, frameId);
observer.Contacts = entities
.Where(target => target != null && target.EntityIndex != observer.EntityIndex)
@ -56,7 +57,9 @@ namespace GameCult.Aetheria.State.Verse
{
TargetEntityIndex = target.EntityIndex,
InfoGathered = information,
Hostile = Hostile(observer, target),
Hostile = Hostile(
run, zone, entities, catalog, observer, target,
secureAreaRadiusMultiplier),
Visible = information > detectionThreshold
};
})
@ -108,6 +111,7 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeCatalogSnapshot catalog,
double deltaSeconds,
double targetInfoDecay,
double secureAreaRadiusMultiplier,
long frameId)
{
var thermal = ThermalPerformance(observer, sensor.EquipmentIndex);
@ -164,7 +168,9 @@ namespace GameCult.Aetheria.State.Verse
{
TargetEntityIndex = target.EntityIndex,
InfoGathered = next,
Hostile = Hostile(observer, target)
Hostile = Hostile(
run, zone, entities, catalog, observer, target,
secureAreaRadiusMultiplier)
};
}
sensor.State.PingedEntityIndices = pinged.OrderBy(index => index).ToArray();
@ -250,6 +256,94 @@ namespace GameCult.Aetheria.State.Verse
.ThermalPerformance ?? 1;
private static bool Hostile(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeCatalogSnapshot catalog,
AetheriaRuntimeEntitySnapshotCommit left,
AetheriaRuntimeEntitySnapshotCommit right,
double secureAreaRadiusMultiplier)
{
var ownerKey = zone.OwnerFactionIndex >= 0 &&
zone.OwnerFactionIndex < (catalog.Corporations?.Count ?? 0)
? catalog.Corporations[zone.OwnerFactionIndex].CorporationKey ?? ""
: "";
if (string.IsNullOrWhiteSpace(ownerKey))
return LegacyHostile(left, right);
return OwnerRejects(run, zone, entities, left, right, ownerKey, secureAreaRadiusMultiplier) ||
OwnerRejects(run, zone, entities, right, left, ownerKey, secureAreaRadiusMultiplier);
}
private static bool OwnerRejects(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeEntitySnapshotCommit ownerEntity,
AetheriaRuntimeEntitySnapshotCommit visitor,
string ownerKey,
double secureAreaRadiusMultiplier)
{
if (!string.Equals(ownerEntity.FactionKey, ownerKey, StringComparison.OrdinalIgnoreCase))
return false;
var relationship = RelationshipTo(run, zone, visitor, ownerKey);
var security = LocalSecurity(
entities, visitor.PositionX, visitor.PositionZ, ownerKey, secureAreaRadiusMultiplier);
return relationship - security <= 0;
}
private static int RelationshipTo(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit entity,
string factionKey)
{
if (string.Equals(entity.FactionKey, factionKey, StringComparison.OrdinalIgnoreCase))
return 4;
if (AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
run.CurrentEntityKey, out var playerZoneIndex, out var playerEntityIndex) &&
playerZoneIndex == zone.ZoneIndex && playerEntityIndex == entity.EntityIndex)
{
var relationship = (run.FactionRelationships ?? Array.Empty<AetheriaRuntimeFactionRelationshipCommit>())
.FirstOrDefault(value => value != null &&
string.Equals(value.FactionKey, factionKey, StringComparison.OrdinalIgnoreCase));
return RelationshipValue(relationship?.Relationship);
}
return 2;
}
private static int LocalSecurity(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
double positionX,
double positionZ,
string ownerKey,
double secureAreaRadiusMultiplier)
{
var security = 0;
foreach (var entity in entities)
{
if (entity == null || entity.SecurityRadius <= 1 ||
!string.Equals(entity.FactionKey, ownerKey, StringComparison.OrdinalIgnoreCase))
continue;
var dx = entity.PositionX - positionX;
var dz = entity.PositionZ - positionZ;
var radius = entity.SecurityRadius * secureAreaRadiusMultiplier;
if (dx * dx + dz * dz < radius * radius)
security = Math.Max(security, entity.SecurityLevel);
}
return security;
}
private static int RelationshipValue(string? relationship)
{
if (string.Equals(relationship, "Hated", StringComparison.OrdinalIgnoreCase)) return 0;
if (string.Equals(relationship, "Hostile", StringComparison.OrdinalIgnoreCase)) return 1;
if (string.Equals(relationship, "Friendly", StringComparison.OrdinalIgnoreCase)) return 3;
if (string.Equals(relationship, "Beloved", StringComparison.OrdinalIgnoreCase)) return 4;
return 2;
}
private static bool LegacyHostile(
AetheriaRuntimeEntitySnapshotCommit left,
AetheriaRuntimeEntitySnapshotCommit right) =>
string.Equals(left.FactionKey, "player", StringComparison.OrdinalIgnoreCase) !=

View file

@ -1148,7 +1148,11 @@ the Terminus proof arena cannot award completion.
- Agent work: each generated NPC ship receives one assigned `patrol` task with
the fossil four-orbit shuffled circuit. Orbit circuits are typed separately
from mining/survey body targets and survive bootstrap into the runtime
checkpoint; Unity no longer creates `Minion` patrol state on activation.
checkpoint. Daemon sensor policy derives the fossil owner-security trespass
rule from faction relationships and orbital security fields. The first
visible hostile ship temporarily drives the shared attack planner without
replacing or advancing the patrol task; loss of contact resumes the same
circuit. Unity no longer creates `Minion` patrol or combat state on activation.
- Physical placement: the daemon resolves orbital entities from the canonical
orbit graph before persistence and deterministically distributes generated
non-player ships through the fossil half-zone volume. The controlled starter
@ -1170,7 +1174,8 @@ the Terminus proof arena cannot award completion.
- Verification layer: focused smokes prove deterministic population, resolved
orbital references and poses, distributed non-player spawns,
fixed-step orbit phase/pose/velocity through Ymir, station/turret/agent
presence, assigned four-orbit patrol persistence and movement, loadout
presence, assigned four-orbit patrol persistence and movement, critical-zone
trespass hostility, combat pre-emption and same-circuit resumption, loadout
provenance, all 64 persisted zones, canonical starter identity, discovery,
and adjacency.

View file

@ -45,7 +45,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
| 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`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it. `Thruster`, rare `AetherDrive`, and `Radiator` now pause their groups at the specialized owner, report the exact fossil success result, and resume common tails at most once, so idle or unfunded actuators cannot consume wear; radiator emission remains its independent always-update path. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. 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 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. `patrol` is a first-class task with orbit targets distinct from survey/mining body targets; every generated tutorial NPC persists its assigned fossil four-orbit shuffled circuit before the first tick. 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. Chain-admitted 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; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no 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. Cancel, complete, fail, stale-assignment repair, and duplicate-carrier repair share one scheduler-owned release primitive that zeros persisted helm input, clears the scheduler target, releases towing parentage, and prevents a terminal task from emitting another command. Real CultCache close/reopen proofs now cover both ordinary mid-travel and the first `entering` phase of a two-edge route; fresh Ymir owners resume without duplicate tasks, entities, or transition events, and Eve commander observes terminal status | multi-client command transfer and model-local barrel-direction metadata |
| 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. `patrol` is a first-class task with orbit targets distinct from survey/mining body targets; every generated tutorial NPC persists its assigned fossil four-orbit shuffled circuit before the first tick. Daemon sensors derive the fossil owner-security trespass rule from the controlled pilot's faction relationship and orbital security fields. Like the fossil `Minion`, a patrol agent temporarily enters the shared combat planner for its first visible hostile ship, keeps its circuit index, and resumes that same patrol when visibility is lost. 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. Chain-admitted 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; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no 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. Cancel, complete, fail, stale-assignment repair, and duplicate-carrier repair share one scheduler-owned release primitive that zeros persisted helm input, clears the scheduler target, releases towing parentage, and prevents a terminal task from emitting another command. Real CultCache close/reopen proofs now cover both ordinary mid-travel and the first `entering` phase of a two-edge route; fresh Ymir owners resume without duplicate tasks, entities, or transition events, and Eve commander observes terminal status | 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 |
## Ownership Invariants

View file

@ -158,7 +158,10 @@ step advances canonical phase and projects orbital entity pose and velocity
before Ymir. Body queries and Eve lower the same orbit graph.
Each generated NPC also enters with a daemon-owned assigned patrol task over
four shuffled orbit identities, matching the fossil activation behavior without
letting the Unity client create agent state.
letting the Unity client create agent state. The daemon derives hostility from
the fossil owner-security and faction-relationship rule. A visible hostile ship
pre-empts circuit movement through the existing combat planner; the patrol task
keeps its circuit index and resumes it when contact is lost.
New Game now submits its advertised Eve operation, waits for a newly published
daemon sector map, and selects the persisted 64-zone tutorial while