## Gunheat wave prediction — mirrors DrussGT's enemyGunHeat / imaginaryGunHeat pattern. ## Tracks two heat values: ## confirmedHeat — reset from actual energy-drop fire detections ## predictedHeat — reset speculatively when enemy COULD fire (heat near 0) ## This lets us create a "predicted" wave 1-2 ticks before energy drop confirms it. ## ## Gun cooling rate in Tank Royale: 0.1/tick. ## Initial gun heat at round start: 3.0 → first possible fire at tick 30. import std/math import gun_harness/gun_interface const GunCoolingRate* = 0.1 InitialGunHeat* = 3.0 type WaveEventKind* = enum wePredicted ## enemy CAN fire this tick (heat just reached 0) weConfirmed ## energy drop confirmed a fire last tick WaveEvent* = object kind*: WaveEventKind fireX*, fireY*: float ## enemy position at (predicted/confirmed) fire time bulletPower*: float tick*: int GunheatTracker* = object confirmedHeat*: float ## heat from last confirmed fire (energy drop) predictedHeat*: float ## heat from last predicted fire (imaginary wave) prevEnemyEnergy*: float canFireTick*: int ## tick when enemy next confirmed able to fire proc initGunheatTracker*(): GunheatTracker = result.confirmedHeat = InitialGunHeat result.predictedHeat = InitialGunHeat result.prevEnemyEnergy = 100.0 result.canFireTick = -1 proc resetRound*(gt: var GunheatTracker) = gt.confirmedHeat = InitialGunHeat gt.predictedHeat = InitialGunHeat gt.prevEnemyEnergy = 100.0 gt.canFireTick = -1 proc tick*(gt: var GunheatTracker, state: WorldState): seq[WaveEvent] = ## Call once per tick with current world state. ## Returns any wave events created this tick (0, 1, or 2 entries). # Cool both heat values gt.confirmedHeat = max(0.0, gt.confirmedHeat - GunCoolingRate) gt.predictedHeat = max(gt.confirmedHeat, gt.predictedHeat - GunCoolingRate) # Check energy drop for confirmed fire (fired last tick) let drop = gt.prevEnemyEnergy - state.enemyEnergy gt.prevEnemyEnergy = state.enemyEnergy if drop >= 0.1 and drop <= 3.0 and gt.confirmedHeat == 0.0: let power = drop # ponytail: subtract one coolingRate because they fired last tick, not this tick gt.confirmedHeat = 1.0 + power / 5.0 - GunCoolingRate gt.predictedHeat = gt.confirmedHeat gt.canFireTick = state.tick result.add WaveEvent( kind: weConfirmed, fireX: state.enemyX, fireY: state.enemyY, bulletPower: power, tick: state.tick, ) # Predicted wave: confirmedHeat just reached 0 AND no confirmed fire this tick elif gt.confirmedHeat == 0.0 and gt.predictedHeat == 0.0: # Enemy CAN fire. Guess power = 2.0 (sensible default, caller can override). # ponytail: flat prior on power; upgrade to BulletPowerPredictor if needed let guessedPower = 2.0 gt.predictedHeat = 1.0 + guessedPower / 5.0 + GunCoolingRate # +cooling: fire next tick gt.canFireTick = state.tick result.add WaveEvent( kind: wePredicted, fireX: state.enemyX, fireY: state.enemyY, bulletPower: guessedPower, tick: state.tick, )