Restores the Identified item-end pass and its tests (AnIdentifiedObserverSeesGearDestroyed, the flashing-gun tick case,
the Identified part of SenseAllocatesNothingOnceWarm) that 49ace47f held for this batch, and extends
AnActIsRevealedOnItsOwnTickWhateverTheListOrder to a moving subject (two velocities, both observer orders).
Builds: all commits build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RecordReveal gates on zone membership and EndReveals is the only way out; the observer's Zone.Entities handler no longer wipes the book and PublishInbox no longer decides membership. Zone records each removal as a one-tick Departure that each observer's Sense judges under the light-act gate; a departed subject's entries are pruned once older than BearingMemory. Unverified build at this commit (first test run follows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RevealsOf returns the book and nothing the subject's gear or Durability says (ruling contact-reads-book-aim-reads-live); FireControl.IsRevealed stays the live aiming read.
- PublishInbox records a paint's reveal only for an emitter still in the zone.
- Sense moves from Entity.Update to a Zone.Update phase after all entities have updated, so reveals do not depend on list order.
- ChunkInfo calls PassiveInfo(position, brightness) in the original multiplication order (PassiveInfoPerSignature becomes PassiveInfo with the signature argument).
- Tests: unseen destruction, ghost paint, Search paint reveal, no self-sense, quiet tracked ship levels, list-order independence.
Not built or run locally; verified on Yggdrasil after the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The observer's Entity owns a reveal book keyed (subject, item). Sense records a
light act when its strength times PassiveInfoPerSignature clears the detection
threshold; PublishInbox records an emitter whose paint arrived. ReceivePaint takes
the emitter and its item. ContactOf derives Detected, Tracked, Classified and
Identified from track, reveals, the zone clock and GameplaySettings.BearingMemory.
ChunkInfo's arithmetic moves into PassiveInfoPerSignature; ActiveSensors fills one
buffer instead of allocating per call.
Not yet built or tested: verify follows with the tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes the stateless interval crossing (_previousRadius, PreviousRadius, Crossing) and the allied-pinger scan over
other ships' Sensors. The pinging Sensor remembers the subjects its wavefront has reached (_reached), in the beam
or not, and a subject's first sweeping tick inside the radius is its one crossing. On it the pinger paints, credits
itself and posts a PingReturn to each ally; Entity publishes returns with paints (PublishPaints is now
PublishInbox) and the ally's own sensors credit them for one tick at the geometric mean range, inside
Rt x Rr < PingRange^2, through the geometric mean of both legs' obscuration (ruling ally-echo-bistatic-oval).
This commit builds and replaces the sensor the previous commit's tests were written against.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LockWarning becomes PaintWarning with a PaintMode (Search for a ping sweep, Track for a lock);
Entity.IncomingLocks becomes IncomingPaints. Sensor replaces the _pingedEntities set with a stateless
crossing rule over the previous and current wavefront radius, limited to a new BeamWidth stat (key 12,
absent reads 360), and credits allied pings from the receiving sensor's own sensitivity and distance.
The HUD lock indicator follows Track paints only. Folds in the radiator sampling-instant pin.
Unverified at this commit (first build still to run).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dictionary is private; the public member is an IReadOnlyDictionary over it, so a write
around SetSignature is a compile error, not a grep. Readers (Thruster, tests) use only the
read members. Builds; the test project at this commit still compiles (no reader writes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VisibilitySources holds Signature (channel, strength, item); the eight direct writes
go through the one writer; Visibility stays the sum; SignatureOn(channel) splits it.
VisibilityData gains Key(3) Channel. No number changes. Not yet built or tested at this
commit (verification follows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Zone.Update publishes every entity's paints before any entity updates (Entity.PublishPaints), so a broken lock clears on the same tick whatever the entity order.
- ActionGameManager.ApplyLockProgress is the one lock-indicator curve, called by the outgoing and the incoming indicator.
- LockWarningTests drive the real LockWeapon through Zone.Update; pins on strength across 0.15..1.0, the direction and sensor progress terms, and both entity orders.
Not yet run: this commit's tests have not been built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock stops being an intent rewrite in Ship.Update: ThrustAllocator.Allocate takes forwardFloor, which ignores the
stick, asks full forward and holds every column with y > 0 at full (ruling overdrive-forward-floor). Ship allocates in
Entity's new UpdateThrottles phase, after every item's UpdatePerformance and before the power bus and Execute, so
liveness and thrust are the ones Execute acts on (finding liveness-read-before-performance).
Tests follow in the next commits; the test project still compiles, new behaviour is untested here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LockWeapon.Execute paints its target with its own Lock (the one value the outgoing indicator also reads);
Entity publishes the one-tick inbox as IncomingLocks; the HUD shows one incoming indicator on the strongest
warning with the outgoing indicator's curves. Unity HUD code not compiled here; tests not yet run at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TurnTime has no caller; IAnalogBehavior's only implementer is Thruster. Pure
deletes, no behaviour change. Not compiled in Unity (the operator's); the
Shared tests build at the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expiry fires when the remaining duration reaches zero (<= 0), so behaviours execute on
Duration/dt ticks and not one more. AConsumableRunsForExactlyItsDurationInTicks pins it
with 0.25 s ticks. Builds; verified in the next run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.Activate re-initialises the running consumable effects through the same
InitializeBehaviors that initialises gear and ActivateConsumable, so a consumable
active across deactivate/refit/activate retargets to the new items.
ConsumableTests gains the stackable overlap/multiply, shared-entry detach,
expiry-boundary, per-design cooldown and refit facts. StatResolverTests' expiry
test builds its consumable through ItemManager.CreateInstance.
Not yet built or run; verification follows in a later commit message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ActivateConsumable runs Initialize on the effect's behaviours; expiry disposes
them before Resolver.Forget, so a consumable's modifiers exist exactly while
its effect is active. ItemManager.CreateInstance returns a ConsumableItem for a
consumable design. StatResolver.DetachModifier drops an entry left with
nothing attached, so ModifierEntryCount returns to baseline. LoadoutTests'
hand-built ConsumableItem goes through CreateInstance. Builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThrottleLockData (union 41) marks the lock; Entity.ThrottleLocked is derived from the
active effects; Ship.Update reads one `move` in place of MovementDirection. Turn is
untouched (ruling overdrive-keep-steering). Not yet verified by a build when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetherDrive.cs: master deleted the behaviour (2cfdc728); the branch only added a
TurnRate line to it. Deletion kept. Thruster remains the one rotation actuator
and feeds Entity.TurnRate. No sim stepping outside the clock on master
(mines presenter, Pirates AI, lot-product all step through Zone.Update).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lands retire-longinusx on master after gear-without-presence. The catalog
is regenerated from master's catalog with retire-longinusx apply (next commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# GameData/Aetheria.cc
SimClock (engine-free) turns real frame seconds into whole fixed steps; ActionGameManager.Update feeds it outside the pause gate and Zone.Update(Time.deltaTime) is gone, with IntroCutscene (dead, Unity-driven pose writes). Entity.TurnRate is the sum of the rotation actuators' applied turn per sim second, zeroed each Update; DrawAhead reads it so EntityInstance and ShipInstance draw the latest sim state ahead by Lead. Unverified at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetherDb mine-launcher now corrects an already authored launcher to 173.15..473.15 K (optimum 249.65, plateau 36,
the sibling weapon's) and authors new ones the same way. Entity.StartingCellTemperature names the 280 the hull
cells start at, and ShippedWeaponOperabilityTests reads it against the shipped catalog. The catalog blob follows
in the next commit, so the test is red at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetherDriveData and AetherDrive, Union(12) (now a retired-do-not-reuse comment, as 26 is),
HardpointType.AetherDrive (last member, nothing shifts), Ship's drive sets, aggregates and
Axis write, the five request-field rows, GameplaySettings Aether multipliers, ShipInstance's
drive particles, SchematicDisplay's RPM display and AetherDriveUi, the undock refusal's drive
clause, the drive prefab and material, and AetherDb's drive roles and brownout row. The
design is kept at parked/aether-drive.
Adds EveryShippedShipHullHasATopSpeed. ARPG.unity still holds stale serialized references to
the removed SchematicDisplay fields and the RPM Display object; the next commit removes them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Soul gate closed at verdict cut-evasion-term.s5 (head 473a4bcc); remaining Low
test gaps are deferred to a follow-up. Merged under operator ruling
aetheria-release:ruling:completed-cuts-merge. Carries the migrated
GameData/Aetheria.cc (LFS sha256 63250cf2...7ae25); master did not change the
catalog.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ship.Update reads Turn and feeds the rotation thrusters and aether drives; it no longer reads the
look direction or snaps Direction toward it. Steering.Toward is the one law from heading to demand
(a heading astern now turns at full demand). HelmInput owns the face-the-aim state. Accelerate,
Combat and MoveTo write Turn through Steering.Toward. Entity.LookDirection is renamed Aim in the
simulation, the Unity readers and the tests. Unity player input follows in the next commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
EntityPack.Boss (key 21) carries Entity.IsBoss through save and Continue. GenerateZone adds one boss
per BossZones entry before the ambient return, flying the faction's BossHull or, with the gap logged,
its most expensive available ship hull. RunGoal (ServerShared) is the one owner of boss life and exit
state, derived from Zone.Entities.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every hit price gains one factor, PDeviation(Evasion, Tracking): how far the target can
and does move off the line of fire within SolutionWindow (0.75 s). Capability is Reach, an
eight-face closed form over the ship's ManoeuvreEnvelope (summed from what each live
thruster and aether drive reports with its own Execute arithmetic, read live: a thruster's
Thrust property is stale until it fires, which is why the Djinni moved 1.6 times its stats,
M25). Behaviour is ManoeuvreTrack, observed once per Entity.Update from the measured
acceleration; a steady burn, coasting and a dither much faster than the window do not
evade, and sideways speed alone does not (ruling evasion-speed). The commit price adds the
term at any flight time, so an instant weapon feels a jinking target. Forecast owns the
term for HitProbability and Inspect, which gain Evasion and PEvasion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mining owns belts, chunks and target picking; scenarios owns admission and run
staging; master owns ships and fire control (rule scenarios-cut.md S4).
- AetheriaStores.CatalogTypes: nine-type union, master's list plus FieldKindData, no Scenario.
- tools/AetherDb/Program.cs: both cases (ship-authoring, field-kinds), one usage string.
- Zone.cs: master's MineAsteroid dropped (mining deleted its inputs); scenarios' SettleBelts and its three calls deleted.
- RunStartTests: the two belt-task tests deleted (they read belt.Transforms and MineAsteroid).
- FireControlCut124Tests: master's strict Exact assertions; mining's CompatibleDrift allowance and its comment dropped.
- GameData/Aetheria.cc: master's pointer 70a7b0a9. This commit lacks the Asteroid field kind in the catalog, so
TheShippedCatalogHasAPickableFieldKind fails here by design; the next commit rebuilds it.
Builds: this commit builds (verified at the end with commit 2); the catalog pin test fails at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four handlers in ActionGameManager call Entity.TargetUnderReticle, TargetNext,
TargetPrevious and TargetNearestEnemy. TargetCandidates, which built, sorted and
searched every visible ship and rock per press, is gone. Previous with no target
takes the farthest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone.Targets (TargetingIndex) answers Within and best-first Best over
ITargetProvider regions. EntityTargets offers the observer's VisibleEntities;
BeltTargets keys each belt's rocks into kinetic bands and bounds each segment's
annular sector for distance, bearing and visibility. Entity.VisibleChunksInReach
queries the index with DetectionReachPerVisibility, an upper bound from the
sensors' hull. Zone.ChunksNear and AsteroidBelt.InnerRadius are deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The placement rule (HardpointAt) already finds the one hardpoint an item lies
in at equip; TryEquip threw that answer away and eighteen readers recomputed it
from the item's origin cell, which need not lie in the hardpoint (Soul's
scenarios batch-4 P30). EquippedItem.Hardpoint now carries the answer, null for
Tool gear and the hull, and every origin-cell lookup reads it instead:
FireControl.ArcFor, the eleven weapon managers, EntityInstance, ShipInstance
and ActionGameManager's articulation grouping. Entity.Hardpoints[,] stays the
per-cell map for armour and display.
TryFindSpace's hardpoint search tries every origin the fit rule can accept,
including ones up and left of the hardpoint, so a design whose shape lacks its
origin cell is found wherever ItemFits would place it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Weapon.CanMine is the one mining predicate; LockWeapon overrides it to
false. Entity.VisibleChunksInReach counts only weapons that can mine.
Tests: LaunchersDoNotCountTowardChunkReach (a launcher-only ship is offered
no rock; a gun of 200 beside a launcher of 400 is offered the near rock
only) and AnOfflineWeaponAddsNoReach (Soul F8: reach reads Active, not
Enabled).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul's S3 and the S2 ruling ("B is the design intent, saving slots in a
hardpoint can be a valid tradeoff for crowded ships if you really need to fit
an extra tool"). Entity.HardpointAt is the one placement rule: at an origin,
the item lies within a hardpoint of its type at that hardpoint's rotation
(HardpointData.TakesAt), its own cells are free, and that hardpoint holds no
other hardpoint item. The origin no longer has to be a hardpoint cell, and
TryFindSpace tries every offset inside a hardpoint, the offsets the fit rule
tries, so an L-shaped hardpoint takes its L design instead of generation
throwing. Leftover hardpoint cells may hold general gear, and a hardpoint still
holds one hardpoint item.
Tests: an L hardpoint takes its L design by hand and in generation (Soul's
P11); a turret's gun goes back beside the tool gear in its leftover cells
(P21); a second gun in a roomy hardpoint is refused; a saved entity reloads to
the same loadout with its items in reverse order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling, 2026-09-30: "Hardpoint fit is loose: nobody's gonna stop you
from putting a small reactor in a large reactor's hardpoint." There is one rule,
HardpointData.Takes: gear of the hardpoint's type fits when its shape, at the
hardpoint's rotation, lies within the hardpoint's cells. TakesAt is the same
rule at one offset, and Entity.ItemFits (equipping, save load, presets) now
asks it instead of repeating the containment test. Generation's hardpoint
picks, HasHome (so station stock and loadouts), product reuse and `AetherDb
hardpoint-fit` all read Takes.
Generation keeps its own placement preference, named IsFilledBy and not a fit
rule: it offers gear that fills the hardpoint when any is on offer and falls
back to anything that fits only when none is, so loadouts change only where a
hardpoint used to stay empty. RestoredHullsTests pick fillers through
IsFilledBy.
Consequence: the Autocannon, Vulcan, scorched void policy, Notorious, pretty
pretty bang bang, Manhattan, plight, DeathCluster, Small Drive and pswarm now
have a home and are offered. Still homeless: CShot RainbowLite Lazer,
ChargeBlast SG, ChargeBlast+-, OK Disperser.
Tests: generation offers exactly the sold hardpoint gear some hull fits; a 7x7
design no hull fits is offered only once a hull that takes it exists; a 5x5
Reactor hardpoint nothing fills still gets a reactor; equipping and the rule
agree for every hull, hardpoint and design; the homeless list is stated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every entity's zone-removal subscription dropped its IFF override for whatever
left the zone, and TryDock removes the docking ship, so a scenario stance was
lost one way (the other entity forgot; the docked ship, deactivated, kept its
own) as soon as the player docked. The removal event already tells the two
apart: TryDock parents the ship to its dock before removing it, and every
leaving-for-good path (death, warp, unload) leaves with no parent. The override
is now dropped only for an entity that leaves unparented.
Tests: a hostile stance holds both ways, including the live hostility each side
tracks, across dock and undock; a destroyed entity takes the player's stance
toward it with it. UndockingAdmitsWithNoAgent commits Soul's probe: undock
admits unpiloted, active, with no agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.Update walks _orderedEquipment, which only TryEquip and TryUnequip
set, so an entity nothing was ever equipped on threw a
NullReferenceException on its first tick. Scenario presets stage bare hulls
(a turret, an unpiloted target), and RunStartTests found it: MapEntity now
sets the ordered equipment to the hull alone when it adds the hull.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone.Admit(entity, piloted) adds the entity to Entities, activates it and,
when piloted, gives it the zone's Minion. It is the only writer of
Entities.Add and the only creator of agents (docs/scenarios-cut.md, 2.2).
The constructor's pack loop admits every packed ship but the player's as
piloted and keeps its own origin-scatter rule, which is generation
placement, not admission. Entity.TryUndock admits the undocked ship
unpiloted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.Death, the owner of the stream, now ends after its first cause. A blast that destroyed a
cockpit fired Death, which removed and deactivated the entity, and the blast's own DamageHull then
raised a second Death to every subscriber but Zone's idempotent handler. Every damage path gets the
guard because none of them raise Death themselves.
New pins: one blast killing three ships (a break after the first kill survives the old suite), a
shieldless ship outside the disc receiving nothing, a cockpit-kill blast and a direct hull kill each
raising Death exactly once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FireControl.Apply now switches on the frozen shot.Fuse: a null fuse keeps
12.3's lane path (ApplyDirectHit), a proximity fuse detonates at arrival with
no roll, and a contact or delayed fuse detonates only on a committed hit
(ApplyBlastHit), locating P through Lane and the new Reach owner (moved
verbatim from Apply's own inline penetration clip; the delayed fuse point
reads the identical rule). Step has no damage branch left.
Detonate (FireControl.cs) is the one owner of blast damage: candidates by
centre-plus-half-diagonal, exact circle/cell overlap shares computed once,
every blast's shield decided live per Q12-9 = A (including the host of a
contact/delayed blast -- Commit now gates its own shield decision on
shot.Fuse == null), armour absorbed per cell, and each item's covered cells
pooled into one ItemAbsorb call. Entity.Absorb and FireControl.Splash are
deleted; Entity gains ToSchematicPoint/ToWorldPoint for the point conversion
Detonate and the delayed fuse point need. Mine.Explode now calls Detonate.
Existing Splash/Absorb-era tests across Cut4/5/6/11/123 are rewritten or
retargeted to the new public owners; behavioural coverage for the blast
model itself lands separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.ItemAbsorb's `item == null` guard is unreachable: both callers (Absorb,
and FireControl.ApplyPooled's Resolve) already guarantee a non-null item
before calling in. Its `total > 0f ? ... : 0f` divide guard is unreachable
too -- the function already returned above whenever total <= 0.1f, so the
divide never sees zero. Both deleted; the surrounding comment now says why
instead of describing a guard that no longer exists.
ArmorAbsorb's own comment was stale from before the "one code path" ruling:
it described the item phase as deferred only for a cell shared with another
lane, when every occupied cell now defers to ItemAbsorb's pool, a lane that
turns out to be the sole contributor included (a pool of one, not a separate
solo path).
Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4, F6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the survey-then-branch (ApplySequential vs. ApplyWithSharedItems)
with a single pooled walk. Lanes are clipped to penetration once, right
after Lanes returns; every item -- single-cell or multi-cell -- pools
whatever lanes reach it and resolves the moment no other unfinished lane
can still reach it down its own remaining clipped walk, with no separate
reachability survey. A cyclic dependency between two non-convex items
resolves every currently open pool at once, order-independent by
construction. Entity.ItemAbsorb becomes the one absorption function,
gated on the pooled total; Absorb (Splash's only remaining caller) now
funnels through it with a one-contribution pool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling (2026-09-25): "proportional absorption; go". A multi-cell item
struck by several lanes of one shot now absorbs their combined incoming damage
(up to its own remaining durability) and each lane's leftover is in proportion
to what it brought, replacing the old behaviour where whichever lane Apply
happened to visit first drained the item and the next lane inherited whatever
was left.
Entity.Absorb is split into ArmorAbsorb (per-cell, never shared) and ItemAbsorb
(the single-lane path), so FireControl.Apply can call armour immediately and
defer item resolution only for a cell whose item is reachable by more than one
lane this shot. FireControl.Apply gains a geometric survey (which items are
reachable by >1 lane, decided before any damage is applied) and takes the
untouched sequential walk (ApplySequential) whenever nothing is shared, or a
new coordinated walk (ApplyWithSharedItems) that deposits each lane's
post-armour remainder into a shared item's pool and resolves it once every
contributing lane has arrived, splitting the leftover proportionally
(sum-then-split, order-independent by construction). A deterministic tie-break
(smallest first-contact Entry) breaks the rare "opposite order" deadlock two
non-convex shared items can create between two lanes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>