diff --git a/ModularBot_garage/.env.example b/ModularBot_garage/.env.example index e07eb1e..81faf6e 100644 --- a/ModularBot_garage/.env.example +++ b/ModularBot_garage/.env.example @@ -105,6 +105,7 @@ TR_TFIL_RANGE_HI=200.0 # px; upper edge of that band TR_TFIL_RANGE_TEMP=0.4 # sharpness of the ring mover's weighted random draw TR_TFIL_RANGE_K=60.0 # px; how fast the weight falls off outside the band TR_TFIL_CORRIDOR_HEAT=10.0 # lava painted per corridor-overlapping tile +TR_TFIL_CORRIDOR_TICKS=0.0 # corridor length in ticks: 0 = to the wall (shipped); N>0 = min(to wall, bullet speed * N) TR_TFIL_WALL_HOTNESS=15.0 # peak heat painted on tiles next to a wall TR_TFIL_WALL_RADIANCE=10.0 # how fast wall heat falls off with distance TR_TFIL_BULLET_CORE=10.0 # tfil only: lava per bullet-overlapping tile (== PathDangerThreshold, so a bullet is never hot on its own) @@ -147,6 +148,7 @@ TR_STRAFE_HEAT_GRID=on # draw the whole heat grid; 0 leaves only the chosen til TR_STRAFE_BULLET_CORE=20.0 # strafe's own retune: lava per bullet-overlapping tile TR_STRAFE_BULLET_AURA=10.0 # strafe's own retune: lava for the bullet aura ring TR_STRAFE_CORRIDOR_HEAT=10.0 # strafe's own retune: lava per corridor tile +TR_STRAFE_CORRIDOR_TICKS=0.0 # same length bound for strafe: 0 = to the wall (shipped) TR_STRAFE_WALL_HOTNESS=15.0 # strafe's own retune: peak wall heat TR_STRAFE_WALL_RADIANCE=5.0 # strafe's own retune: wall heat falloff diff --git a/ModularBot_garage/src/env_report.nim b/ModularBot_garage/src/env_report.nim index 63778d9..57bbdd0 100644 --- a/ModularBot_garage/src/env_report.nim +++ b/ModularBot_garage/src/env_report.nim @@ -311,6 +311,8 @@ proc printEffectiveValues(ctx: EnvReportContext) = emit("TR_TFIL_RANGE_K", $RangeK, sourceOf("TR_TFIL_RANGE_K")) emit("TR_TFIL_CORRIDOR_HEAT", $the_floor_is_lava_ring.CorridorHeat, sourceOf("TR_TFIL_CORRIDOR_HEAT")) + emit("TR_TFIL_CORRIDOR_TICKS", $the_floor_is_lava.TfilCorridorTicks, + sourceOf("TR_TFIL_CORRIDOR_TICKS")) emit("TR_TFIL_WALL_HOTNESS", $the_floor_is_lava_ring.WallHotness, sourceOf("TR_TFIL_WALL_HOTNESS")) emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance, @@ -373,6 +375,7 @@ proc printEffectiveValues(ctx: EnvReportContext) = emit("TR_STRAFE_BULLET_CORE", $StrafeBulletCore, sourceOf("TR_STRAFE_BULLET_CORE")) emit("TR_STRAFE_BULLET_AURA", $StrafeBulletAura, sourceOf("TR_STRAFE_BULLET_AURA")) emit("TR_STRAFE_CORRIDOR_HEAT", $StrafeCorridorHeat, sourceOf("TR_STRAFE_CORRIDOR_HEAT")) + emit("TR_STRAFE_CORRIDOR_TICKS", $StrafeCorridorTicks, sourceOf("TR_STRAFE_CORRIDOR_TICKS")) emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS")) emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE")) @@ -646,6 +649,7 @@ proc knownEnvNames*(): seq[string] = "TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG", "TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP", "TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS", + "TR_TFIL_CORRIDOR_TICKS", "TR_TFIL_WALL_RADIANCE", "TR_TFIL_BULLET_CORE", "TR_TFIL_BULLET_AURA", "TR_TFIL_TILE_REPLAN", "TR_TFIL_COMMIT_TICKS", "TR_TFIL_NO_REV", "TR_TFIL_COMMIT_LOG", "TR_TFIL_COMMIT_ARRIVAL", "TR_TFIL_COMMIT_MARGIN", @@ -664,6 +668,7 @@ proc knownEnvNames*(): seq[string] = "TR_FIRE_LAG", "TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA", "TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS", + "TR_STRAFE_CORRIDOR_TICKS", "TR_STRAFE_WALL_RADIANCE", SurfPrefDistEnv, SurfDistBandEnv, SurfWallMarginEnv, SurfRadialFracEnv, SurfLogEnv, diff --git a/common_libs/movements/strafe.nim b/common_libs/movements/strafe.nim index aab0766..917d220 100644 --- a/common_libs/movements/strafe.nim +++ b/common_libs/movements/strafe.nim @@ -226,6 +226,13 @@ const CorridorHeatDefault = 10.0 ## corridor heat (== PathDangerThreshold) const WallHotnessDefault = 15.0 ## wall radiance peak (retune) const WallRadianceDefault = 5.0 ## wall radiance falloff (retune) +## j148: the corridor LENGTH bound, `TR_STRAFE_CORRIDOR_TICKS`. The shipped +## corridor runs from the bullet to the ARENA WALL; a bullet only covers +## `speed * t` px in `t` ticks, so a fixed TIME window is the physical length. +## 0 (the default) = to the wall, byte-for-byte shipped. +const DefaultStrafeCorridorTicks = 0.0 +var StrafeCorridorTicks* = DefaultStrafeCorridorTicks + ## Heat shape is override-able so the shipped field and the retune can be ## compared on one binary. The DEFAULTS are the retune (see the block above); ## the knobs are strafe-specific, so the ring mover's own `TR_TFIL_*` names do @@ -374,6 +381,15 @@ proc loadStrafeHeatEnv*() = StrafeCorridorHeat = getEnvFloat("TR_STRAFE_CORRIDOR_HEAT", CorridorHeatDefault) StrafeWallHotness = getEnvFloat("TR_STRAFE_WALL_HOTNESS", WallHotnessDefault) StrafeWallRadiance = getEnvFloat("TR_STRAFE_WALL_RADIANCE", WallRadianceDefault) + StrafeCorridorTicks = getEnvFloat("TR_STRAFE_CORRIDOR_TICKS", DefaultStrafeCorridorTicks) + +proc strafeCorridorReach*(tWall, speed: float): float = + ## The ONE place the strafe corridor length is decided (same rule as tfil's + ## `corridorReach`, same physics: a bullet covers `speed * t` px in `t` ticks). + ## 0 (default) = to the arena wall, byte-for-byte shipped; N > 0 = N ticks. + ## Only the LENGTH changes: the heat inside the surviving corridor is today's + ## (`heatDecay(along / speed)` untouched) — this is NOT the j119 time-heat model. + if StrafeCorridorTicks <= 0.0: tWall else: min(tWall, speed * StrafeCorridorTicks) proc loadStrafeEnv*() = ## Read the strafe knobs. Called once at module init; callable again after @@ -700,6 +716,7 @@ proc buildHeat(m: var StrafeModule, ws: WorldState) = elif dx < 0.0: tMin = min(tMin, (0.0 - b.x) / dx) if dy > 0.0: tMin = min(tMin, (m.arenaHeight - b.y) / dy) elif dy < 0.0: tMin = min(tMin, (0.0 - b.y) / dy) + tMin = strafeCorridorReach(tMin, speed) if tMin == 0.0: continue let (_, auraR) = bulletRadii(b.power) let bMag = bulletMagScale(b.power) diff --git a/common_libs/movements/the_floor_is_lava.nim b/common_libs/movements/the_floor_is_lava.nim index 2a1a2b5..f82166f 100644 --- a/common_libs/movements/the_floor_is_lava.nim +++ b/common_libs/movements/the_floor_is_lava.nim @@ -269,6 +269,31 @@ proc loadTfilHeatEnv*() = loadTfilHeatEnv() +# ── Corridor LENGTH bound (TR_TFIL_CORRIDOR_TICKS, default 0 = to the wall) ── +# +# WHY: the shipped corridor is the rotated rectangle from the bullet to the +# ARENA WALL, so one distant bullet blankets a 40px-wide swath across the whole +# map. That is not physical: in `t` ticks a bullet covers `speed * t` px, and +# `speed = 20 - 3*power`, so a fast (low-power) bullet's reach is LONG and a +# slow one's is SHORT. +# +# WHAT: bound the corridor's LENGTH, nothing else — the heat inside the +# surviving corridor is EXACTLY today's (`heatDecay(along/speed)` is unchanged), +# so unlike the j119 time-indexed heat model this does NOT decay heat along the +# corridor; it only removes corridor that no bullet will reach. +var TfilCorridorTicks* = 0.0 ## 0 (default) = to the wall: byte-for-byte shipped + +proc loadTfilCorridorEnv*() = + TfilCorridorTicks = getEnvFloat("TR_TFIL_CORRIDOR_TICKS", 0.0) + +loadTfilCorridorEnv() + +proc corridorReach*(tWall, speed: float): float = + ## The ONE place the corridor length is decided, so the heat field and the + ## drawn outline can never disagree. `tWall` = distance to the wall along the + ## heading, `speed` = the ghost's own px/tick. 0 = to the wall (shipped). + if TfilCorridorTicks <= 0.0: tWall else: min(tWall, speed * TfilCorridorTicks) + proc heatDecay*(dt: float): float = ## Fraction of a bullet's heat still present `dt` ticks before it arrives. ## Exactly 1.0 when the time model is off, so the default field is @@ -506,6 +531,9 @@ type CorridorGeom = object bx, by: float ## bullet origin proc corridorGeom(b: TrackedBullet, arenaWidth, arenaHeight: float): CorridorGeom = + ## `tMin` is the corridor LENGTH: the distance to the wall, bounded by + ## `corridorReach` (TR_TFIL_CORRIDOR_TICKS). Unset (0) -> exactly the wall + ## distance, so the shipped field and outline are byte-for-byte unchanged. let speed = sqrt(b.velX * b.velX + b.velY * b.velY) if speed < 0.001: return let dx = b.velX / speed @@ -515,6 +543,7 @@ proc corridorGeom(b: TrackedBullet, arenaWidth, arenaHeight: float): CorridorGeo elif dx < 0.0: tMin = min(tMin, (0.0 - b.x) / dx) if dy > 0.0: tMin = min(tMin, (arenaHeight - b.y) / dy) elif dy < 0.0: tMin = min(tMin, (0.0 - b.y) / dy) + tMin = corridorReach(tMin, speed) CorridorGeom(dx: dx, dy: dy, px: -dy, py: dx, tMin: tMin, bx: b.x, by: b.y) proc lavaAt(m: TFILModule, col, row: int): float = diff --git a/docs/movement_campaign.md b/docs/movement_campaign.md index 5715700..4dd6bdf 100644 --- a/docs/movement_campaign.md +++ b/docs/movement_campaign.md @@ -3660,3 +3660,12 @@ isolates the knob):** the known strafe-over-tfil gap reproducing itself, and it is exactly why the pre-registration demanded the within-mover reference: read against `tfil_off` alone, the knob looks like a winner it is not. + +**j148 — corridor LENGTH bound (unmeasured).** `TR_TFIL_CORRIDOR_TICKS` and +`TR_STRAFE_CORRIDOR_TICKS` (both default `0`) bound the corridor — the rotated +rectangle from the ghost bullet along its heading — by `min(distance to the wall, +bulletSpeed * TICKS)`, so a fast (low-power) bullet's corridor is long and a +slow one's is short, instead of every bullet blanketing the arena to the wall. +`0` is exactly today's behaviour; only the LENGTH changes, the heat inside the +surviving corridor is untouched. **Untested** — no battle, no measurement, the +parity guard only says the default path is byte-for-byte unchanged.