Keep weapon locks authoritative while idle
This commit is contained in:
parent
31ee0e4347
commit
6c4819fa41
4 changed files with 190 additions and 19 deletions
|
|
@ -726,10 +726,16 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
||||
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
||||
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5)),
|
||||
new AetheriaRuntimeBehaviorField(23, PerformanceStat(360)),
|
||||
new AetheriaRuntimeBehaviorField(24, PerformanceStat(0))
|
||||
new AetheriaRuntimeBehaviorField(23, PerformanceStat(45)),
|
||||
new AetheriaRuntimeBehaviorField(24, PerformanceStat(0)),
|
||||
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
||||
]);
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([CatalogItem("test-instant", payload)], [], []);
|
||||
RequireNear(45,
|
||||
AetheriaRuntimeEquippedBehaviorQueries.Find(source, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)
|
||||
.Single().EvaluateStat(23),
|
||||
0.000001,
|
||||
"lock-transition fixture must retain its authored 45-degree acquisition cone");
|
||||
var fire = new AetheriaRuntimeDaemonIntentState();
|
||||
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
||||
{
|
||||
|
|
@ -747,6 +753,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Require(run.ShotReceipts.Count == 1 && run.ShotReceipts[0].SourceEntityIndex == source.EntityIndex,
|
||||
$"one instant weapon request must remain pending until daemon lock acquisition commits its shot; " +
|
||||
$"receipts={run.ShotReceipts.Count}, direction={source.DirectionX},{source.DirectionY}, contacts={string.Join(";", (source.Contacts ?? []).Select(value => $"{value.TargetEntityIndex}:{value.InfoGathered}"))}, states={string.Join(";", (source.WeaponStates ?? []).Select(value => $"{value.BehaviorKind}:pending={value.TriggerPending}:lock={value.LockProgress}:target={value.LockTargetEntityIndex}:burst={value.BurstRemaining}"))}");
|
||||
Require(run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
||||
value.SourceEntityIndex == 0 && value.TargetEntityIndex == 1) == 1 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
||||
value.SourceEntityIndex == 0 && value.TargetEntityIndex == 1) == 1,
|
||||
"daemon lock acquisition must publish one start and one completed transition instead of client-timed feedback");
|
||||
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.InstantWeapon);
|
||||
Require(!state.TriggerPending,
|
||||
"instant weapon request must clear after the committed burst begins");
|
||||
|
|
@ -799,6 +810,84 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
combat.Props["radialFillMinimum"] == "0.25" &&
|
||||
combat.Props["radialFillMaximum"] == "0.75",
|
||||
"Eve combat presentation must preserve the fossil's presentation timing and radial meter semantics");
|
||||
|
||||
var secondTarget = Entity(2, 100, "raider");
|
||||
zone.Entities = [source, target, secondTarget];
|
||||
source.TargetEntityIndex = secondTarget.EntityIndex;
|
||||
source.Contacts =
|
||||
[
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{ TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true },
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{ TargetEntityIndex = 2, InfoGathered = 1, Hostile = true, Visible = true }
|
||||
];
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, 13, 1.3);
|
||||
Require(state.LockTargetEntityIndex == 2 && state.LockProgress > 0 && state.LockProgress < 0.99 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
||||
value.TargetEntityIndex == 1 && value.Reason == "target-changed") == 1 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
||||
value.TargetEntityIndex == 2) == 1 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
||||
value.TargetEntityIndex == 2) == 0,
|
||||
$"idle authored lock behavior must tick every daemon step and transfer chronology exactly once on target change; " +
|
||||
$"target={state.LockTargetEntityIndex} progress={state.LockProgress:0.###} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
||||
|
||||
for (var frame = 14; frame <= 17; frame++)
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, frame, frame * 0.1);
|
||||
Require(state.LockProgress > 0.99 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
||||
value.TargetEntityIndex == 2) == 1 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
||||
value.TargetEntityIndex == 2) == 1,
|
||||
"idle authored lock behavior must continue progressive acquisition without duplicating transition events");
|
||||
|
||||
source.DirectionX = -1;
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, 18, 1.8);
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, 19, 1.9);
|
||||
Require(state.LockProgress < 0.99 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
||||
value.TargetEntityIndex == 2 && value.Reason == "angle") == 1,
|
||||
$"crossing the authored lock angle must publish one loss transition while continued decay remains silent; " +
|
||||
$"direction={source.DirectionX},{source.DirectionY} source={source.PositionX:0.###},{source.PositionZ:0.###} target={secondTarget.PositionX:0.###},{secondTarget.PositionZ:0.###} progress={state.LockProgress:0.###} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
||||
|
||||
source.DirectionX = 1;
|
||||
for (var frame = 20; frame <= 21; frame++)
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, frame, frame * 0.1);
|
||||
Require(state.LockProgress > 0.99 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
||||
value.TargetEntityIndex == 2) == 2,
|
||||
"restored facing must reacquire through a second completed transition without replaying lock start");
|
||||
|
||||
zone.Entities = [source, target];
|
||||
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
NewPhysics(), catalog, 22, 2.2);
|
||||
Require(state.LockTargetEntityIndex == -1 && state.LockProgress == 0 &&
|
||||
run.ShotReceipts.Count == 1 &&
|
||||
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
||||
value.TargetEntityIndex == 2 && value.Reason == "target-invalid") == 1,
|
||||
$"an invalid target must clear an idle weapon's authoritative lock and publish one loss transition; " +
|
||||
$"target={state.LockTargetEntityIndex} progress={state.LockProgress:0.###} shots={run.ShotReceipts.Count} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
||||
var resetSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
||||
new AetheriaRuntimeDaemonFrameDocument { FrameId = 22, Run = run },
|
||||
new AetheriaRuntimeDaemonHealthDocument(),
|
||||
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
||||
Require(Flatten(resetSurface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
||||
node.Props["eventKind"] == "weapon.lock.lost" &&
|
||||
node.Props["targetEntityIndex"] == "2" &&
|
||||
node.Props["reason"] == "target-invalid" &&
|
||||
double.Parse(node.Props["auxiliaryValue"], CultureInfo.InvariantCulture) > 0.99),
|
||||
"Eve feedback must project the daemon lock-loss transition and its prior completed progress");
|
||||
}
|
||||
|
||||
private static void DeployableWeaponRunsThroughYmirAndDetonatesOnDaemon()
|
||||
|
|
|
|||
|
|
@ -319,11 +319,16 @@ namespace GameCult.Aetheria.State.Verse
|
|||
settings, catalog, frameId);
|
||||
StepConstantWeapons(run, zone, byIndex, attacker, weaponGroup, deltaSeconds,
|
||||
settings, catalog, frameId);
|
||||
var weapons = ResolveWeapons(attacker, weaponGroup, catalog, settings);
|
||||
var weapons = ResolveWeapons(attacker, catalog, settings);
|
||||
var requestedEquipment = weaponGroup >= 0 && weaponGroup <
|
||||
(attacker.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>()).Count
|
||||
? attacker.WeaponGroups[weaponGroup] ?? Array.Empty<int>()
|
||||
: Array.Empty<int>();
|
||||
foreach (var weapon in weapons)
|
||||
{
|
||||
weapon.State.Firing = false;
|
||||
if (weaponGroup >= 0)
|
||||
if (weaponGroup >= 0 &&
|
||||
(catalog == null || requestedEquipment.Contains(weapon.State.OwnerIndex)))
|
||||
weapon.State.TriggerPending = true;
|
||||
if (weapon.State.Reloading)
|
||||
{
|
||||
|
|
@ -347,8 +352,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
foreach (var weapon in weapons)
|
||||
{
|
||||
weapon.State.LockTargetEntityIndex = -1;
|
||||
weapon.State.LockProgress = 0;
|
||||
ResetWeaponLock(run, zone, attacker, weapon, frameId, "target-invalid");
|
||||
weapon.State.TriggerPending = false;
|
||||
}
|
||||
continue;
|
||||
|
|
@ -356,7 +360,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
foreach (var weapon in weapons)
|
||||
{
|
||||
UpdateWeaponLock(attacker, target, weapon, deltaSeconds, settings);
|
||||
UpdateWeaponLock(run, zone, attacker, target, weapon, deltaSeconds, frameId);
|
||||
if (!weapon.State.TriggerPending && weapon.State.BurstRemaining <= 0)
|
||||
continue;
|
||||
if (DistanceSq(attacker, target) > weapon.Range * weapon.Range)
|
||||
{
|
||||
weapon.State.LastRefusalReason = "out-of-range";
|
||||
|
|
@ -991,19 +997,22 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
private static void UpdateWeaponLock(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit attacker,
|
||||
AetheriaRuntimeEntitySnapshotCommit target,
|
||||
ResolvedWeapon weapon,
|
||||
double deltaSeconds,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
long frameId)
|
||||
{
|
||||
var weaponState = weapon.State;
|
||||
if (weaponState.LockTargetEntityIndex != target.EntityIndex)
|
||||
{
|
||||
ResetWeaponLock(run, zone, attacker, weapon, frameId, "target-changed");
|
||||
weaponState.LockTargetEntityIndex = target.EntityIndex;
|
||||
weaponState.LockProgress = 0;
|
||||
}
|
||||
|
||||
var previousProgress = weaponState.LockProgress;
|
||||
var targetDirection = Normalize(target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ);
|
||||
var lookDirection = Normalize(attacker.DirectionX, attacker.DirectionY);
|
||||
var dot = Math.Max(-1.0, Math.Min(1.0,
|
||||
|
|
@ -1013,6 +1022,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
weaponState.LockProgress = Clamp01(
|
||||
weaponState.LockProgress - deltaSeconds * weapon.LockDecayPerSecond);
|
||||
PublishWeaponLockTransitions(run, zone, attacker, target.EntityIndex, weapon,
|
||||
previousProgress, weaponState.LockProgress, frameId, "angle");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1026,6 +1037,77 @@ namespace GameCult.Aetheria.State.Verse
|
|||
weapon.LockSpeed *
|
||||
Math.Pow(information, weapon.LockSensorImpact);
|
||||
weaponState.LockProgress = Clamp01(weaponState.LockProgress + acquisition);
|
||||
PublishWeaponLockTransitions(run, zone, attacker, target.EntityIndex, weapon,
|
||||
previousProgress, weaponState.LockProgress, frameId, "acquiring");
|
||||
}
|
||||
|
||||
private static void ResetWeaponLock(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit attacker,
|
||||
ResolvedWeapon weapon,
|
||||
long frameId,
|
||||
string reason)
|
||||
{
|
||||
var state = weapon.State;
|
||||
var previousProgress = state.LockProgress;
|
||||
var previousTarget = state.LockTargetEntityIndex;
|
||||
state.LockTargetEntityIndex = -1;
|
||||
state.LockProgress = 0;
|
||||
if (previousProgress > 0.99 && previousTarget >= 0)
|
||||
AppendWeaponLockEvent(run, zone, attacker, previousTarget, weapon, frameId,
|
||||
"weapon.lock.lost", reason, previousProgress, 0);
|
||||
}
|
||||
|
||||
private static void PublishWeaponLockTransitions(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit attacker,
|
||||
int targetEntityIndex,
|
||||
ResolvedWeapon weapon,
|
||||
double previousProgress,
|
||||
double currentProgress,
|
||||
long frameId,
|
||||
string reason)
|
||||
{
|
||||
if (previousProgress <= 0 && currentProgress > 0)
|
||||
AppendWeaponLockEvent(run, zone, attacker, targetEntityIndex, weapon, frameId,
|
||||
"weapon.lock.started", reason, previousProgress, currentProgress);
|
||||
if (previousProgress <= 0.99 && currentProgress > 0.99)
|
||||
AppendWeaponLockEvent(run, zone, attacker, targetEntityIndex, weapon, frameId,
|
||||
"weapon.lock.acquired", reason, previousProgress, currentProgress);
|
||||
else if (previousProgress > 0.99 && currentProgress <= 0.99)
|
||||
AppendWeaponLockEvent(run, zone, attacker, targetEntityIndex, weapon, frameId,
|
||||
"weapon.lock.lost", reason, previousProgress, currentProgress);
|
||||
}
|
||||
|
||||
private static void AppendWeaponLockEvent(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit attacker,
|
||||
int targetEntityIndex,
|
||||
ResolvedWeapon weapon,
|
||||
long frameId,
|
||||
string kind,
|
||||
string reason,
|
||||
double previousProgress,
|
||||
double currentProgress)
|
||||
{
|
||||
var state = weapon.State;
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{attacker.EntityIndex}:weapon:{state.OwnerIndex}:{state.BehaviorIndex}:target:{targetEntityIndex}:{kind}",
|
||||
Kind = kind,
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = attacker.EntityIndex,
|
||||
TargetEntityIndex = targetEntityIndex,
|
||||
ItemKey = weapon.ItemKey,
|
||||
SubjectKey = $"{state.OwnerKind}:{state.OwnerIndex}:{state.BehaviorIndex}",
|
||||
Reason = reason,
|
||||
ScalarValue = currentProgress,
|
||||
AuxiliaryValue = previousProgress
|
||||
});
|
||||
}
|
||||
|
||||
private static void PreparePhysicalPayloads(
|
||||
|
|
@ -1500,20 +1582,13 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static IReadOnlyList<ResolvedWeapon> ResolveWeapons(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
int weaponGroup,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
{
|
||||
var equipmentIndices = weaponGroup >= 0 && weaponGroup < (entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>()).Count
|
||||
? entity.WeaponGroups[weaponGroup] ?? Array.Empty<int>()
|
||||
: Array.Empty<int>();
|
||||
var authored = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)
|
||||
.Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon, StringComparison.Ordinal))
|
||||
.Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.DeployableWeapon, StringComparison.Ordinal))
|
||||
.Select(behavior => ResolveAuthoredWeapon(entity, behavior, settings))
|
||||
.Where(weapon => weaponGroup >= 0
|
||||
? equipmentIndices.Contains(weapon.State.OwnerIndex)
|
||||
: weapon.State.TriggerPending || weapon.State.BurstRemaining > 0)
|
||||
.ToArray();
|
||||
if (catalog != null)
|
||||
return authored;
|
||||
|
|
|
|||
|
|
@ -264,7 +264,14 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
Weapon behaviors now also publish typed runtime rows for firing state, ammo,
|
||||
burst timing, cooldown progress, charged-weapon charging/charge state,
|
||||
constant-weapon reload/ammo-interval state, and lock-weapon lock progress
|
||||
plus target entity reference.
|
||||
plus target entity reference. Authored instant lock behavior evaluates every
|
||||
fixed daemon step whether or not that weapon currently owns a trigger or
|
||||
burst; trigger selection owns permission to fire, not permission for lock
|
||||
state to remain alive. Target changes, invalid targets, acquisition, and
|
||||
angular decay commit through the same lock-state path and append stable
|
||||
`weapon.lock.started`, `weapon.lock.acquired`, and `weapon.lock.lost` events
|
||||
with prior/current progress. Eve projects those events generically while the
|
||||
continuous combat presentation remains the reticle's state owner.
|
||||
Sensor, radiator, reactor, and capacitor behaviors publish typed state rows
|
||||
for ping state, radiator temperature/throughput, reactor draw/load, and
|
||||
capacitor charge/capacity/efficiency. Aether drives publish typed axis,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role; generation fills the largest compatible footprint before applying economic weighting, so small gear remains a valid large-mount fallback without displacing available station-scale gear; cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; ordinary ships begin without generated cargo, and the deleted scenario-string path can no longer silently invent non-catalog item identities; station cargo slots and receipts preserve the same canonical keys; zone generation derives one continuing deterministic RNG stream per availability corporation, so cross-faction calls cannot perturb another faction; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil |
|
||||
| Flight intent | `Ship`, `Thruster`, `AetherDrive`, input paths | daemon intent + Ymir physics | analog movement/look/tractor | EveUnity input driver | movement intents feed Ymir-owned body integration; authored thrust allocation, torque, energy and heat remain simplified | prove original drive/equipment behavior ordering and runtime interpolation |
|
||||
| Collision | Unity physics and entity handlers | Ymir | collision/result facts | visual interpolation only | Ymir.Core world stepping wired for entity bodies, radial fields and contacts; missing-owner negative proof passes | publish authoritative collision events and prove Unity cannot write transforms back |
|
||||
| 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; the provider cockpit projects that same selected-target lock through a generic Eve progress component without client smoothing or reconstruction | target-switch reset, angular decay, visibility/nearest/explicit cases, lock transition events, and authored reticle animation |
|
||||
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; every authored instant lock behavior now evaluates on every daemon step independently of trigger/burst selection, so an idle weapon cannot retain a stale completed lock; acquisition and decay derive from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock; target changes and invalid targets clear the daemon state; `weapon.lock.started`, `weapon.lock.acquired`, and `weapon.lock.lost` publish stable one-time chronology with target, weapon identity, reason, and prior/current progress through the generic feedback stream; the provider cockpit and generic combat reticle consume the same continuous selected-target lock without client smoothing or reconstruction | angular loss fixture, visibility/nearest/explicit targeting cases, provider audio/effect roles, and authored reticle parity |
|
||||
| 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, capacitor/reactor/radiator network, and cockpit thermal exposure | daemon | ship/equipment power, temperature, medical state, risk and death feedback | schematic/post effects | owner-exists; wired; exclusive; projected; generic timing and native asset delivery lowered: catalog hull and installed-item shapes derive cell mass/conductivity/topology; local temperature derives thermal performance, wear potential and online state; all consumers share capacitor-first/reactor-residual transactions; powered radiators pump before reactor settlement; reactor heat then enters the same cell network before conduction/radiation; cockpit-cell temperature drives the fossil nonlinear heatstroke/hypothermia accumulation, linear recovery, severe-risk crossings, and ordinary exactly-once destruction with a typed cause; Eve exposes cockpit, risk, heatstroke source-weight, phasing and death-transition facts; EveUnity generically resolves the original severe pulse plus live/reconnect-safe cause-specific death crossfade; Aetheria deterministically translates the five legacy profiles into URP profiles, persists every override as a real profile sub-asset, advertises them by semantic role through CultMesh CDN, and binds them in the canonical client through a weight-only native sink; rotated equipment topology, parent/child radiation, schematic composition, legacy SSR/AO/roundness equivalents, and live/reconnect death-fade parity remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, generic schematic meters, renderer-feature parity, and live/reconnect death-fade screenshots |
|
||||
|
|
@ -37,7 +37,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Celestial world | `ZoneRenderer`, body/asteroid prefab scripts | daemon render projection plus provider assets | ordinary generic Eve entity SoA rows | EveUnity generic entity/prefab lowerer | wired, exclusive, projected, and lowered: daemon orbit and terrain queries emit stable presentation-only identities for suns, planets, gas giants, and live asteroid instances; provider packaging extracts only each advertised visual child and strips fossil scripts/physics; released EveUnity `0.3.51` applies semantic scale around provider-authored local scale and contains no celestial or Aetheria branch. The released warm witness records eleven bodies and eighteen asteroids with provider refs and enabled renderers, including seven celestial instances intersecting the pilot frustum | body material/light readability, close-range composition matching the fossil, asteroid variation, and Electron lowering |
|
||||
| Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | daemon publishes canonical gravity/fog splat layers through `field.volume3d`; Eve Fields `0.2.3` owns portable `PowerPulse` and `AnimatedRadialCosine` semantics; Aetheria emits the fossil global animated simplex/cellular/ambient stack, body-owned radial waves, and separate negative gameplay `gravity.height` and positive visual `fog.surface_height`; the shared camera-relative lattice spans 1536 world units as 256 six-unit Stardust cells, rasterizes gravity at eight texels per cell into 2048 texels, and snaps the viewport by one six-unit cell; daemon entities follow fossil terrain height plus hull grid offset after Ymir XZ motion; released EveUnity `0.3.52` lowers the authored 2048/256/512 layer topology, provider shader ABI, pre-postprocess URP pass, authoritative flow time, explicit camera matrix, and float32 packed-output diagnostics without provider code. The provider bundle runs the actual Stardust compute shader across X, Y, and diagonal one-cell remaps and proves 705 overlapping world cells remain bit-identical after buffer reassignment. The released witness proves the same snapped grid drives 65,536 rendered particles while the map camera remains isolated. The flight post-process bundle gate now loads and verifies serialized ACES, exposure/contrast, bloom, vignette, and film-grain components rather than accepting an empty profile | restore readable patch structure, gravity waves, surrounding world geometry, and final luminance/composition parity; visual parity remains explicitly open |
|
||||
| HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | provider-authored transparent cockpit is wired, projected, and lowered through generic UI Toolkit panes, metrics, and progress components for hull, shield, capacitor, temperature, cargo, weapon cooldown/groups, selected target, target lock, target shield, and target hull; the original armor/equipment schematic and transient warning choreography remain reference-only | schematic topology, warning timelines, dynamic action state, and composited live capture |
|
||||
| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, authoritative weapon reload chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; refusal identity, authored weapon, target, reason, and remaining magazine state are runtime-independent cockpit inputs | add lock transitions, thermal and docking event families, negative transaction fixtures, and runtime effect consumption |
|
||||
| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, authoritative weapon reload chronology, lock start/acquired/lost chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; every lock transition carries authored weapon identity, target, reason, and prior/current progress, while reconnect priming prevents retained events from replaying as new feedback | add thermal and docking event families, negative transaction fixtures, provider lock audio/effect roles, and broader runtime effect consumption |
|
||||
| Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | provider manifests name deterministic bundle-internal paths; the authoring build derives presentation-only prefabs, strips missing and non-Eve `MonoBehaviour` components, externalizes embedded tractor and shield event visuals, preserves particle/unlit/lit source semantics during URP conversion, verifies saved prefabs and the loaded bundle including native program/profile ABI, and publishes the immutable roughly 55 MB bundle through CultMesh; the released cold profile starts empty, receives and verifies the body through `cultmesh.content.v1`, atomically promotes the exact hash with no partial, and lowers rendered provider identity; the warm profile reuses the committed body. Negotiated mapped/zero-copy delivery remains unproven | negotiated mapped/network body selection and same-machine zero-copy proof |
|
||||
| Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | owner-exists; not parity-proven | mid-loop restart |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | 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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue