651ce80620
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>
122 lines
4.8 KiB
Nim
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
|