Publish authoritative weapon reloads
This commit is contained in:
parent
c31aabe88e
commit
9d55ab67ff
4 changed files with 59 additions and 16 deletions
|
|
@ -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.###} " +
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue