TRXTD/scripts/sim.mts
Tronax 4f8400c480
feat: add 9-level tower evolution system with distinct tier abilities
Extend all 5 towers to 9 upgrade levels, divided into 3 visual and
functional evolution tiers:
- Tier 1 (L1-3, Yellow): Base stats (damage, range, rate).
- Tier 2 (L4-6, Orange): First major ability evolution with pulsing orange aura.
- Tier 3 (L7-9, Blue): Final devastating evolution with blue aura.

Evolutions per tower:
- Arrow Tower:
  * L4-6 (Orange): Multishot firing 2-3 arrows simultaneously at distinct targets.
  * L7-9 (Blue): 4-arrow multishot + Poison DoT (up to 28 dmg/s).
- Cannon:
  * L4-6 (Orange): Incendiary shells applying Burn DoT in splash radius.
  * L7-9 (Blue): Flak shells hitting flying units with full splash + burn damage.
- Frost Tower:
  * L4-6 (Orange): Permafrost - every 3rd pulse completely freezes enemies (speed = 0).
  * L7-9 (Blue): Shatter - frozen enemies receive +50% damage from all sources.
- Tesla Tower:
  * L4-6 (Orange): Chain lightning applies stun on each hit.
  * L7-9 (Blue): Lightning Storm - every 4th shot strikes and stuns ALL enemies in range.
- Laser Tower:
  * L4-6 (Orange): Prism refraction splitting the beam onto 2-3 secondary targets.
  * L7-9 (Blue): Piercing beam penetrating all enemies in a line up to 280px range.

Engine & Renderer updates:
- Added DoT tick handling (poison/burn) and freeze/stun status effects.
- Added visual indicators for frozen enemies (ice spikes) and DoT particles.
- Added 3-slot tier-colored pips on tower base plates + pulsating evolution auras.
- Updated TowerPanel with 9-star tier display (★★★ yellow, ★★★ orange, ★★★ blue)
  and dynamic special ability descriptions.

Testing & Validation:
- Added `scripts/test-evolution.mts` covering all 10 evolved mechanics (all green).
- Verified lockstep multiplayer determinism via `scripts/test-mp.mts`.
- Verified solo campaign balance preservation via `scripts/sim.mts`.
- Full TypeScript typecheck and production build passing.
2026-08-16 13:54:09 +02:00

159 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Headless balance simulation: a greedy bot plays the game on 'normal'
* to verify the campaign is winnable (and not trivially easy).
*
* Run: npm run sim
*/
import { GameEngine } from '../src/game/engine.ts'
import { TOWERS } from '../src/game/config.ts'
import type { TowerKind } from '../src/game/types.ts'
/** campaign balance stays tuned for 3 levels evolutions are endless-mode luxury */
const SIM_MAX_LEVEL = 3
const eng = new GameEngine()
eng.startGame('normal')
// predetermined build spots (tile coords) near path hot spots
const spots: [number, number][] = [
[8, 5], // center between the two long vertical runs
[3, 0],
[7, 0],
[11, 0],
[4, 4],
[12, 6],
[8, 8],
[8, 10],
[12, 10],
[16, 3],
[18, 4],
[15, 0],
[4, 7],
[8, 2],
[12, 3],
[7, 10],
[13, 10],
[1, 4],
[18, 6],
[16, 6],
[5, 0],
[9, 0],
[13, 0],
[8, 6],
[12, 7],
[4, 2],
]
// what to build in which order (afterwards: upgrades only)
const plan: TowerKind[] = [
'arrow',
'frost',
'arrow',
'cannon',
'arrow',
'tesla',
'frost',
'arrow',
'cannon',
'laser',
'tesla',
'laser',
'frost',
'cannon',
'tesla',
'arrow',
'laser',
'frost',
'cannon',
'tesla',
'laser',
'arrow',
'arrow',
'arrow',
'arrow',
'arrow',
]
function bot(): void {
// spend greedily: cheapest sensible action first, repeat
for (;;) {
let acted = false
// next planned build on the next free spot
const built = eng.towers.length
if (built < plan.length) {
const kind = plan[built]
const spot = spots[built % spots.length]
const cost = towerCost(kind)
if (eng.money >= cost && eng.canPlace(spot[0], spot[1])) {
if (!eng.placeTower(kind, spot[0], spot[1])) throw new Error('place failed at ' + spot)
acted = true
continue
}
// spot blocked -> skip this spot for that build
if (!eng.canPlace(spot[0], spot[1])) {
// try any other free spot
const alt = spots.find((s) => eng.canPlace(s[0], s[1]))
if (alt && eng.money >= cost) {
eng.placeTower(kind, alt[0], alt[1])
acted = true
continue
}
}
}
// cheapest upgrade among towers (max 3 levels)
let bestCost = Infinity
let bestT: ReturnType<typeof eng.towers.at> = undefined
for (const t of eng.towers) {
const c = upgradeCostOf(t.kind, t.level)
if (c !== null && c < bestCost) {
bestCost = c
bestT = t
}
}
if (bestT && bestCost <= eng.money) {
eng.upgradeTower(bestT)
acted = true
continue
}
if (!acted) break
}
// start next wave early for the bonus, like an aggressive player
if (eng.phase === 'intermission') eng.startWave(true)
}
function towerCost(kind: TowerKind): number {
return TOWERS[kind].cost
}
function upgradeCostOf(kind: TowerKind, level: number): number | null {
if (level >= SIM_MAX_LEVEL) return null
return TOWERS[kind].upgradeCost[level - 1]
}
let lastWave = 0
let guard = 0
while (eng.phase !== 'gameover' && eng.phase !== 'victory' && guard < 60 * 60 * 60) {
eng.update(1 / 60)
bot()
if (eng.waveNo !== lastWave) {
lastWave = eng.waveNo
const kindCounts: Record<string, number> = {}
for (const t of eng.towers) kindCounts[t.kind + t.level] = (kindCounts[t.kind + t.level] ?? 0) + 1
console.log(
`Welle ${String(eng.waveNo).padStart(2)} | ❤ ${String(eng.lives).padStart(2)} | 🪙 ${String(Math.floor(eng.money)).padStart(4)} | Türme: ${eng.towers.length} ${JSON.stringify(kindCounts)}`,
)
}
guard++
}
console.log('---')
console.log(`Ergebnis: ${eng.phase === 'victory' ? 'SIEG' : 'NIEDERLAGE'} in Welle ${eng.waveNo}, Leben übrig: ${eng.lives}, Punkte: ${eng.score}`)
if (eng.phase !== 'victory') {
console.log(' → Balancing zu schwer! Gegner-Werte abschwächen.')
} else if (eng.lives > 14) {
console.log(' → Balancing zu leicht (zu viele Leben übrig).')
}