Publish authoritative weapon reloads

This commit is contained in:
Meta Vens 2026-07-11 20:50:09 +01:00
commit 9d55ab67ff
4 changed files with 59 additions and 16 deletions

View file

@ -625,7 +625,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
new AetheriaRuntimeBehaviorField(12, new AetheriaRuntimeBehaviorValue(
"item-key", "", 0, false, "", "test-ammo",
Array.Empty<AetheriaRuntimeBehaviorValue>(), Array.Empty<AetheriaRuntimeBehaviorMapEntry>())),
new AetheriaRuntimeBehaviorField(13, Number(6)),
new AetheriaRuntimeBehaviorField(13, Number(3)),
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
new AetheriaRuntimeBehaviorField(16, PerformanceStat(330)),
new AetheriaRuntimeBehaviorField(17, PerformanceStat(3)),
@ -643,7 +643,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
Array.Empty<AetheriaRuntimeNameFile>());
var target = Entity(1, 105, "raider");
target.Kind = "station";
target.StatGrids = [Grid("hull", 7), Grid("shield", 0), Grid("heat", 0)];
target.StatGrids = [Grid("hull", 14), Grid("shield", 0), Grid("heat", 0)];
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "agent-attack-smoke",
@ -714,14 +714,22 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"weapon progress must belong to the equipped authored behavior instead of a synthetic entity weapon");
var authoredWeapon = agent.WeaponStates.Single(value =>
value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind);
RequireEqual(3, authoredWeapon.Ammo,
"a three-round authored burst must decrement one magazine round per committed projectile");
RequireNear(45, agent.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
0.000001, "a committed authored weapon round must drain its energy from canonical capacitor state");
RequireEqual(2, CargoQuantity(agent, "test-ammo"),
"cargo ammunition must pay for reload, not each magazine round");
Require(run.GameEvents.Count(value => value.Kind == "projectile.launched" && value.ItemKey == "test-lock-cannon") >= 3,
"authored burst count and interval must emit every due round through the projectile owner");
RequireEqual(0, authoredWeapon.Ammo,
"two three-round authored bursts must consume both complete magazines");
RequireNear(40, agent.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
0.000001, "two authored bursts must drain exactly their combined energy from canonical capacitor state");
RequireEqual(1, CargoQuantity(agent, "test-ammo"),
"one empty magazine must consume exactly one reserve cargo commodity before reload");
Require(run.GameEvents.Count(value => value.Kind == "projectile.launched" && value.ItemKey == "test-lock-cannon") >= 6,
"two authored bursts must emit all six due rounds through the projectile owner");
Require(run.GameEvents.Count(value => value.Kind == "weapon.reload.started") == 1 &&
run.GameEvents.Count(value => value.Kind == "weapon.reload.completed") == 1,
"empty magazine transition must publish one authoritative reload start and completion pair");
var reloadStarted = run.GameEvents.Single(value => value.Kind == "weapon.reload.started").FrameId;
var reloadCompleted = run.GameEvents.Single(value => value.Kind == "weapon.reload.completed").FrameId;
Require(reloadCompleted > reloadStarted && !run.GameEvents.Any(value =>
value.Kind == "projectile.launched" && value.FrameId > reloadStarted && value.FrameId < reloadCompleted),
"reload interval must be a projectile-free authoritative timeline, not a cosmetic client delay");
Require(!target.IsActive,
$"attack task must end through daemon damage after Ymir projectile contacts; hull={Stat(target, "hull"):0.###} " +
$"agent={agent.PositionX:0.###},{agent.PositionZ:0.###} target={target.PositionX:0.###},{target.PositionZ:0.###} " +

View file

@ -16602,6 +16602,8 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
"weapon.State.BurstRemaining = weapon.BurstCount",
"weapon.State.BurstTimer += deltaSeconds",
"weapon.SingleAmmoBurst",
"weapon.reload.started",
"weapon.reload.completed",
"AetheriaRuntimeCargoTransactions.TryRemoveQuantity(",
"CommitEnergy(entity, weapon.Energy)",
"SpawnProjectile(zone, attacker, target, weapon, settings)",

View file

@ -251,6 +251,7 @@ namespace GameCult.Aetheria.State.Verse
{
weapon.State.Reloading = false;
weapon.State.Ammo = weapon.MagazineSize;
AppendWeaponEvent(run, zone, attacker, weapon, frameId, "weapon.reload.completed");
}
}
weapon.State.CooldownProgress = Math.Max(0, weapon.State.CooldownProgress - deltaSeconds);
@ -286,8 +287,13 @@ namespace GameCult.Aetheria.State.Verse
{
if (weapon.State.CooldownProgress > 0)
continue;
if (weapon.SingleAmmoBurst && CommitWeaponRound(attacker, weapon) != WeaponRoundResult.Fired)
continue;
if (weapon.SingleAmmoBurst)
{
var triggerResult = CommitWeaponRound(attacker, weapon);
if (triggerResult == WeaponRoundResult.ReloadStarted)
AppendWeaponEvent(run, zone, attacker, weapon, frameId, "weapon.reload.started");
if (triggerResult != WeaponRoundResult.Fired) continue;
}
weapon.State.BurstRemaining = weapon.BurstCount;
weapon.State.BurstInterval = weapon.BurstTime / weapon.BurstCount;
weapon.State.BurstTimer = 0;
@ -298,10 +304,16 @@ namespace GameCult.Aetheria.State.Verse
weapon.State.BurstTimer += deltaSeconds;
while (weapon.State.BurstRemaining > 0 && weapon.State.BurstTimer > 0)
{
if (!weapon.SingleAmmoBurst && CommitWeaponRound(attacker, weapon) != WeaponRoundResult.Fired)
if (!weapon.SingleAmmoBurst)
{
weapon.State.BurstRemaining = 0;
break;
var roundResult = CommitWeaponRound(attacker, weapon);
if (roundResult == WeaponRoundResult.ReloadStarted)
AppendWeaponEvent(run, zone, attacker, weapon, frameId, "weapon.reload.started");
if (roundResult != WeaponRoundResult.Fired)
{
weapon.State.BurstRemaining = 0;
break;
}
}
weapon.State.BurstRemaining--;
weapon.State.BurstTimer -= weapon.State.BurstInterval;
@ -346,6 +358,27 @@ namespace GameCult.Aetheria.State.Verse
}
}
private static void AppendWeaponEvent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit entity,
ResolvedWeapon weapon,
long frameId,
string kind)
{
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
{
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:weapon:{weapon.State.OwnerIndex}:{weapon.State.BehaviorIndex}:{kind}",
Kind = kind,
FrameId = frameId,
ZoneIndex = zone.ZoneIndex,
SourceEntityIndex = entity.EntityIndex,
SubjectKey = weapon.ItemKey,
ItemKey = weapon.ItemKey,
ScalarValue = weapon.State.Ammo
});
}
private static void UpdateWeaponLock(
AetheriaRuntimeEntitySnapshotCommit attacker,
AetheriaRuntimeEntitySnapshotCommit target,

View file

@ -36,7 +36,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
| 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 |
| Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | owner-exists; basic lowering; composition parity absent | visual parity captures |
| HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | reference | source-fact and timing matrix |
| 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 plus projectile launch/impact, entity damage and alive-to-dead destruction chronology | add lock transitions, thermal and docking event families with 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, and authoritative weapon reload start/completion chronology; reload interval is proven projectile-free | add lock transitions, resource refusals, thermal and docking event families with runtime effect consumption |
| Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | wired; basic lowering; not live-proven remotely | cold/warm remote-only live load and cache 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 | incomplete; no scenario proof | fly/fire/loot/dock/trade |