---
title: "Gameplay simulation patterns (DOTS)"
slug: /workflows/gameplay-simulation/
markdown: /workflows/gameplay-simulation.md
section: gameplay
tier: 2
summary: "Crowd movement, sensing, target selection, spawn placement and match flow on ECS."
sources: ['dots/gameplay-sim.md']
canonical: https://unityworkflowlibrary.dev/workflows/gameplay-simulation/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# Gameplay simulation patterns (DOTS)

> Crowd movement, sensing, target selection, spawn placement and match flow on ECS.

Sim systems built on top of ECS: crowd movement, AI sensing, target selection, spawn placement and match flow.

- **PRESENTATION-Y-LIFT-IS-THE-COMBAT-HEIGHT — if sim stays on the XZ ground plane and
  presentation adds a hover Y, muzzle, projectile hit capsules, aim points, and death pose
  must use that same lift.** Otherwise shots leave the feet, capsules miss the visual body,
  and corpses hover for the whole death window. Drop the hover on death (then shared burrow),
  don't keep applying it while `DeathState != 0`.

- **SPAWN-QUANTIZE-COINCIDENT — a spawn/formation resolver must NOT snap valid points to nav-cell
  centers, and any separation force needs an ASYMMETRIC zero-distance fallback.** Two paired traps
  that together make "N units spawn but N-1 are visible, forever": (1) a placement helper that
  returns the walkable CELL CENTER even when the desired point's own cell was already valid
  quantizes every spawn to the grid — formation offsets smaller than a cell (1.15 m spread on a 2 m
  grid) collapse onto the SAME center, spawning units perfectly coincident. Return the exact
  desired point when its cell passes; snap to centers only while searching outward from a failed
  cell. (2) Coincident units then never separate when the avoidance/separation fallback at
  distance≈0 is a CONSTANT direction — both get the identical push AND identical steer (same cell),
  so they march in lock-step for life. Derive the fallback from per-agent identity (index/entity
  hash, e.g. golden-angle by index) so any overlap breaks symmetry on the first tick. Also: a
  placement "last resort" return must still be walkable-checked (ring-search) — a blind
  `pos + fwd * K` can land inside a blocked stamp and read as "unit spawns but never moves".
  (Seen in a mobile strategy game: every 4-unit wave rendered as 3 — the two middle units resolved to
  one cell center and marched stacked through whole matches.)

- **ABILITY-LADDER-SUPERSEDE - a research ladder that grants a per-TIER ability must declare which
  tier it replaces, or a fixed-capacity loadout silently drops the strongest tiers.** The shape:
  `Critical Strike I...V` where each level's upgrade grants its own ability id into a
  `DynamicBuffer<AbilitySlot>` capped at N (8 is typical). Grant logic that only APPENDS (and skips
  duplicates) means one 5-level track eats 5 of the 8 slots - research two ladders on the same unit
  and the later, HIGHER tiers hit the cap and are dropped with no error. The unit keeps tier I
  forever while the shop happily sells II-V, which reads as "the upgrade did nothing". Fix is an
  explicit authored `SupersedesAbilityId` on the ability: at spawn, drop any collected id that a
  higher tier supersedes; at live grant, OVERWRITE the superseded slot instead of appending. Do not
  infer supersession from "same EffectKind" - two distinct actives legitimately share a kind
  (Blizzard and Chain Lightning are both DamageRadius). And when several tiers CAN coexist, make the
  consumer take the best row rather than stacking them. (a mobile strategy game, 9-track x 5-level
  tower academies across 7 races.)

- **DAMAGE-PROC-DEFERRED-DRAIN - on-attack procs that touch a THIRD entity (lifesteal, splash)
  are recorded in the damage job and resolved in one ordered drain system; only the procs that
  modify the CURRENT event (crit, evasion) belong inline.** The damage pass is a parallel job over
  VICTIMS, so healing the attacker or damaging the victim's neighbours is a cross-entity write two
  parallel indices can race on (many victims share one attacker). Enqueue a proc credit into a
  `NativeQueue<T>.ParallelWriter` owned by the damage system's `SystemHandle` and drain it in a
  system ordered `[UpdateAfter]` - the same producer/consumer split structural changes already use.
  Two details that bite: (1) a splash-produced DamageEvent must carry a `FromProc` flag or the next
  tick rolls the attacker's procs on it again and splash feeds itself forever; (2) roll evasion ONCE
  per victim per tick, not per event, or a volley re-rolls the same dodge N times. Attribution stays
  with the attacker so kill bounty still pays. (a mobile strategy game.)
  **(3) QUEUE-CONSUMER-COMPLETE-PRODUCER: a raw NativeQueue is NOT a component type, so the ECS
  dependency system cannot chain the producer job to ANY consumer — even one in another system that
  fetched the queue off a singleton/SystemHandle. Every consumer must explicitly complete the
  producer before its first touch (even a `Count` read trips the safety system):
  `state.WorldUnmanaged.ResolveSystemStateRef(GetExistingUnmanagedSystem<ProducerSystem>)
  .Dependency.Complete;`. `[UpdateAfter]` ordering does NOT complete jobs, and `GetSingleton` on
  the wrapper component only syncs the component copy, not the container inside it. The symptom is
  a per-tick InvalidOperationException flood ("previously scheduled job X writes to NativeQueue")
  that only appears once combat actually exercises the queue.** (a mobile strategy game: husk-spawn
  queue, ~1000 exceptions/min; the sibling proc-credit queue had the completion and never flooded.)

- **MATCH-LAUNCH-SINGLE-FLIGHT — do not `CreateServerWorld` / `Listen` on the menu
  click.** World create reloads UITK and a second click `Dispose`+recreates the
  listen-server mid-bootstrap (`ObjectDisposedException` on catalog buffers).
  One `StartHostLocal` / `StartHostRelay` from the load routine; extra clicks no-op
  while that load is in flight. See the scene flow page same id.

- **CROWD-SEPARATION-TWO-CHANNEL - crowd avoidance needs a soft steering channel AND a hard
  overlap channel; one signal cannot serve both, and the seam that hides it is the anti-seek
  projection.** The safe march recipe (cap the boids push under unit speed + project away its
  anti-seek component so a crowd can never reverse the lane and eject units off-map) has a
  structural blind spot: units steering at the SAME point - a shared spline/file carrot, one
  chase target - need exactly the anti-seek push the projection deletes, so they compress
  straight through each other; and any hold state that early-returns (Engage stop, arrived
  goal hold) discards the avoidance write entirely, freezing the stack in place for the whole
  fight. Fix shape: the avoidance job outputs TWO fields - soft AdjustXZ (wide ring, strafe
  bias, lateral-projected in march, unchanged) and hard SeparationXZ that fires ONLY while
  radii actually interpenetrate, is pure radial, unprojected, capped below unit speed, and is
  applied in EVERY move mode (march adds it to desired; Engage/goal holds integrate it with
  the same walkability step-refusal). Overlap-gating is what makes anti-seek safe: strength
  decays to zero at contact distance, so it can slow a march into a body but cannot build an
  ejection velocity. Two stability details: (1) enemy pairs resolve at a FRACTION of summed
  radii (~0.6) - melee chase deliberately stops deep inside reach, so a full-radius pushback
  vs the unit's own target ejects it out of attack range and flickers Engage<->March forever;
  (2) coincident pairs need the per-identity golden-angle fallback direction or both take the
  same push and never part (see SPAWN-QUANTIZE-COINCIDENT). Verified by pairwise census in a
  live 45-unit brawl: deep overlaps (>50% of contact) at zero, shallow contacts transient.
  (a mobile strategy game, lane waves stacking on the spline carrot and inside melee balls.)

- **CROWD-GOAROUND-THIRD-TERM - two avoidance channels are still not enough: a blocked mover
  needs a TANGENTIAL go-around term, and the holding front rank must YIELD less than movers.**
  Amendment to CROWD-SEPARATION-TWO-CHANNEL, from the very next playtest: with soft steering +
  hard overlap separation + surround slots all working, units still "pushed each other from the
  back" - because radial separation is force resolution, not navigation. A mover whose corridor
  to its carrot is blocked by a stopped body has no signal that says go AROUND; its soft push
  from a dead-ahead blocker is almost pure anti-seek (deliberately projected away in march), so
  it leans forward into the blocker's back - and the symmetric hard push meanwhile bulldozes the
  engaged rank into the fight. Two additions close it: (1) go-around - when a HOLDING body
  (engaged, or steer ~ own pos) intrudes a mover's forward corridor (lateral < summed radii,
  ahead < ~2.5x summed radii), add a push PERPENDICULAR to the seek direction, side chosen by
  the blocker's lateral sign (dead-center ties break by entity index); perpendicular means the
  anti-seek projection preserves it, so the unit actually curves. Clamp the lookahead below the
  carrot distance or the final approach orbits the very body its surround slot hugs; react to
  HOLDING blockers only or same-speed column marches weave. (2) asymmetric yield - a holder
  receives only ~25% of a mover's hard push (holder-holder and mover-mover stay symmetric), so
  the front rank stands and queue pressure resolves as flow around the flanks. Census note: once
  a waiting ring exists, "same bearing, one behind the other" pairs become LEGITIMATE (rank
  formation); the metric that still means pressing is contact-range overlap, not bearing
  alignment. (a mobile strategy game, same feature, one playtest after the two-channel fix.)

- **AVOIDANCE-NEVER-OVERDRIVES-ANIM-ANCHOR - crowd forces may REDIRECT a march but never push a
  unit's ground speed above its authored move speed, and an observed-velocity anim driver needs
  error-proportional smoothing - otherwise units near crowds park their Speed param astride a
  blend-tree anchor and the walk anim visibly shimmers.** The mechanism hides in the composition:
  soft-push / go-around forces keep their FORWARD component after the anti-seek projection, so a
  locomotion clamp with headroom (1.5x authored speed) lets crowd pressure ADD velocity - a big
  unit trailing its own wave cruises ABOVE its authored speed (probe: god at 3.43 avg vs 3.1
  authored), wobbling tick-to-tick across the run anchor where the 1D blend tops out AND the
  stride-scale param kicks in; every crossing flips gait math, which reads as "the walk anim
  shakes near other units" and is invisible alone (a steady blend ratio looks fine). Constant-
  tuning the avoidance magnitudes moves the wobble around and can make it WORSE. Diagnose with
  telemetry, never eyes: sample per-tick raw ground speed + the driver's smoothed param - the
  tell is raw avg ABOVE authored MoveSpeed with body contacts at ZERO (forces, not blockers, are
  driving). Fix BOTH owners: (1) clamp the locomotion desired-vector magnitude to authored
  maxSpeed (direction preserved - redirect, never overdrive; separation-only hold modes are
  already below it); (2) make the driver's tracking rate proportional to |error| clamped to the
  legacy accel/decel caps, so sub-0.3 m/s crowd ripple tracks at a crawl while real start/stops
  stay snappy. Related fairness fix from the same hunt: reciprocal size split - each side of an
  avoidance pair yields by `2*otherR/(selfR+otherR)` (equal pairs unchanged) so crowds part
  around big units instead of vibrating them - and facing follows navigation intent (seek +
  projected soft avoid) only, never the hard separation channel. (a mobile strategy game: undead
  god "walk shakes near units, worse after tuning"; after both fixes the same probe read 3.10
  sd 0.00 in the same near-units condition.)

- **AI-SENSOR-DAMAGE-EDGE - macro-AI threat/state sensors arm on DAMAGE EDGES with a decaying
  hold, never on proximity to always-trafficked zones or a level-trigger over non-recovering
  state.** Two sensor shapes pin an AI state machine permanently, and the starvation is
  invisible because the AI "does nothing" (no research, no build orders) while looking busy
  (panic unit dumps): (1) enemy-proximity radii around defense structures in a lane-pushing
  game - wave traffic lives inside those radii by map geometry, so "enemies near tower /
  barracks" is true ~100% of the match; (2) a level-trigger on state that never recovers
  ("any own structure below X% HP => threatened") - structures don't regen, so one bad push
  arms panic for the rest of the match. Correct shape: arm on damage-evidence EDGES - LastHit
  counter pulses (XOR signature over the sensor set, split BY STRUCTURE CLASS so mid-lane
  barracks chip can't arm base-panic / research-block), HP-drop deltas vs a stored sample -
  each arming re-stamps a decaying hold timer, and level state (critical HP) only AIMS the
  response (lane/focus), never arms it. A real push lands its first hit pulse within a
  second, so proximity early-warning buys nothing. Diagnose with a live probe of the hold
  timers: a value pinned at its cap across samples minutes apart = a per-frame re-armer, not
  a real siege. (a mobile strategy game: bots never researched and mass-dumped barracks stock
  all match; damage-edge rearm verified live - inject fortress drop + rax hit pulse, arm ->
  decay -> calm.)

- **AI-SENSOR-CALM-COUNTER - the safe complement to AI-SENSOR-DAMAGE-EDGE: a LEVEL-trigger is
  legitimate exactly when the RESPONSE clears the trigger, and visible durable opponent state
  (enemy building levels) is the right calm-tier sensor for "react to the opponent's
  investment".** Damage edges alone leave a macro AI blind until the enemy's stronger waves are
  already hitting its base - the developer reads it as "bots don't defend when I upgrade their
  lane". The missing sensor is opponent DEVELOPMENT: compare each own lane structure's level to
  the nearest enemy counterpart facing it (nearest-by-XZ pairs the shared-corridor opponents
  without hardcoding map topology) and treat the deficit as lane pressure. Why a level-trigger
  is safe HERE when it pins panic elsewhere: counter-leveling REMOVES the deficit, so the
  trigger self-clears - unlike critical HP on non-regenerating structures, which never recovers
  and pins forever. Three guardrails keep it sane: (1) it AIMS calm investment only (score +
  host-rank bonuses toward counter-leveling that lane, army-tech bump while behind) and never
  arms panic - damage edges keep owning that; (2) scale the response by the deficit so a
  widening gap escalates; (3) read only state a human opponent could see on the map (levels,
  built structures), never hidden purchases - reactivity, not cheating. Durable state also
  needs no per-tick edge detection: sense it once per think. (a mobile strategy game built the
  damage-edge side; play-verified the counter-sense - bot counter-leveled the exact
  facing lane in ~6s while the non-facing bot stayed calm.)

- **TARGET-PRIORITY-FRONT-BAND - a target-priority table needs a proximity-dominance band for
  melee, or priority + retaliation + keep-target hysteresis compose into "the whole pack drags
  through the front line chasing one archer".** The pathology needs three individually-correct
  pieces: (1) a priority table (hero 110 > unit 100, or any type preference); (2) a retaliation
  bonus stamped from a RecentAttacker slot (+15 to whoever shot you - a back-rank ranged unit by
  construction); (3) keep-current hysteresis that re-applies the bonus while the target stays in
  leash. Result: a melee unit shot once by an archer scores it above the enemy standing in its
  face, walks INTO the enemy front line (avoidance stops it, so it deals zero damage while
  bumping), and the leash keeps the fixation alive because the archer keeps refreshing
  RecentAttacker. Reads as "units fixate on ranged units instead of fighting what's in front".
  Fix shape: a FRONT-FIGHT band - any valid candidate inside
  `max(attack.Range, MeleeFrontFightRadius)` + target pad gets a bonus that DOMINATES the whole
  table plus every stacked bonus (+1000 vs a 30-125 table), applied in BOTH the acquire scan and
  the keep-current re-evaluation (only one side = either never engages the blocker or never lets
  go of it). Within the band the normal table still orders (an in-band hero beats an in-band
  unit; hysteresis still prevents thrash between equal in-band picks), and the unit may still
  INTEND the distant pick - it just fights whatever is at hand on the way. Melee-only by default:
  ranged units don't move to shoot, so priority inside attack range is fine (the ranged variant -
  band = own attack range so a ranged unit never walks while something is shootable - is a
  separate decision, it changes hero-focus behavior). Verify with a live pairwise probe, not by
  eye: for every melee unit whose target is beyond the band, assert no valid enemy stands inside
  it (a mobile strategy game: 63-69 melee across two matches, 0 violations post-fix).

- **SURROUND-RING-VS-FOOTPRINT - melee surround-slot systems assume POINT targets; a footprint
  target's ring slots land inside its own nav stamp and the waiting ring parks attackers
  permanently out of reach.** Two facts collide at buildings: (1) blocked-footprint stamps
  quantize OUTWARD to whole nav cells (a cell is blocked if the footprint touches it), so
  blocked ground extends up to one cell past the authored half-extent; (2) surround ring radii
  derive from stop distance (attack range measured to the footprint SURFACE), so ring-1 slots
  sit inside the stamp - every slot fails the walkable check - and the ring-2 "wait for a slot
  to free" fallback sits OUTSIDE attack range, where nothing ever frees it (slots free on
  attacker deaths; a building's ring has none). The unit arrives at ring 2 and stands forever:
  not engaged (out of range), not moving (arrived), not attacking - until an enemy UNIT enters
  aggro and retargets it. Alignment-dependent ("usually happens near buildings", never always).
  Fix shape: footprint targets SKIP the ring system entirely - center steer + walkability
  step-refusal IS the surround for a wall - and engage/attack reach vs footprint targets gains
  a stamp-quantization slack (~half a nav cell + body radius) applied IN LOCKSTEP across target
  acquisition, melee reach, and ranged reach: any one of the three missing the slack recreates
  the freeze as "engaged but never swings". The slack widens the settle stand-off from walls -
  keep it a documented tuning const. (a mobile strategy game: "AI just stops attacking or moving
  until a unit retarget, when a building is around".)

- **PROJECTILE-ARM-EXEMPTS-ORDERED-TARGET - a spawn-protection arm window on a target-ordered
  projectile visual must still hit-test the ORDERED victim from frame 0, and a forward muzzle
  offset must collapse to the body axis when the target stands inside its reach.** At point-blank
  (a melee attacker hugging a caster) the bolt crosses its victim entirely INSIDE the arm window
  (0.06s at ~17 m/s is ~1.1m - wider than a whole hit capsule), then sails past and flies its full
  life "off to the side" - while server damage (unarmed, target-locked) lands fine, so it reads as
  a broken attack that no damage log corroborates. Arm windows exist to skip near-muzzle bodies;
  the ordered target is never a false positive, so exempt it from the gate instead of shortening
  the window (shorter still loses a frame at low FPS). Companion launch trap: a staff-tip/bow
  muzzle authored FORWARD of the body can sit at or past a hugging target, so the launch direction
  reads sideways/backward from camera - when the flat self-to-target distance is inside the
  muzzle's flat reach + a small pad, launch from the body axis at the authored height instead.
  (a mobile strategy game: mage autos vs adjacent melee.)

- **KCC-SWEEP-SLIDE-STEP-DEPENETRATE — a kinematic character controller is sweep EVERY leg + slide +
  step-up + budgeted `ComputePenetration` depenetration, with movement BLOCKED while embedded.** One
  truncating capsule cast is not a controller: a glancing hit stops the player dead without SLIDE,
  jagged mesh triangles wedge the capsule on "flat" ground without a STEP, a cast that starts
  interpenetrating returns distance 0 and freezes the player permanently, and a horizontal-only
  sweep with `y += vertical` applied blind tunnels on the axis gravity uses most (one 60 Hz tick at
  terminal velocity is 0.667 m). "Allow movement while overlapped" as the wedge recovery is the
  mesh-walk exploit door. Grounded is the persisted flag the sweep WROTE, probed with a sphere (a
  centre ray lands in crevices and sucks the body down) and true only while DESCENDING — a
  proximity test reads grounded during the first ticks of a jump. (a survival game)

## RADIAL-UP-NEEDS-THE-BODY-CENTRE-NOT-THE-POSITION

- **On a curved world, `normalize(position)` is only up when the simulation origin IS the body's
  centre.** Any recentred frame — a floating-origin rebase, a per-region chart, a sector local to a
  patch of surface — puts the origin ON the surface, so the centre is a large offset in local metres.
  A kilometre from that origin the position vector is nearly HORIZONTAL, and it degrades smoothly
  rather than failing: it is exactly right at the origin, so a probe parked at spawn looks correct.
- Up is `normalize(position - centreInLocalMetres)`, read from the frame the entity belongs to. The
  frame already carries that centre; nothing should re-derive it.
- Symptom shape when this is wrong: the tangent projection strips the wrong component so movement
  leaks into the radial axis, a ground settle pushes bodies sideways into terrain, and a ±N m ground
  raycast fires ALONG the surface and reports no hit — so a spawn gated on ground support places
  nothing at all and the feature is silently absent rather than visibly broken.
- The fix is one owner for the formula. If the helper that has it takes an `EntityManager`, a Burst
  job cannot call it and every job writes its own copy; give the helper a plain-data overload taking
  the frame, and have the manager version forward to it.
- Audit by grepping for a normalize of a raw position (`normalize(position)`, `normalize(self)`,
  `normalizesafe(transform.Position, up)`). Measured on one lane: four jobs and a spawn probe.

## REBASE-CARRIES-WHAT-THE-ENTITY-RIDES

- **A floating-origin rebase or region transfer keyed per character must move the vehicle the
  character sits in, in the same commit, or refuse to start.** A seat that copies the vehicle pose
  into its occupant every tick fights a transfer that moves the occupant alone. Measured on one
  server: a seated driver placed 14 m past the 2,048 m rebase line was re-prepared every ~0.6 s
  (22 times in 14 s) and never committed. Both poses froze, the occupant's region changed about 20
  times without its pose moving, and the vehicle stayed in the old region.
- The refusal that should have caught it checked only for a physics parent, and a seat is not a
  parent. Make "is riding something" its own gate, or make the vehicle root the transferred unit
  and carry occupants and cargo with it.
- Test a seated crossing, not only a walking one. A scripted vehicle driven at the line wanders
  and wedges on terrain; place it past the line to prove the transfer path, and record the driven
  crossing as unproven.

## LIVE-KEY-NEVER-FROM-A-LOSSY-DESCRIPTOR

- **Never rebuild a live object's identity key from a stored description of it; look the live object up by the handle
  you already hold.** A region registry keyed frames bit for bit on a cell-plus-offset origin. Boarding rebuilt the
  frame from its saved descriptor, which kept the origin as double meters, and at planet distance the round trip
  dropped offset bits. The registry refused "a live frame with different canonical metadata", and the catch
  disconnected the player. A model of the conversion failed for 10000 of 10000 sampled origins.
- Lease or join by the membership or handle the caller holds. Keep descriptors for persistence and for creating a new
  frame, never for finding an existing one.
- A thrown prepare step that has committed nothing releases and latches (logged once, no retry loop). It never
  disconnects the client.

## RANGE-CONTRACT-USES-EFFECTIVE-GEOMETRY

- A promise that one weapon can answer another must compare effective distances from the same origin and plane, including target footprint and reach allowances. Comparing two authored range numbers alone cannot prove it.
- Scope collision-clearance exceptions to the attackers that need them. Keep acquisition, release and burst/volley checks on the same minimum/maximum band; present those resolved values in the UI.
- Regression evidence: a 6.5 m ranged attack plus 1.65 m footprint and 2 m melee clearance stopped about 10.1 m from a tower with a 9 m weapon. Removing the inapplicable ranged allowance restored reach without widening tower balance.

## AMBIENT-WANDER-IS-A-DESTINATION-NOT-A-BEARING

- **An idle agent that redraws a uniform heading on every decision does not wander, it paces.** A
  uniform bearing has a MEAN turn of 90 degrees against the heading it already had, and half of all
  draws exceed that. At a decision cadence of 0.4 to 0.7 seconds — normal for a cheap utility brain —
  that is a body reversing several times a second. It reads as broken to anyone watching, and no
  amount of turn-rate limiting in the locomotion hides it, because the intent itself is noise.
- Wander is a **destination held until it is reached**: a leg of an authored length, on a heading
  turned by at most an authored angle from the CURRENT one. Correlating the new heading with the old
  is the whole fix; the leg length and the turn ceiling are what make one species hold a line and
  another dart about, from the same code.
- Most legs should end in a **pause**, not another leg. An undisturbed animal, civilian or idle unit
  spends most of its time stopped, and a stop is what makes the movement between stops read as
  purposeful. Author the chance and a seconds band per kind.
- A leg needs an abandon timer as well as an arrival test, or a body walking into geometry holds a
  destination it can never reach. Size it from the leg length at the gait it will use, with slack for
  avoidance detours — not a constant.
- Deriving the bearing from a hash of (identity, decision tick) instead of storing it does not avoid
  any of this. It is still a fresh uniform draw per decision; it is only a reproducible one.

## OPPOSED-BEHAVIOURS-NEED-A-SHARED-BOUND

- **Two behaviours that pull opposite ways will oscillate unless one of them is clamped inside the
  other's threshold.** Cohesion pulling a member back to its group and wander pushing it outward is
  the canonical pair: wander aims out, cohesion fires, the body returns, wander aims out again. The
  visible result is identical to random-bearing pacing, so the two defects mask each other and fixing
  one alone leaves the symptom in place.
- Clamp at the SOURCE: the wander destination is clipped to the cohesion radius when it is chosen, so
  cohesion never has to fire for a self-inflicted excursion. One authored radius, not two.
- The general test: for every pair of behaviours where one's success condition is the other's trigger,
  the first must be unable to cross the second's threshold on its own.

## ONE-DECISION-ONE-CLOCK

- **A decision guarded by two timers ANDed together is guarded by the shorter one.** The shape
  `if (committed && now < nextThink) return;` reads as "held for the longer of the two" and behaves as
  "held for the shorter": once the think timer fires, the commitment is re-scored and can flip, so a
  2.5 second commitment inside a 0.5 second think cadence never held anything. It is a silent defect —
  the field exists, is written, is read, and does nothing.
- The two clocks mean different things and must be applied separately: the think cadence decides HOW
  OFTEN the agent looks, the commitment decides WHETHER it may change its mind. Gate them in that
  order and both keep their meaning.
- Holding a decision must not hold its AIM. A chase committed for two seconds against a target
  position captured two seconds ago is a predator running at where the prey used to be. On every
  think, re-read the target's position for the action already chosen, and leave alone the actions
  whose target IS the decision (a roam destination) or which have no target (a pause).
- Debug readouts must show what the agent DID, not the candidate that won. If the winning candidate is
  post-processed into something else, relabel it in the readout; a score table disagreeing with the
  body next to it costs someone an afternoon.

## A-PHYSICAL-BODY-IS-NOT-AN-OBSERVER

- **OBSERVER-IS-A-ROLE-NOT-A-POSITION (HARD) - reusing the streaming/AI observer marker so a spawned
  body keeps its ground loaded makes every spawn open its own streaming ring and re-trigger every
  refresh keyed on observer count.** Measured: each dropped item cost 20-30 ms for several
  frames after it landed, with three terrain chunk generations per drop. The marker had been added for
  one honest reason - the chunk under the item must stay collidable or it falls through - but the
  systems downstream read it as "a viewer is here": the streaming system opened a Chebyshev square of
  `max(viewRadius, collisionRadius)` chunks around it plus look-ahead from its velocity, and a
  trunk-collider system and a census system re-ran their whole refresh because the observer COUNT
  changed. After splitting the role: zero generations in 705 frames, frame time flat.
  - **Split the marker, do not scale the radius.** A support marker whose sample span is **0** keeps
    exactly the chunk the body rests on - no ring, no look-ahead, no census, no AI activation - and
    the viewer marker stays with players. Put the span in a pure function
    (`Span(supportOnly, viewRadius, collisionRadius)`) so a test pins it with no world.
  - **Every system keyed on observer COUNT is the blast radius.** Enumerate them from the marker's own
    declaration before adding a second observer kind; a count-triggered refresh turns "one more
    observer" into a full rebuild, which is why the cost appeared on the frames AFTER the spawn.

## CLAMP-DEMAND-TO-THE-ENVELOPE-BEFORE-ALLOCATING

- **An actuator allocator that minimises a weighted force + torque error buys extra force with torque
  error when the demand is impossible.** Measured on a thruster-driven hull whose main
  engines sit off the centre of mass: the velocity loop asked 628 m/s2 of a 30 m/s2 ship at full
  throttle, and 20 s of "fly straight and hover" ended at -103 m/s vertical, tumbling. Clamping the
  desired acceleration and turn acceleration per axis to the measured envelope first: 220 m/s, 0 deg/s.
  - **The envelope is the largest pure push (or turn) per axis the same allocator reaches with the
    other axes held at zero**, found by bisection. Rebuild it when an actuator is lost or restored,
    never per tick; a lost thruster then shrinks the envelope instead of spinning the hull.
  - **Measure with more solver iterations than the tick uses.** At 12 coordinate-descent sweeps a
    damaged hull's 3.3% torque residual failed a 1% test at every scale and read as "cannot go
    forward" (true value 29.8 m/s2 at 200 sweeps); in flight the rate loop absorbs that residual.
  - **Start the per-tick solve from zero throttles**, so a predicted client's replay needs no stored
    solver state.
  - **Time the rebuild, not only the step .** Measured on a 22-thruster hull: the per-tick
    solve cost 3.2-3.3 us in a Burst job, while the envelope rebuild (64 sweeps, 12 bisection steps,
    twelve axis directions) took 20.5 ms managed, longer than a 60 Hz tick. A volley that damages ten
    ships would stall the server. Rebuild in a Burst job off the tick that applied the damage and fly
    on the old envelope until the new one lands; gate the rebuild time in the same test as the step.

## A-TIMED-STATE-NEVER-SAVES-AS-ITSELF

**A server state that a clock ends (bleeding out, a stun, a burn, a respawn lock) is saved as
where the clock leads, never as the state itself, because offline time never runs the clock.**
Measured on a downed state with a 60 s bleed-out: a body saved as downed would come back downed
after a logout, a crash or a server restart, with a fresh timer or none. Logging off then works as
an escape, and a restart turns into a free revive. The fix:

- **Save the outcome.** The downed player saved at shutdown or on leaving comes back needing
  respawn. A revive saves again, alive, with its low health.
- **Give it its own flag when the outcome's side effects have not happened yet.** "Dead" there
  meant the loot was already on a corpse; a downed body still carried it. Folding downed into dead
  would lose the backpack. A separate flag lets the offline-body owner finish the death properly,
  moving the loot onto a corpse the moment the player leaves.
- **Trigger a save when the state changes**, not only on the periodic save, or a crash between the
  two brings the body back standing.
- Test the restart path for both the timed state and its clean exit (downed -> respawn needed;
  revived -> alive with its health).

## SHARED-CAP-DRAINS-BY-PRIORITY

- **A first-come per-frame cap shared by several event topics starves whichever topics flush last.** Measured:
  presentation topics flushed in fixed bus order, so early high-volume topics (impacts, deaths) spent the per-frame
  view cap, and later ones (crit, shield break, level-up) played nothing in busy frames although their authored rows
  existed. Queue requests during the flush and drain them by an authored per-topic priority afterwards; gate
  frequent topics with an authored cooldown. Test at the cap, not in a quiet frame.

## STICK-DEPTH-FROM-THE-RENDERED-BODY

- **Embedding a stuck projectile relative to the shared gameplay hit capsule floats it on thin bodies and sinks it
  into bulky ones.** Measured over 108 unit looks x 72 rays: infantry torsos sit 19-25 cm inside a 0.40 m capsule, so
  a 0.12 m embed left tips 7-13 cm outside the skin (about 90 % outside), while horses are wider than the capsule.
- Author a presentation body radius per unit type (the median radial skin distance from the capsule axis, which
  ignores held weapons and arms). Intersect the flight ray with that visual capsule, because an oblique ray crosses
  more than `hitR - visualR`; add the embed; clamp at the ray's closest approach to the axis. Result: 1 % outside,
  tip p50 19 cm in (17 % of the shaft). Gameplay hit radii stay unchanged.
- **An open vehicle (catapult, cart, frame) is mostly air, so no radius works.** Author solid-part boxes per vehicle
  (oriented boxes in model space, an authored cap per unit, one validator shared by the inspector, the authoring tool
  and the entity builder). At impact only, slab-test the flight line against them, embed from the first box entered
  but never past its middle, and move a shot through a gap sideways onto the nearest part. Measured: 58 % of tips
  outside the mesh before, 0-1 % floating with 8 boxes.
- Propose boxes from a voxelised solid (fill inside along two axes so open meshes do not streak), then greedily carve
  the all-solid box that covers the most uncovered volume. Boxes fitted around mesh islands, or merged part OBBs of
  spoked wheels, stay half empty. Score "tip inside" with an entry/exit depth counter along the line; nearest-face
  tests break on interpenetrating parts.
- Measure offline: an Edit-mode probe that bakes each look's posed mesh into a collider and runs the runtime stick
  formula over a fixed ray set covers every look, while Play catches a few arrows before their targets die.

## CROWD-SLOTS-PROMOTE-THROUGH-THE-OPEN-STRETCH

Reserved attack and queue rows around one structure (a crowd attacking a tower or gate). Two lifecycle bugs appeared
only under attrition; see OPPOSED-BEHAVIOURS-NEED-A-SHARED-BOUND for the engage gate (engage only while holding an
attack slot).

- **One row per step empties the front rank.** Measured: attack slots in use fell from 51 of 51 to 0-5 once the
  structure killed faster, while 141 queue holders sat 11+ rows out with their cooldowns expired. Promotion moved a
  unit one row per step (walk plus cooldown), and that latency across 11 rows exceeded an attacker's life. A ready
  holder moves through the whole open stretch in one step: melee to the innermost free row, ranged to the outermost
  row within its reach.
- **"Join the back of your sector" must count only rows between the unit and the target.** Counting every claimed row
  in the arc, including rows behind the unit, left 111 units slotless at 4-10 m, standing inside the rows, because no
  row qualified (they are also never sent more than one row outward). Back = the outermost claimed row within the
  unit's own distance.
- **Verify every crowd or queue fix under high attrition.** Both bugs were invisible with a weak weapon (51/51 slots
  used at 950 units): churn only happens when attackers die fast. Each probe run includes a high-kill leg, records
  kills per second beside the result, and gates on a per-row histogram of slot kind and holding state.
- **Only claims of the unit's own blocking kind count as "the row in front"** (added). A ring of another
  kind, such as a ranged firing row, blocked melee promotion when any claim counted: measured 0 melee units within
  8 m while 245 ready holders waited behind 45 archers.

## BENCHMARK-STATES-ITS-SCENARIO

- **A synthetic job benchmark needs a Play-realistic clock and event rate.** Measured: a 1 s integer clock hid a
  0.35 s per-slot refill cooldown (full refill every tick), and 10 % of attack-row deaths per 30 fps frame (about
  150 kills/s, where Play peaked near 2 kills/s) invented a throttling that never happens. Take the clock and event
  rates from Play numbers and report the scenario beside the time.
- **The first Editor Burst timing of a job can read about 4x slow** (0.52 ms against 0.16 ms at 600 units, same job,
  cause not isolated; likely first-run compile or cache state). Warm up, repeat, and report the Burst safety-check
  state with the number.

## A-GHOST-WITHOUT-VELOCITY-IS-STILL-A-BODY

- **Classify what a shot or blade struck by what the entity IS, never by `PhysicsVelocity`.** Measured: a hitscan and a
  melee sweep labelled a hit "dynamic body" only when the entity had `PhysicsVelocity`; server-moved kinematic ghosts
  (animals: colliders, no velocity) all came back "static" with victim ghost 0, alive or dead, so the client drew a
  rock impact, left the decal on the world and showed no body-hit feedback, while damage (keyed on the hit entity)
  still landed, which hid it. The projectile path of the same weapon already used "any ghost or velocity", so one
  weapon had two answers.
- One classifier for every hit path (hitscan, blade, projectile), keyed on what makes a body: a health (or other
  body) component, or velocity. A test entity with health and no velocity must classify as a body.
- **Ghost-ness is not a body (amended).** The first fix keyed on "any ghost"; placed building pieces
  (doors, storage boxes) are ghosts too, so a bullet on a door would read as a body: decal dropped where decals skip
  moving bodies, and body-hit feedback on wood. A ghost only says the entity replicates. Test a ghost with no health
  classifies as static. A corpse must keep its health component until despawn, or it stops being a body.
