Files
SirRoboGarage/common_libs/guns/stop_shot.nim
T
SirStone 2cc2a3bd87 fix(ModularBot): ram loop prevention, dead-target guards, cleaner logging
- 30-tick cooldown after ghost-stuck/timeout ram exit prevents re-entry loop
- enemy_tracker.update() skips dead bots to prevent same-tick scan resurrection
- TFIL graphics cleared when ramming is active movement
- [config] logs: white base with green-highlighted changes only
- [ram:enter] logs trigger reason and key values on false→true transition
- [death] and [target-invalid] logs retained for diagnostics
2026-09-20 20:44:45 +02:00

102 lines
3.4 KiB
Nim

## Stop-shot gun: predicts the point where a decelerating enemy will stop.
## Many bots pause briefly when reversing direction — catches them at the stop.
## Falls back to linear prediction when enemy is not decelerating.
## Coordinate system: 0° = East, CCW positive (Tank Royale standard).
import std/math
import gun_harness/gun_interface
# TR deceleration constants (px/tick²)
const BrakeDecel = 2.0 ## same-direction stop
const CoastDecel = 1.0 ## cross-zero coasting stop
type StopShotGun* = object
prevSpeed: float
prevHeading: float
prevTick: int
frames: int
cachedTick: int
cachedPredX: float
cachedPredY: float
debugGraphics*: bool
proc initStopShotGun*(): StopShotGun = StopShotGun(debugGraphics: false)
proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction =
if bulletSpeed <= 0.0:
return GunPrediction(x: state.enemyX, y: state.enemyY)
# Per-tick cache: all power bins share one prediction
if state.tick == g.cachedTick:
return GunPrediction(x: g.cachedPredX, y: g.cachedPredY)
defer:
g.cachedTick = state.tick
g.cachedPredX = result.x
g.cachedPredY = result.y
# Update history
let isNew = state.tick > g.prevTick
if isNew:
g.prevSpeed = state.enemySpeed
g.prevHeading = state.enemyHeading
g.prevTick = state.tick
inc g.frames
# Need 2+ frames to detect deceleration
if g.frames < 2:
result = GunPrediction(x: state.enemyX, y: state.enemyY)
return
let speed = state.enemySpeed
let prev = g.prevSpeed
# Detect deceleration: |speed| is shrinking
let decelerating = abs(speed) < abs(prev) and abs(speed) > 0.01
if not decelerating:
# Linear fallback
let headRad = degToRad(state.enemyHeading)
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
let t = dist / bulletSpeed
var px = state.enemyX + cos(headRad) * speed * t
var py = state.enemyY + sin(headRad) * speed * t
result = GunPrediction(
x: clamp(px, 0.0, state.arenaWidth),
y: clamp(py, 0.0, state.arenaHeight)
)
return
# Pick decel rate: braking (speed toward zero on same sign) = 2, else 1
# ponytail: simplified; TR has exact rules per direction but this is close enough
let decel = if speed * prev > 0.0: BrakeDecel else: CoastDecel
# Simulate stop position
let headRad = degToRad(state.enemyHeading)
var v = speed
var sx = state.enemyX
var sy = state.enemyY
while abs(v) > 0.001:
sx += v * cos(headRad)
sy += v * sin(headRad)
let step = if v > 0.0: -decel else: decel
v += step
if (v > 0.0) != (speed > 0.0): v = 0.0 # crossed zero
# Bullet travel time to current pos, check against ticks to stop
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
let bulletTicks = dist / bulletSpeed
let stopTicks = abs(speed) / decel
# If bullet arrives well after stop, aim at stop; otherwise linear blend
let px = if bulletTicks >= stopTicks: sx else: state.enemyX + cos(headRad) * speed * bulletTicks
let py = if bulletTicks >= stopTicks: sy else: state.enemyY + sin(headRad) * speed * bulletTicks
result = GunPrediction(
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
y: clamp(py, BotRadius, state.arenaHeight - BotRadius)
)
proc onResult*(g: var StopShotGun, e: FeedbackEvent) =
discard # analytical gun — no learning