Restore daemon-owned consumable lifecycle
This commit is contained in:
parent
be92f4c7af
commit
c4ae06d9cf
7 changed files with 332 additions and 7 deletions
|
|
@ -4920,6 +4920,121 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.IsTrue(result.Intents.SensorPingRequested);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConsumableSimulationAtomicallyConsumesCargoPreservesQualityAndExpires()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
var actor = run.Zones[0].Entities[0];
|
||||
actor.CargoContents = new[]
|
||||
{
|
||||
new AetheriaRuntimeCargoBayLoadoutCommit
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
X = 2,
|
||||
Y = 3,
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = "repair-gel",
|
||||
Quantity = 2,
|
||||
Quality = 0.73
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { CatalogItem("repair-gel", Array.Empty<AetheriaRuntimeBehaviorPayload>(), category: AetheriaRuntimeItemCategories.Consumable, stackable: false, duration: 2) },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var intents = new[] { new AetheriaRuntimeDaemonConsumableIntent { ActorEntityKey = "zone.0.entity.0", ItemKey = "repair-gel" } };
|
||||
|
||||
AetheriaRuntimeConsumableSimulation.Step(run, intents, catalog, 40, 0.5);
|
||||
|
||||
Assert.AreEqual(1, actor.CargoContents[0].Items[0].Item.Quantity);
|
||||
Assert.AreEqual(1, actor.ActiveConsumables.Count);
|
||||
Assert.AreEqual(0.73, actor.ActiveConsumables[0].Quality, 0.0001);
|
||||
Assert.AreEqual(1.5, actor.ActiveConsumables[0].RemainingDuration, 0.0001);
|
||||
Assert.AreEqual("consumable.activated", run.GameEvents.Single().Kind);
|
||||
|
||||
AetheriaRuntimeConsumableSimulation.Step(run, Array.Empty<AetheriaRuntimeDaemonConsumableIntent>(), catalog, 41, 1.5);
|
||||
Assert.AreEqual(1, actor.ActiveConsumables.Count, "The fossil grants one final update at exactly zero duration.");
|
||||
AetheriaRuntimeConsumableSimulation.Step(run, Array.Empty<AetheriaRuntimeDaemonConsumableIntent>(), catalog, 42, 0.01);
|
||||
Assert.AreEqual(0, actor.ActiveConsumables.Count);
|
||||
Assert.AreEqual(1, run.GameEvents.Count(value => value.Kind == "consumable.expired"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConsumableSimulationRejectsDuplicateWithoutConsumingCargo()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
var actor = run.Zones[0].Entities[0];
|
||||
actor.CargoContents = new[]
|
||||
{
|
||||
new AetheriaRuntimeCargoBayLoadoutCommit
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "repair-gel", Quantity = 1, Quality = 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
actor.ActiveConsumables = new[]
|
||||
{
|
||||
new AetheriaRuntimeActiveConsumableCommit { ItemKey = "repair-gel", Duration = 2, RemainingDuration = 1 }
|
||||
};
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { CatalogItem("repair-gel", Array.Empty<AetheriaRuntimeBehaviorPayload>(), category: AetheriaRuntimeItemCategories.Consumable, stackable: false, duration: 2) },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
|
||||
AetheriaRuntimeConsumableSimulation.Step(run,
|
||||
new[] { new AetheriaRuntimeDaemonConsumableIntent { ActorEntityKey = "zone.0.entity.0", ItemKey = "repair-gel" } },
|
||||
catalog, 50, 0.25);
|
||||
|
||||
Assert.AreEqual(1, actor.CargoContents[0].Items[0].Item.Quantity);
|
||||
Assert.AreEqual(1, actor.ActiveConsumables.Count);
|
||||
Assert.AreEqual("already-active", run.GameEvents.Single(value => value.Kind == "consumable.activation.refused").Reason);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InputCapabilitiesAdvertiseOnlyCurrentlyActivatableConsumables()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
run.CurrentEntityKey = run.EntityRecordKey(0, 0);
|
||||
var actor = run.Zones[0].Entities[0];
|
||||
actor.CargoContents = new[]
|
||||
{
|
||||
new AetheriaRuntimeCargoBayLoadoutCommit
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "repair-gel", Quantity = 1 } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "ore", Quantity = 4 } }
|
||||
}
|
||||
}
|
||||
};
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[]
|
||||
{
|
||||
CatalogItem("repair-gel", Array.Empty<AetheriaRuntimeBehaviorPayload>(), category: AetheriaRuntimeItemCategories.Consumable, duration: 2),
|
||||
CatalogItem("ore", Array.Empty<AetheriaRuntimeBehaviorPayload>(), category: AetheriaRuntimeItemCategories.SimpleCommodity)
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var frame = new AetheriaRuntimeDaemonFrameDocument { FrameId = 70, Run = run };
|
||||
|
||||
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame, catalog: catalog);
|
||||
|
||||
Assert.IsTrue(capability.Actions.Any(action => action.Operation.EndsWith("ActivateConsumable", StringComparison.Ordinal) && action.Payload["itemKey"] == "repair-gel"));
|
||||
Assert.IsFalse(capability.Actions.Any(action => action.Operation.EndsWith("ActivateConsumable", StringComparison.Ordinal) && action.Payload["itemKey"] == "ore"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BehaviorStateProjectorDerivesEquipmentRowsFromCatalogPayloads()
|
||||
{
|
||||
|
|
@ -5642,12 +5757,15 @@ public class DaemonRuntimeDocumentTests
|
|||
string itemKey,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> behaviorPayloads,
|
||||
int price = 0,
|
||||
string hullType = "")
|
||||
string hullType = "",
|
||||
string category = "",
|
||||
bool stackable = false,
|
||||
double duration = 0)
|
||||
{
|
||||
return new AetheriaRuntimeCatalogItem(
|
||||
itemKey,
|
||||
itemKey,
|
||||
"",
|
||||
category,
|
||||
"",
|
||||
"",
|
||||
price,
|
||||
|
|
@ -5669,8 +5787,8 @@ public class DaemonRuntimeDocumentTests
|
|||
hullType,
|
||||
behaviorPayloads.Select(payload => payload.Kind).ToArray(),
|
||||
1,
|
||||
false,
|
||||
0,
|
||||
stackable,
|
||||
duration,
|
||||
1,
|
||||
"",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeConsumableSimulation
|
||||
{
|
||||
public static void Step(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
IEnumerable<AetheriaRuntimeDaemonConsumableIntent> intents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (run == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
var intentIndex = 0;
|
||||
foreach (var intent in intents ?? Enumerable.Empty<AetheriaRuntimeDaemonConsumableIntent>())
|
||||
{
|
||||
Activate(run, intent, catalog, frameId, intentIndex);
|
||||
intentIndex++;
|
||||
}
|
||||
|
||||
foreach (var zone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
foreach (var entity in zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
if (entity == null)
|
||||
continue;
|
||||
|
||||
var active = (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Where(effect => effect != null)
|
||||
.ToArray();
|
||||
for (var index = 0; index < active.Length; index++)
|
||||
{
|
||||
active[index].RemainingDuration -= deltaSeconds;
|
||||
if (active[index].RemainingDuration < 0)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:consumable:{index}:expired",
|
||||
Kind = "consumable.expired",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = entity.EntityIndex,
|
||||
ItemKey = active[index].ItemKey
|
||||
});
|
||||
}
|
||||
}
|
||||
entity.ActiveConsumables = active.Where(effect => effect.RemainingDuration >= 0).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CanActivate(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
string itemKey)
|
||||
{
|
||||
var item = catalog?.FindItem(itemKey);
|
||||
return entity != null && item != null &&
|
||||
string.Equals(item.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal) &&
|
||||
item.Duration > 0 &&
|
||||
(item.Stackable || !(entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Any(active => active != null && string.Equals(active.ItemKey, itemKey, StringComparison.Ordinal))) &&
|
||||
AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out _, out _, out _);
|
||||
}
|
||||
|
||||
private static void Activate(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeDaemonConsumableIntent? intent,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
int intentIndex)
|
||||
{
|
||||
var itemKey = intent?.ItemKey ?? "";
|
||||
if (!TryResolveActor(run, intent?.ActorEntityKey, out var zone, out var entity))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "actor-unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = catalog?.FindItem(itemKey);
|
||||
if (item == null || !string.Equals(item.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "not-consumable");
|
||||
return;
|
||||
}
|
||||
if (item.Duration <= 0)
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "invalid-duration");
|
||||
return;
|
||||
}
|
||||
if (!item.Stackable && (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Any(active => active != null && string.Equals(active.ItemKey, itemKey, StringComparison.Ordinal)))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "already-active");
|
||||
return;
|
||||
}
|
||||
if (!AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out var consumed))
|
||||
{
|
||||
Refuse(run, frameId, intentIndex, zone, entity, itemKey, "missing-cargo");
|
||||
return;
|
||||
}
|
||||
|
||||
entity.ActiveConsumables = (entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Where(active => active != null)
|
||||
.Append(new AetheriaRuntimeActiveConsumableCommit
|
||||
{
|
||||
ItemKey = item.ItemKey,
|
||||
Quality = consumed.Item.Quality,
|
||||
Duration = item.Duration,
|
||||
RemainingDuration = item.Duration
|
||||
})
|
||||
.ToArray();
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:consumable:{intentIndex}:activated",
|
||||
Kind = "consumable.activated",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = entity.EntityIndex,
|
||||
ItemKey = item.ItemKey,
|
||||
ScalarValue = consumed.Item.Quality,
|
||||
AuxiliaryValue = item.Duration
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryResolveActor(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
string? actorEntityKey,
|
||||
out AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
out AetheriaRuntimeEntitySnapshotCommit? entity)
|
||||
{
|
||||
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(actorEntityKey ?? "", out var actorZoneIndex, out var actorEntityIndex);
|
||||
foreach (var candidateZone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
foreach (var candidate in candidateZone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
var canonicalKey = candidate == null
|
||||
? ""
|
||||
: run.EntityRecordKey(candidateZone.ZoneIndex, candidate.EntityIndex);
|
||||
if (candidate != null &&
|
||||
(string.Equals(canonicalKey, actorEntityKey, StringComparison.Ordinal) ||
|
||||
(candidateZone.ZoneIndex == actorZoneIndex && candidate.EntityIndex == actorEntityIndex)))
|
||||
{
|
||||
zone = candidateZone;
|
||||
entity = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
zone = null;
|
||||
entity = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Refuse(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
long frameId,
|
||||
int intentIndex,
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
string itemKey,
|
||||
string reason)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:consumable:{intentIndex}:refused",
|
||||
Kind = "consumable.activation.refused",
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone?.ZoneIndex ?? -1,
|
||||
SourceEntityIndex = entity?.EntityIndex ?? -1,
|
||||
ItemKey = itemKey,
|
||||
Reason = reason
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76ddd6e0004f0a247a16d1a393d768d1
|
||||
|
|
@ -32,6 +32,13 @@ namespace GameCult.Aetheria.State.Verse
|
|||
if (worldPhysics == null)
|
||||
throw new ArgumentNullException(nameof(worldPhysics));
|
||||
|
||||
AetheriaRuntimeConsumableSimulation.Step(
|
||||
run,
|
||||
intents?.Consumables ?? Enumerable.Empty<AetheriaRuntimeDaemonConsumableIntent>(),
|
||||
catalog,
|
||||
frameId,
|
||||
deltaSeconds);
|
||||
|
||||
foreach (var zone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
||||
{
|
||||
if (zone == null)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,16 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
actions.AddRange((entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>()).Select((_, index) => Action($"weapon-group.{index}.fire", $"Fire Weapon Group {index + 1}", "FireWeaponGroup", "weapon-group", $"{run.CurrentEntityKey}#weapon-group/{index}", ("weaponGroup", index.ToString()))));
|
||||
actions.AddRange((entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).Where(slot => slot?.Item != null).Select((slot, index) => Action($"equipment.{index}.activate", $"Activate {slot.Item.ItemKey}", "SetBehaviorActive", "equipment", $"{run.CurrentEntityKey}#equipment/{index}", ("equipmentIndex", index.ToString()), ("behaviorIndex", "0"), ("active", "true"))));
|
||||
actions.AddRange((entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).SelectMany(bay => bay.Items).Where(slot => slot?.Item != null).Select((slot, index) => Action($"cargo.{slot.Item.ItemKey}.{index}.use", $"Use {slot.Item.ItemKey}", "ActivateConsumable", "consumable", $"{run.CurrentEntityKey}#cargo/{index}", ("itemKey", slot.Item.ItemKey))));
|
||||
actions.AddRange((entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.SelectMany(bay => bay.Items)
|
||||
.Where(slot => slot?.Item != null && AetheriaRuntimeConsumableSimulation.CanActivate(entity, catalog, slot.Item.ItemKey))
|
||||
.GroupBy(slot => slot.Item.ItemKey, StringComparer.Ordinal)
|
||||
.Select((group, index) =>
|
||||
{
|
||||
var itemKey = group.Key;
|
||||
var item = catalog?.FindItem(itemKey);
|
||||
return Action($"cargo.{itemKey}.use", $"Use {item?.Name ?? itemKey}", "ActivateConsumable", "consumable", $"{run.CurrentEntityKey}#cargo/{index}", ("itemKey", itemKey));
|
||||
}));
|
||||
actions.AddRange(TradeActions(run, entity, catalog));
|
||||
}
|
||||
return new AetheriaRuntimeInputCapabilityDocument { Version = frame?.FrameId ?? 0, Actions = actions.ToArray(), DefaultProfiles = BuildDefaultProfiles(actions) };
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
of falling through to legacy DTO classification. Slot-binding drag/drop now
|
||||
routes through typed daemon operations, and rebuilt current-entity binds come from typed
|
||||
action-bar descriptor rows instead of stale slot-local `Entity` references.
|
||||
`AetheriaRuntimeConsumableSimulation` is the sole live owner of consumable
|
||||
activation and lifetime. It resolves semantic activation intents against the
|
||||
typed catalog and the actor's concrete cargo, atomically removes one unit,
|
||||
preserves that instance's quality, applies authored stackability, and creates
|
||||
the canonical active-effect row. Daemon simulation time ages and expires those
|
||||
rows and publishes lifecycle/refusal facts. Input capability projection derives
|
||||
only currently activatable consumable actions from this state; Unity's restored
|
||||
`ConsumableItemEffect` and action-bar fill are read models and cannot consume
|
||||
cargo or advance timers.
|
||||
Consumable activation, active-duration fill, runtime duration, and effectiveness curves now use typed
|
||||
item rows with neutral defaults for missing optional facets. Gear action-bar bindings read custom icon resource
|
||||
paths from typed item rows, then use typed weapon and hardpoint facets for
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | live generic Unity witness now proves advertised dock/undock actions, daemon bay assignment, dock-parent inclusion in the SoA view, sharing of the dock parent's equipment-derived contact picture, generic camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock; the starter station and local raider form a deterministic Terminus departure proof | restore fossil component-prerequisite rejection reasons, exact undock placement, wormhole priority, reconnect, and docked inventory/trade surfaces |
|
||||
| Inventory/equipment | inventory panels + ServerShared | daemon | inventory/equipment surfaces | generic UI | owner-exists; wired/projected in fragments; not proven | all transfer directions and rejection |
|
||||
| Trade | trade UI/economy | daemon | stock, quote, purchase commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; Eve advertises only currently available stock actions; generic Unity live witness receives an authoritative purchase receipt while paused | sell flow, quote expiry, contested stock, and full trade surface parity |
|
||||
| Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; dynamic original behavior source absent | dynamic equipment/cargo actions |
|
||||
| Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; cargo actions are catalog-filtered to consumables the current actor can actually activate, including suppression of active non-stackable items | dynamic equipment behavior validity and runtime action-bar quantity/fill proof |
|
||||
| Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | basic follow lowered; dock/view parity absent | authored framing and dock transition |
|
||||
| Render channels | Unity layers/culling masks | provider variant + runtime | semantic view id | native layer policy | specification work; incomplete uncommitted prototype | no map glyphs in pilot view |
|
||||
| Minimap/tactical map | `ZoneRenderer`, map cameras | daemon world facts | explicit map projections | Unity and Electron | facts fragmented; generic lowerers absent | same state in both runtimes |
|
||||
|
|
@ -44,7 +44,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; shared evaluated-stat query proven; execution families remain partial | every behavior family, ordering, resource transaction and modifier lifecycle |
|
||||
| Sensors/visibility | installed sensor sensitivity, decaying signature sources, and observer information thresholds | daemon | contacts and ping events | indicators/maps | passive reach now derives from enabled installed Sensor behaviors with diminishing-return array composition regardless of entity kind; contact refresh no longer overwrites emitted visibility; the old station range survives only as obsolete snapshot compatibility | author station/ship sensor loadout budgets, then prove power/thermal shutdown, threshold decay, ping, and classification timelines |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines | daemon | commander task board, corporation survey ledger, and shared semantic controls | Eve commander/pilot clients | point exploration, attack with optimum-range control, same-zone haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home wired through shared commands; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing; capability/priority assignment and rejection behavior proven; generic Electron commander lowering proves the worker roster | cross-zone logistics execution, cancellation, reconnect, and multi-client command transfer |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions and active effects | action bar/schematic | reference | consume, stacking, duration and cleanup |
|
||||
| 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, advances timers on daemon time with the fossil's exact-zero final tick, removes expired effects, and emits activation/refusal/expiry facts; Unity remains a projection | execute authored behavior payloads in fossil order, including resource failure, effectiveness curves, and modifier cleanup |
|
||||
|
||||
## Ownership Invariants
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue