Fire control Cut 6b: a shot's dice belong to the shot, and airburst resolves in the simulation
6.1 (Soul finding 6): Commit builds a local CultMath.Random from (Zone.CombatSeed, shot.ShotId) instead of reading and writing the shared ItemManager.Random stream. Zone exposes CombatSeed (the same expression that already seeds its own _random), so a shot's outcome is a pure function of zone identity and shot id -- immune to whatever else drew from the shared stream in between. 6.2 (Soul finding 5): airburst is wired into the simulation instead of back into Projectile.cs. PendingShot gains BurstPosition/BurstRadius, frozen at Fire time from PredictedIntercept and the weapon's authored WeaponItemData.AirburstRange when WeaponModifiers carries Airburst. FireControl.Step resolves such a shot via Splash instead of Apply, never both. Projectile.cs/ProjectileManager.cs drop the dead AirburstDistance/AirburstRange fields and write-back. Tests: FireControlCut6Tests.cs pins both rules (208 baseline + 5 new, all green). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
063804801f
commit
39ad4bfc80
6 changed files with 358 additions and 12 deletions
|
|
@ -195,6 +195,15 @@ public static class FireControl
|
|||
}
|
||||
|
||||
var commitHorizon = source.ItemManager.GameplaySettings.CommitHorizon;
|
||||
|
||||
// Cut 6b, 6.2 (Soul finding 5): airburst is a property of the weapon, resolved and frozen here like
|
||||
// every other payload field -- never decided later by a Unity projectile. A weapon without the
|
||||
// Airburst flag freezes a zero radius, which Step below reads as "resolve this shot with Apply."
|
||||
var weaponItemData = item.Data as WeaponItemData;
|
||||
var isAirburst = weaponItemData != null && weaponItemData.WeaponModifiers.HasFlag(WeaponModifiers.Airburst);
|
||||
var burstPosition = isAirburst && target != null ? PredictedIntercept(weapon, source, target) : targetPosition;
|
||||
var burstRadius = isAirburst ? weaponItemData.AirburstRange : 0f;
|
||||
|
||||
var shot = new PendingShot
|
||||
{
|
||||
ShotId = zone.NextShotId(),
|
||||
|
|
@ -214,6 +223,8 @@ public static class FireControl
|
|||
FireTargetVelocity = targetVelocity,
|
||||
ArrivalTime = now + flightTime,
|
||||
CommitTime = now + max(0f, flightTime - commitHorizon),
|
||||
BurstPosition = burstPosition,
|
||||
BurstRadius = burstRadius,
|
||||
Committed = false
|
||||
};
|
||||
|
||||
|
|
@ -258,7 +269,11 @@ public static class FireControl
|
|||
|
||||
if (shot.Committed && now >= shot.ArrivalTime)
|
||||
{
|
||||
Apply(shot);
|
||||
// Cut 6b, 6.2: an airburst shot (frozen BurstRadius > 0) resolves as an area effect instead of
|
||||
// a discrete hit -- Splash instead of Apply, never both, which is exactly the double-
|
||||
// application Soul was told to hunt for.
|
||||
if (shot.BurstRadius > 0f) Splash(zone, shot.BurstPosition, shot.BurstRadius, shot.Damage, shot.DamageType);
|
||||
else Apply(shot);
|
||||
zone.ShotResolved.OnNext(shot.Outcome);
|
||||
shots.RemoveAt(i);
|
||||
}
|
||||
|
|
@ -272,7 +287,13 @@ public static class FireControl
|
|||
// sequence is exactly as if the shot had never queued.
|
||||
private static ShotOutcome Commit(Zone zone, PendingShot shot, float now)
|
||||
{
|
||||
var random = shot.Source.ItemManager.Random;
|
||||
// Cut 6b, 6.1 (Soul finding 6): a shot's dice belong to the shot, not to whatever else happened to
|
||||
// draw from ItemManager.Random first. A pure function of (zone identity, shot id) -- the `| 1u` guards
|
||||
// the degenerate zero seed -- so two runs of the same fight from the same galaxy seed roll identically
|
||||
// no matter what the UI drew from the shared stream in between. This generator is local and dies with
|
||||
// the call: nothing outside Commit may seed or advance a combat draw, and Commit may not read or write
|
||||
// a shared stream.
|
||||
var random = new Random((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u);
|
||||
var p = shot.PBase;
|
||||
if (p > 0f && shot.Target != null)
|
||||
{
|
||||
|
|
@ -312,7 +333,6 @@ public static class FireControl
|
|||
else if (shieldActive) shieldBroken = true;
|
||||
}
|
||||
|
||||
shot.Source.ItemManager.Random = random;
|
||||
return MakeOutcome(shot, hit, shielded, shieldBroken, cell, aimed, now);
|
||||
}
|
||||
|
||||
|
|
@ -436,6 +456,12 @@ public struct PendingShot
|
|||
public float Tracking;
|
||||
public float Precision;
|
||||
|
||||
// Cut 6b, 6.2: the airburst payload, frozen at fire alongside everything else above. BurstRadius is zero
|
||||
// for a weapon without the Airburst flag (WeaponModifiers, Enums.cs) -- Step reads that zero as "resolve
|
||||
// with Apply," not a separate bool.
|
||||
public float3 BurstPosition;
|
||||
public float BurstRadius;
|
||||
|
||||
public float3 FireTargetPosition;
|
||||
public float3 FireTargetVelocity;
|
||||
public float FireTime;
|
||||
|
|
|
|||
|
|
@ -489,6 +489,13 @@ public class WeaponItemData : GearData
|
|||
|
||||
[Inspectable, JsonProperty("modifiers"), Key(28)]
|
||||
public WeaponModifiers WeaponModifiers;
|
||||
|
||||
// Cut 6b (docs/fire-control-cut.md, 6.2): the authored blast radius an Airburst-flagged weapon's shot
|
||||
// splashes with at arrival, frozen into PendingShot.BurstRadius at fire. Zero (and unread) for a weapon
|
||||
// without the Airburst flag. No authored value exists yet -- the flag is wired and unexercised until Cut
|
||||
// 6a or a later authoring pass sets it.
|
||||
[Inspectable, JsonProperty("airburstRange"), Key(29)]
|
||||
public float AirburstRange;
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.hulldata", "1"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public class Zone
|
|||
public GalaxyZone GalaxyZone { get; }
|
||||
public Galaxy Galaxy { get; }
|
||||
|
||||
// Cut 6b (docs/fire-control-cut.md, 6.1, Soul finding 6): one zone, one stable identity -- a galaxy seed
|
||||
// reproduces it, and nothing else in the zone (an NPC joining, a shop opening, a wormhole exit) can
|
||||
// perturb it. FireControl.Commit builds its own local generator from this and the shot's own id instead
|
||||
// of touching a shared stream.
|
||||
public uint CombatSeed { get; }
|
||||
|
||||
public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy)
|
||||
{
|
||||
_time = pack.Time;
|
||||
|
|
@ -61,7 +67,8 @@ public class Zone
|
|||
Pack = pack;
|
||||
_itemManager = itemManager;
|
||||
Settings = settings;
|
||||
_random = new Random(galaxyZone?.Name.StableHash() ?? 1337u);
|
||||
CombatSeed = galaxyZone?.Name.StableHash() ?? 1337u;
|
||||
_random = new Random(CombatSeed);
|
||||
var cache = itemManager.ItemData;
|
||||
|
||||
// Cut 5, 5.6 (docs/fire-control-cut.md, Soul finding 7; operator ruling Q3): death removes the ship,
|
||||
|
|
|
|||
Loading…
Reference in a new issue