Files
SirRoboGarage/common_libs/movement_harness/bullet_shadows.nim
T
SirStone 651ce80620 feat(ModularBot): KNN gun, gunheat tracker, bullet shadows — inspired by DrussGT
New DrussGT-inspired modules:
- KNNGun: K-nearest-neighbor statistical targeting using GF density peaks
- GunheatTracker: dual-heat system (predicted + confirmed) for 1-2 tick lead
- ShadowTracker: computes GF regions safe from in-flight bullets (enemy wave dodge)

VirtualBodyTracker now integrates gunheat for earlier fire detection and shadows
for safe-zone multiplier (90% reduction in danger zones).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 11:58:57 +02:00

122 lines
4.8 KiB
Nim

## Bullet shadow tracker — computes GF regions guaranteed safe because
## our in-flight bullets would intercept an enemy bullet traveling there.
##
## Geometry: 0° = East, X = East, Y = North (Tank Royale).
## Algorithm mirrors DrussGT EnemyWave.logShadow: simulate each of our bullets
## forward tick-by-tick, find where it intersects the expanding enemy wave ring,
## convert intersection points to GF values.
import std/math
import gun_harness/gun_interface
const
MaxBullets* = 32 ## slots; ponytail: simple array, 1 bullet/tick max
type
MyBullet* = object
x*, y*: float
headingRad*: float
speed*: float
alive*: bool
ShadowTracker* = object
bullets*: array[MaxBullets, MyBullet]
numSlots: int ## high-water mark
BulletShadow* = object
## GF range [gfLow, gfHigh] shadowed by one of our bullets for a wave.
gfLow*, gfHigh*: float
# ── bullet lifecycle ──────────────────────────────────────────────────────────
proc addBullet*(st: var ShadowTracker, x, y, headingRad, power: float) =
for i in 0..<MaxBullets:
if not st.bullets[i].alive:
st.bullets[i] = MyBullet(x: x, y: y, headingRad: headingRad,
speed: bulletSpeed(power), alive: true)
if i >= st.numSlots: st.numSlots = i + 1
return
proc removeBullet*(st: var ShadowTracker, idx: int) {.inline.} =
if idx >= 0 and idx < MaxBullets:
st.bullets[idx].alive = false
proc removeBulletNear*(st: var ShadowTracker, x, y: float) =
## Kill the live bullet slot closest to (x, y). Used when a hit event fires
## with the bullet's last known position.
var bestIdx = -1
var bestDist = 1e18
for i in 0..<st.numSlots:
if not st.bullets[i].alive: continue
let d = hypot(st.bullets[i].x - x, st.bullets[i].y - y)
if d < bestDist:
bestDist = d
bestIdx = i
if bestIdx >= 0:
st.bullets[bestIdx].alive = false
proc tick*(st: var ShadowTracker) =
## Advance all live bullets one tick (call once per game tick).
for i in 0..<st.numSlots:
if st.bullets[i].alive:
st.bullets[i].x += st.bullets[i].speed * cos(st.bullets[i].headingRad)
st.bullets[i].y += st.bullets[i].speed * sin(st.bullets[i].headingRad)
# ── shadow computation ────────────────────────────────────────────────────────
proc getShadows*(st: ShadowTracker,
waveFireX, waveFireY: float,
waveBearingRad: float, ## bearing from enemy to us at fire time
waveRadius: float, ## current radius of the wave (px)
waveSpeed: float): seq[BulletShadow] =
## For each live bullet, simulate it forward against the expanding wave ring.
## Returns GF ranges [gfLow, gfHigh] that are shadowed.
##
## Caller supplies wave parameters directly to avoid coupling to VBWave.
let maxEA = arcsin(min(8.0 / waveSpeed, 1.0))
if maxEA < 1e-9: return
for bi in 0..<st.numSlots:
let b = st.bullets[bi]
if not b.alive: continue
let dx = b.speed * cos(b.headingRad)
let dy = b.speed * sin(b.headingRad)
var prevDist = hypot(b.x - waveFireX, b.y - waveFireY)
# ponytail: 300-tick horizon covers arena diagonal / min bullet speed
for step in 1..300:
let bx = b.x + float(step) * dx
let by = b.y + float(step) * dy
let curDist = hypot(bx - waveFireX, by - waveFireY)
let waveAt = waveRadius + float(step - 1) * waveSpeed
let waveNext = waveRadius + float(step) * waveSpeed
# Bullet crossed the ring: was outside at step-1, inside at step, and approaching
if curDist < waveNext and prevDist > waveAt and curDist < prevDist:
# Angular half-width of bot at crossing distance
let ringR = (waveAt + waveNext) * 0.5
let angHalf = arctan(BotRadius / max(ringR, 1.0))
# Center angle of intersection point (absolute bearing from wave origin)
let centerAngle = arctan2(by - waveFireY, bx - waveFireX)
# Convert to GF
var off = centerAngle - waveBearingRad
while off > PI: off -= 2.0 * PI
while off < -PI: off += 2.0 * PI
let gfCenter = clamp(off / maxEA, -1.0, 1.0)
let gfHalf = angHalf / maxEA
result.add BulletShadow(
gfLow: clamp(gfCenter - gfHalf, -1.0, 1.0),
gfHigh: clamp(gfCenter + gfHalf, -1.0, 1.0),
)
break # one shadow per bullet per wave
if curDist > prevDist: break # bullet diverging — no future crossing
prevDist = curDist
proc isShadowed*(shadows: openArray[BulletShadow], gf: float): bool {.inline.} =
for s in shadows:
if gf >= s.gfLow and gf <= s.gfHigh: return true
false