From 882733819b88855e5265802454739d9cf6267972 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Sat, 26 Sep 2026 12:56:26 +0200 Subject: [PATCH] j134 fix: tracker must take the mover's switch per-call (instance flag was silently off on the object-literal construction path) + apply the correction on the reading that carries the server's turn-N+1 energy change (two-slot buffer); live-verified alignment --- ModularBot_garage/src/ModularBot.nim | 5 +- common_libs/movement_harness/fire_tracker.nim | 93 ++++++++++++------- common_libs/movements/learned_surfer.nim | 10 +- common_libs/movements/strafe.nim | 10 +- common_libs/movements/the_floor_is_lava.nim | 10 +- .../movements/the_floor_is_lava_ring.nim | 10 +- common_libs/movements/wave_surfer.nim | 10 +- common_libs/tests/test_strafe_fire_fix.nim | 64 ++++++++----- 8 files changed, 133 insertions(+), 79 deletions(-) diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index c6c9149..d8b8fa3 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -613,8 +613,7 @@ method onBulletHit*(bot: ModularBot, e: BulletHitBotEvent) = bot.surfMover.noteDamageDealt(e.damage) bot.learnedMover.noteDamageDealt(e.damage) if FireDiag: - echo "[firediag] EV dmg tick=", bot.tick, " damage=", e.damage - # Capture the rack BEFORE resolveOwnBullet drops the bullet's mode slot. + echo "[firediag] EV dmg tick=", bot.tick, " getTurn=", getTurn(), " damage=", e.damage # Capture the rack BEFORE resolveOwnBullet drops the bullet's mode slot. let hitMode = bot.bulletMode.getOrDefault(e.bullet.bulletId, vb.rm1v1) # onBulletHit only fires for our own bullets (victimId != myId); see json_parse.nim. let gunId = bot.resolveOwnBullet(e.bullet.bulletId) @@ -673,7 +672,7 @@ method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) = bot.surfMover.noteEnemyBulletHit(e.bullet.power) bot.learnedMover.noteEnemyBulletHit(e.bullet.power) if FireDiag: - echo "[firediag] EV hit tick=", bot.tick, " power=", e.bullet.power + echo "[firediag] EV hit tick=", bot.tick, " getTurn=", getTurn(), " power=", e.bullet.power # Feeds the ram bullet-rain abort window. Accumulate REAL ENERGY (the server's # 4p/6p-2 damage), not raw firepower, so `damageRatePerTurn` is a real # energy/turn rate (see movements/ram_decision.bulletDamage). diff --git a/common_libs/movement_harness/fire_tracker.nim b/common_libs/movement_harness/fire_tracker.nim index 5357b42..c6519a1 100644 --- a/common_libs/movement_harness/fire_tracker.nim +++ b/common_libs/movement_harness/fire_tracker.nim @@ -14,9 +14,9 @@ ## ## ── The two server facts that break the window (verified in the server) ───── ## * `rules.kt BULLET_HIT_ENERGY_GAIN_FACTOR = 3`: when an ENEMY bullet hits -## US the SHOOTER's energy RISES by `3*power` in the same tick. That rise -## is folded into the delta we read and can MASK the `power` the enemy spent -## firing the same tick (net delta >= 0 reads as "no fire"). +## US the SHOOTER's energy RISES by `3*power`. That rise is folded into +## the delta we read and can MASK the `power` the enemy spent firing the +## same tick (net delta >= 0 reads as "no fire"). ## `noteEnemyBulletHit` adds the bonus back. ## * our own bullet damaging the enemy the same tick adds `damage` to the ## delta, which can push it above the power cap and get the enemy's OWN @@ -29,34 +29,59 @@ ## `common_libs/tests/measure_strafe_fire_catch.py`). ## ## ── Switch ────────────────────────────────────────────────────────────────── -## Default ON. `TR_FIRE_FIX=0` disables the correction in EVERY mover; -## `TR_STRAFE_FIRE_FIX=0` still disables it in STRAFE (j133 back-compat), so -## STRAFE is on only when BOTH are on. With the fix off `detect` is exactly -## `prev - energy` inside the caller's own window — byte-identical to the -## shipped detector. +## The tracker stores NO enable flag: the CALLER passes `fix` to `detect` and +## guards its `note*` calls with its own knob. That is deliberate — an +## instance-level flag was silently false on the ModularBot construction path +## (the movers are built as object literals, not via `init*`), so the "fix" +## never ran live. The mover's module-global switch is now the single source of +## truth: `TR_FIRE_FIX` (default ON) for every mover, ANDed with +## `TR_STRAFE_FIRE_FIX` for STRAFE. With `fix=false`, `detect` is exactly +## `prev - energy` inside the caller's own window — byte-identical to shipped. +## +## ── Why the correction is applied with a one-SCAN delay ───────────────────── +## MEASURED LIVE (job j134, `TR_FIRE_DIAG`): the server emits the hit event on +## turn N but applies the energy change to the SHARED energy reading of turn +## N+1. Concretely, `EV hit getTurn=67` (dispatched at `bot.tick=66`, i.e. +## after that turn's movement) is followed by the `raw=-5.803` gain in the +## reading of `getTurn=68`. Because `note*` is called during `go()`, AFTER the +## same-tick `endScan`, a one-slot double buffer (`incoming` -> `pending`) makes +## the correction land on the reading that actually carries the change: +## * scan T : `detect` applies `pending`; `endScan` rotates incoming->pending +## * `go()` : `note*` adds to `incoming` +## * scan T+1 : `detect` applies 0 (event not yet rotated in) +## * scan T+2 : `detect` applies the event's correction — the reading whose +## `raw` shows the gain. Without this the correction lands one +## reading EARLY on a zero delta (a spurious wave) and the real +## delta is left uncorrected. +## +## (The j133 offline corpus model already applied the event to the row that +## carries its energy change; this makes the live path agree with it.) import std/math type FireTracker* = object - ## Per-enemy last-known energy plus the corrections accumulated from the - ## event stream since the previous scan. One instance per mover. + ## Per-enemy last-known energy plus the event corrections. One instance + ## per mover. prevEnergy*: seq[tuple[id: int, energy: float]] - hitBonusPending*: float ## 3 * power of enemy bullets that hit us - dealtPending*: float ## damage our bullets dealt to the enemy - enabled*: bool ## the j134 correction is active + hitBonusPending*: float ## applied to the CURRENT reading + dealtPending*: float + hitBonusIncoming: float ## noted since the last `endScan` + dealtIncoming: float splitWaves*: int ## waves emitted by splitting a too-large drop correctedTicks*: int ## readings whose drop was non-trivially corrected -proc initFireTracker*(enabled: bool): FireTracker = - FireTracker(prevEnergy: @[], enabled: enabled) +proc initFireTracker*(): FireTracker = + FireTracker(prevEnergy: @[]) proc reset*(t: var FireTracker) = ## Per-ROUND reset: drops the energy memory and any un-consumed event - ## correction. Keeps `enabled` (a module-knob, not round state). + ## correction. t.prevEnergy = @[] t.hitBonusPending = 0.0 t.dealtPending = 0.0 + t.hitBonusIncoming = 0.0 + t.dealtIncoming = 0.0 t.splitWaves = 0 t.correctedTicks = 0 @@ -73,32 +98,31 @@ proc prevEnergySet*(t: var FireTracker, id: int, energy: float) = t.prevEnergy.add((id: id, energy: energy)) proc noteEnemyBulletHit*(t: var FireTracker, power: float) = - ## `onHitByBullet` -> `e.bullet.power`. No-op when the fix is off. - if t.enabled: - t.hitBonusPending += 3.0 * power + ## `onHitByBullet` -> `e.bullet.power`. The caller gates this on its switch. + ## Staged for the scan AFTER next (see the header). + t.hitBonusIncoming += 3.0 * power proc noteDamageDealt*(t: var FireTracker, damage: float) = - ## `onBulletHit` -> `e.damage`. No-op when the fix is off. - if t.enabled: - t.dealtPending += damage + ## `onBulletHit` -> `e.damage`. The caller gates this on its switch. + t.dealtIncoming += damage proc detect*(t: var FireTracker, id: int, energy: float, - lo, hi: float): seq[float] = + lo, hi: float, fix: bool): seq[float] = ## Advance the tracker with one enemy energy reading and return the fire ## powers to spawn (empty = no fire). ## - ## `lo`/`hi` are the CALLER's shipped window, so with the fix off the result - ## is exactly the old `if drop >= lo and drop <= hi: @[drop]`. With the fix - ## on the delta is corrected for the two observable server effects and a + ## `lo`/`hi` are the CALLER's shipped window, so with `fix=false` the result + ## is exactly the old `if drop >= lo and drop <= hi: @[drop]`. With `fix` + ## the delta is corrected for the two observable server effects and a ## still-too-large delta is split rather than dropped. let prev = t.prevEnergyGet(id) let raw = prev - energy var drop = raw - if t.enabled: + if fix: drop += t.hitBonusPending - t.dealtPending if abs(drop - raw) > 1e-9: inc t.correctedTicks t.prevEnergySet(id, energy) - if t.enabled and drop > hi: + if fix and drop > hi: # NEVER silently drop a drop. A delta above the power cap is either several # fires folded into one reading or un-modelled contamination; some heat # beats none. Split into the fewest waves each <= 3.0. @@ -111,7 +135,12 @@ proc detect*(t: var FireTracker, id: int, energy: float, result = @[drop] proc endScan*(t: var FireTracker) = - ## Call once after the per-enemy scan: each event correction applies to - ## exactly one reading. - t.hitBonusPending = 0.0 - t.dealtPending = 0.0 + ## Call once after the per-enemy scan. Rotates the event corrections one + ## slot: the events noted since the previous `endScan` become the corrections + ## applied by the NEXT scan, and the ones applied by the current scan are + ## discarded. See the header for the measured one-turn server lag this + ## encodes. + t.hitBonusPending = t.hitBonusIncoming + t.dealtPending = t.dealtIncoming + t.hitBonusIncoming = 0.0 + t.dealtIncoming = 0.0 diff --git a/common_libs/movements/learned_surfer.nim b/common_libs/movements/learned_surfer.nim index ea6106e..76dd99a 100644 --- a/common_libs/movements/learned_surfer.nim +++ b/common_libs/movements/learned_surfer.nim @@ -312,7 +312,7 @@ proc resetRound*(m: var LearnedSurferModule) = proc initLearnedSurfer*(): LearnedSurferModule = result.debugGraphics = false - result.fire = initFireTracker(LearnedFireFix) + result.fire = initFireTracker() result.sbc = initCountedSbc(LS_NADE, LS_BINS, max(0, LearnedDecayEvery), max(0, LearnedDecayShift)) @@ -498,11 +498,13 @@ proc prevEnergySet(m: var LearnedSurferModule, id: int, energy: float) = proc noteEnemyBulletHit*(m: var LearnedSurferModule, power: float) = ## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off). - m.fire.noteEnemyBulletHit(power) + if LearnedFireFix: + m.fire.noteEnemyBulletHit(power) proc noteDamageDealt*(m: var LearnedSurferModule, damage: float) = ## `onBulletHit` -> our same-tick damage to the enemy (no-op when off). - m.fire.noteDamageDealt(damage) + if LearnedFireFix: + m.fire.noteDamageDealt(damage) proc detectFire(m: var LearnedSurferModule, id: int, ex, ey, eenergy: float, ws: WorldState) = @@ -510,7 +512,7 @@ proc detectFire(m: var LearnedSurferModule, id: int, ex, ey, eenergy: float, ## drop IS a wave; the state is the wave-relative state at THIS tick (the ## fire tick). Window 0.09..3.01, LEARNED's shipped window; the shared ## tracker corrects the delta and splits an over-cap drop across waves. - for drop in m.fire.detect(id, eenergy, 0.09, 3.01): + for drop in m.fire.detect(id, eenergy, 0.09, 3.01, LearnedFireFix): let botX = ws.selfX let botY = ws.selfY let bspeed = 20.0 - 3.0 * drop diff --git a/common_libs/movements/strafe.nim b/common_libs/movements/strafe.nim index d2f6c72..f6104a2 100644 --- a/common_libs/movements/strafe.nim +++ b/common_libs/movements/strafe.nim @@ -474,7 +474,7 @@ type lastTileCol, lastTileRow: int proc initStrafe*(): StrafeModule = - StrafeModule(debugGraphics: false, fire: initFireTracker(StrafeFireFix)) + StrafeModule(debugGraphics: false, fire: initFireTracker()) proc removeBulletNear*(m: var StrafeModule, x, y: float) = ## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead. @@ -592,7 +592,8 @@ proc noteEnemyBulletHit*(m: var StrafeModule, power: float) = ## "no fire"). ModularBot forwards `onHitByBullet`'s `e.bullet.power` here so ## `detectFires` can add the bonus back before classifying the delta. ## No-op when the fix is off (shipped detector preserved). - m.fire.noteEnemyBulletHit(power) + if StrafeFireFix: + m.fire.noteEnemyBulletHit(power) proc noteDamageDealt*(m: var StrafeModule, damage: float) = ## The mirror contamination: OUR bullet damaging the enemy this tick adds @@ -600,7 +601,8 @@ proc noteDamageDealt*(m: var StrafeModule, damage: float) = ## cap and get the enemy's OWN shot rejected by the shipped `<= 3.01` test. ## ModularBot forwards `onBulletHit`'s `e.damage` here. ## No-op when the fix is off. - m.fire.noteDamageDealt(damage) + if StrafeFireFix: + m.fire.noteDamageDealt(damage) proc detectFires(m: var StrafeModule, ws: WorldState) = ## The shared tracker does the delta correction + split; STRAFE supplies its @@ -612,7 +614,7 @@ proc detectFires(m: var StrafeModule, ws: WorldState) = " raw=", raw, " bonus=", m.fire.hitBonusPending, " dealt=", m.fire.dealtPending - for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01): + for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01, StrafeFireFix): m.spawnTrackedWave(ws, ei, p) m.fire.endScan() diff --git a/common_libs/movements/the_floor_is_lava.nim b/common_libs/movements/the_floor_is_lava.nim index 26c335a..5f08869 100644 --- a/common_libs/movements/the_floor_is_lava.nim +++ b/common_libs/movements/the_floor_is_lava.nim @@ -263,7 +263,7 @@ type lastPickCall: int ## callCount at the last pick (log only) picks: int ## number of picks this round (log only) -proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false, fire: initFireTracker(TfilFireFix)) +proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false, fire: initFireTracker()) proc removeBulletNear*(m: var TFILModule, x, y: float) = ## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead. @@ -371,17 +371,19 @@ proc spawnTrackedWave(m: var TFILModule, ws: WorldState, ei: EnemyInfo, proc noteEnemyBulletHit*(m: var TFILModule, power: float) = ## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off). - m.fire.noteEnemyBulletHit(power) + if TfilFireFix: + m.fire.noteEnemyBulletHit(power) proc noteDamageDealt*(m: var TFILModule, damage: float) = ## `onBulletHit` -> our same-tick damage to the enemy (no-op when off). - m.fire.noteDamageDealt(damage) + if TfilFireFix: + m.fire.noteDamageDealt(damage) proc detectFires(m: var TFILModule, ws: WorldState) = ## Check all enemies for energy drops; spawn a tracked bullet per confirmed ## fire. The shared tracker corrects the delta and splits over-cap drops. for ei in ws.enemies: - for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01): + for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01, TfilFireFix): m.spawnTrackedWave(ws, ei, p) m.fire.endScan() diff --git a/common_libs/movements/the_floor_is_lava_ring.nim b/common_libs/movements/the_floor_is_lava_ring.nim index f0fb279..2b88ded 100644 --- a/common_libs/movements/the_floor_is_lava_ring.nim +++ b/common_libs/movements/the_floor_is_lava_ring.nim @@ -255,7 +255,7 @@ type proc initTFILRing*(): TFILRingModule = TFILRingModule(debugGraphics: false, band: (lo: RangeLo, hi: RangeHi), - fire: initFireTracker(TfilRingFireFix)) + fire: initFireTracker()) proc removeBulletNear*(m: var TFILRingModule, x, y: float) = ## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead. @@ -329,17 +329,19 @@ proc spawnTrackedWave(m: var TFILRingModule, ws: WorldState, ei: EnemyInfo, proc noteEnemyBulletHit*(m: var TFILRingModule, power: float) = ## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off). - m.fire.noteEnemyBulletHit(power) + if TfilRingFireFix: + m.fire.noteEnemyBulletHit(power) proc noteDamageDealt*(m: var TFILRingModule, damage: float) = ## `onBulletHit` -> our same-tick damage to the enemy (no-op when off). - m.fire.noteDamageDealt(damage) + if TfilRingFireFix: + m.fire.noteDamageDealt(damage) proc detectFires(m: var TFILRingModule, ws: WorldState) = ## Check all enemies for energy drops; spawn a tracked bullet per confirmed ## fire. The shared tracker corrects the delta and splits over-cap drops. for ei in ws.enemies: - for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01): + for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01, TfilRingFireFix): m.spawnTrackedWave(ws, ei, p) m.fire.endScan() diff --git a/common_libs/movements/wave_surfer.nim b/common_libs/movements/wave_surfer.nim index 2d72df1..e18e78c 100644 --- a/common_libs/movements/wave_surfer.nim +++ b/common_libs/movements/wave_surfer.nim @@ -129,7 +129,7 @@ proc resetRound*(m: var WaveSurferModule) = for i in 0.. the shooter's `3*power` bonus (no-op when off). - m.fire.noteEnemyBulletHit(power) + if SurfFireFix: + m.fire.noteEnemyBulletHit(power) proc noteDamageDealt*(m: var WaveSurferModule, damage: float) = ## `onBulletHit` -> our same-tick damage to the enemy (no-op when off). - m.fire.noteDamageDealt(damage) + if SurfFireFix: + m.fire.noteDamageDealt(damage) proc gfToBin(gf: float64): int {.inline.} = clamp(int(round((gf.clamp(-1.0, 1.0) + 1.0) * 0.5 * float64(WS_BINS - 1))), @@ -191,7 +193,7 @@ proc detectFire(m: var WaveSurferModule, id: int, ex, ey, eenergy, ## (corrected) one-tick drop. `drop` IS the firepower. Window 0.1..3.0, ## SURF's shipped window; the shared tracker corrects the delta and splits an ## over-cap drop across several waves. - for power in m.fire.detect(id, eenergy, 0.1, 3.0): + for power in m.fire.detect(id, eenergy, 0.1, 3.0, SurfFireFix): let bspeed = 20.0 - 3.0 * power let bearing = arctan2(botY - ey, botX - ex) let d = hypot(botX - ex, botY - ey) diff --git a/common_libs/tests/test_strafe_fire_fix.nim b/common_libs/tests/test_strafe_fire_fix.nim index 337ef49..3204fd3 100644 --- a/common_libs/tests/test_strafe_fire_fix.nim +++ b/common_libs/tests/test_strafe_fire_fix.nim @@ -1,4 +1,4 @@ -## Unit checks for the j133 STRAFE fire-detection fix (`TR_STRAFE_FIRE_FIX`). +## Unit checks for the j133/j134 fire-detection fix (`TR_STRAFE_FIRE_FIX`). ## ## NO battle, NO Java, NO server. Run with: ## nim c -r --nimcache:/tmp/nc_j133 --path:common_libs \ @@ -17,6 +17,16 @@ ## The fix undoes both from the events (`noteEnemyBulletHit` / `noteDamageDealt`) ## and splits any still-too-large delta instead of dropping it. These checks pin ## the four behaviours plus the OFF-switch parity with the shipped detector. +## +## ── The one-SCAN delay (j134, MEASURED LIVE) ──────────────────────────────── +## The correction is NOT applied to the immediately-next reading. The server +## emits the hit event on turn N but applies the energy change to turn N+1's +## reading (live evidence: `EV hit getTurn=67` -> the `raw=-5.803` gain appears +## in the `getTurn=68` reading). Because `note*` runs during `go()`, AFTER that +## turn's `endScan`, the tracker keeps a one-slot double buffer so the +## correction lands on the reading that carries the change. A unit test must +## therefore feed: (1) a scan to rotate the event in, then (2) the reading whose +## raw shows the effect. `advance` below does exactly that. import std/[math, os] import gun_harness/gun_interface @@ -44,13 +54,10 @@ proc freshModule(): StrafeModule = # ── 1. plain fire (unchanged behaviour) ─────────────────────────────────────── block: - let m = freshModule() - var mm = m + var mm = freshModule() mm.detectFires(ws(100.0)) - mm.detectFires(ws(97.95)) # fired 2.05? use a clean power - # second reading: 100 -> 97.95 is a 2.05 drop, in range -> one wave - check "plain fire creates exactly one wave", - mm.bullets.len == 1 + mm.detectFires(ws(97.95)) # 2.05 drop, in range -> one wave + check "plain fire creates exactly one wave", mm.bullets.len == 1 check "plain fire wave power == drop", mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.05) < 1e-9 @@ -58,12 +65,12 @@ block: block: # Enemy at 100 fires 0.35 while an EARLIER 0.45 bullet of its own hits us # (+3*0.45 = +1.35). Net energy = 100 - 0.35 + 1.35 = 101.0 -> raw drop = -1.0. - let m = freshModule() - var mm = m - mm.noteEnemyBulletHit(0.45) - mm.detectFires(ws(101.0)) - check "masked fire: WITH fix a wave is created", - mm.bullets.len == 1 + var mm = freshModule() + mm.detectFires(ws(100.0)) # establish prev = 100 + mm.noteEnemyBulletHit(0.45) # event noted (staged) + mm.detectFires(ws(100.0)) # rotates the staged correction in + mm.detectFires(ws(101.0)) # the reading carrying the change + check "masked fire: WITH fix a wave is created", mm.bullets.len == 1 check "masked fire: WITH fix the power is recovered", mm.bullets.len == 1 and abs(mm.bullets[0].power - 0.35) < 1e-9 @@ -71,24 +78,21 @@ block: block: # Enemy at 50 fires 2.0 and our bullet hits it for 1.8 -> energy 46.2, # raw drop 3.8 > 3.01 (shipped detector rejects it outright). - let m = freshModule() - var mm = m - mm.prevEnergySet(1, 50.0) # last reading before the contested tick + var mm = freshModule() + mm.prevEnergySet(1, 50.0) mm.noteDamageDealt(1.8) - mm.detectFires(ws(46.2)) - check "inflated drop: WITH fix a wave is created", - mm.bullets.len == 1 + mm.detectFires(ws(50.0)) # rotates the staged correction in + mm.detectFires(ws(46.2)) # raw 3.8 - 1.8 = 2.0 -> wave + check "inflated drop: WITH fix a wave is created", mm.bullets.len == 1 check "inflated drop: WITH fix the power is recovered", mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.0) < 1e-9 # ── 4. never silently drop a too-large delta (unobserved contamination) ─────── block: - let m = freshModule() - var mm = m + var mm = freshModule() mm.detectFires(ws(100.0)) mm.detectFires(ws(93.0)) # 7.0 with no events -> split, not dropped - check "too-large drop is SPLIT, never dropped", - mm.bullets.len == 3 + check "too-large drop is SPLIT, never dropped", mm.bullets.len == 3 check "split waves all within the power cap", mm.bullets.len == 3 and mm.bullets[0].power <= 3.0 and mm.bullets[1].power <= 3.0 check "split waves sum back to the drop", @@ -98,15 +102,18 @@ block: block: putEnv("TR_STRAFE_FIRE_FIX", "0") loadStrafeEnv() - # (a) masked fire: shipped detector sees drop = -1.0 -> NO wave. + # (a) masked fire: shipped detector sees raw = -1.0 -> NO wave. var a = freshModule() + a.detectFires(ws(100.0)) a.noteEnemyBulletHit(0.45) # must be a no-op when the fix is off + a.detectFires(ws(100.0)) a.detectFires(ws(101.0)) check "OFF: masked fire is NOT detected (shipped behaviour)", a.bullets.len == 0 # (b) inflated drop: shipped detector sees 3.8 -> rejected -> NO wave. var b = freshModule() b.prevEnergySet(1, 50.0) b.noteDamageDealt(1.8) + b.detectFires(ws(50.0)) b.detectFires(ws(46.2)) check "OFF: inflated drop is rejected (shipped behaviour)", b.bullets.len == 0 # (c) in-range drop is still caught exactly once. @@ -121,9 +128,18 @@ block: # ── 6. default is ON ───────────────────────────────────────────────────────── block: delEnv("TR_STRAFE_FIRE_FIX") + delEnv("TR_FIRE_FIX") loadStrafeEnv() check "default TR_STRAFE_FIRE_FIX is ON", StrafeFireFix +# ── 7. the global TR_FIRE_FIX also gates STRAFE ────────────────────────────── +block: + putEnv("TR_FIRE_FIX", "0") + loadStrafeEnv() + check "TR_FIRE_FIX=0 disables STRAFE too", not StrafeFireFix + delEnv("TR_FIRE_FIX") + loadStrafeEnv() + if failures == 0: echo "\nAll STRAFE fire-detection fix checks passed." else: