Field Engineering Report · 2026-06-10 · Issue 020

Behind That Rock — cover a soldier can actually use, from the bearing it actually faces

A boulder you can see on the open slope did nothing for the man tucked behind it: combat read a single averaged cover number per 5-metre cell, so a soldier hugging a rock was scored exactly as exposed as one standing in the open. The obvious fix — add cover on open ground — had already been tried and reverted: it made both sides survive longer and dragged every firefight into an attritional grind (+89% WIA). The thing that was missing wasn't more cover. It was direction.

0.39 was 0.08cover behind a boulder
0.00cover from the flank (directional)
−22% WIA 7.92→6.17US wounded (it protects, not grinds)
+7%enemy still killed (via the flank)

01The bug, measured

The 2026-06-09 pass promoted the drawn boulders to real sim objects (terrain.coverObjects, ~12,000 per valley, drawn = sim). But the firefight still queried the 5-metre raster. A new oracle (scripts/cover-directional-probe.ts) tucks a target just behind an open-ground boulder, puts a shooter on the far side so the round must pass through the rock, and measures the cover combat actually applies:

BEFORE behind 0.08 · open 0.08 · flank 0.08 ← the object is invisible to combat

Behind the rock, in the open, flanked — all identical. The rock the player could see was decoration to the bullets. That is the bug.

02Why the obvious fix was wrong — and the real one

Cover is the most balance-sensitive quantity on the map: add it omnidirectionally and both sides get harder to hit, so firefights stop resolving and casualties climb on a longer clock (the reverted stamp: WIA 3.92→7.42). The insight is that real cover isn't omnidirectional — a boulder only stops the round coming from the direction it faces. So the model is a directional, posture-aware occlusion:

terrain.coverOcclusion(shooter, target, postureHeight): an object BETWEEN shooter and target, close in front, across the sight line → blocks min(1, objHeight / targetHeight) · intrinsicStopProb (boulder 0.62, outcrop 0.72) → DIRECTIONAL: a flanker's round isn't on that line → 0 cover → POSTURE: a low rock hides a prone man (0.5 m) far more than a standing one (1.6 m) coverFor(target, fromPos) // the FIRE-hit path only — detection unchanged = max(rasterCover, coverOcclusion(fromPos, target, postureHeight)) findCover(...) // both factions now MOVE to tuck behind a rock the raster called bare
A radial plot of cover from one boulder: green and amber lobes pointing at the rock, ~0 on the flanks
The whole idea in one picture. A man tucks behind one boulder; the lobes show the cover he gets as the shooter's bearing sweeps 360°. It points at the rock — strong from there, ~0 from the flanks and rear. The prone lobe (green) dwarfs the standing one (amber): a low rock hides a man on his belly. This is the geometry the 5-metre averaged scalar physically could not hold — and it is exactly why the fix doesn't grind: a flanker is never in the lobe.

03Proof — the oracle, and the balance it was supposed to break

cover-directional-probebeforeaftermeans
behind the boulder (standing)0.080.39real, usable cover
from the flank0.080.00directional — a flanker sees him
open ground (control)0.080.006no object, no cover
prone vs standing behind it≈ equal0.62 vs 0.24posture — get low
The balance test it was supposed to fail. A same-seed A/B (12×50, the ITM_NOOBJCOVER kill-switch toggles only this code) is the exact opposite of the 2026-06-09 grind:
cover OFFcover ON
US KIA1.251.17
US WIA7.926.17 (−22%)
enemy accounted4.504.83 (+7%)
stranded00
Casualties went down, not up. Why: directionality. The patrol — caught in the open — now reaches a boulder and survives; the ambushers, already on rocky high ground, gain little new; and because cover only screens the threat arc, the squad's autonomous flank (shipped earlier) fires from a bearing the rock doesn't cover, so the enemy still dies. The fight resolves by maneuver, not by a longer attrition clock. Held-out A/B (fresh holdout-* seeds, Law 3 — never tuned on): US WIA 6.75 → 4.33 (−36%), KIA 0.50 flat, enemy 5.50→5.58, 0 stranded. The win generalizes — and the cover parameters were set from physical reasoning (a boulder's stop-probability, its footprint), not curve-fit to any seed.

04What it costs, and what it doesn't

Mechanism: lib/sim/terrain.ts (coverOcclusion + bucket index) · lib/sim/combat.ts (coverFor, findCover) · kill-switch ITM_NOOBJCOVER. Oracle: scripts/cover-directional-probe.ts. Raw record: docs/progress/2026-06-10-open-issues/020-directional-cover/. Built by an autonomous agent fleet; every number is reproducible from a seed in seconds.