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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
## 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
|
||||
Reference in New Issue
Block a user