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.
This commit is contained in:
parent
ef75203db7
commit
4f8400c480
8 changed files with 518 additions and 98 deletions
|
|
@ -5,8 +5,12 @@
|
|||
* 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')
|
||||
|
||||
|
|
@ -122,20 +126,12 @@ function bot(): void {
|
|||
}
|
||||
|
||||
function towerCost(kind: TowerKind): number {
|
||||
const costs: Record<TowerKind, number> = { arrow: 50, cannon: 110, frost: 80, tesla: 130, laser: 160 }
|
||||
return costs[kind]
|
||||
return TOWERS[kind].cost
|
||||
}
|
||||
|
||||
function upgradeCostOf(kind: TowerKind, level: number): number | null {
|
||||
const up: Record<TowerKind, [number, number]> = {
|
||||
arrow: [60, 110],
|
||||
cannon: [100, 180],
|
||||
frost: [70, 130],
|
||||
tesla: [120, 200],
|
||||
laser: [150, 260],
|
||||
}
|
||||
if (level >= 3) return null
|
||||
return up[kind][level - 1]
|
||||
if (level >= SIM_MAX_LEVEL) return null
|
||||
return TOWERS[kind].upgradeCost[level - 1]
|
||||
}
|
||||
|
||||
let lastWave = 0
|
||||
|
|
|
|||
133
scripts/test-evolution.mts
Normal file
133
scripts/test-evolution.mts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Evolution system test: each mechanic is verified on an isolated engine so
|
||||
* only the tower under test can kill the test enemies.
|
||||
*/
|
||||
import { GameEngine } from '../src/game/engine.ts'
|
||||
import { TOWERS } from '../src/game/config.ts'
|
||||
import type { Tower, TowerKind, Enemy } from '../src/game/types.ts'
|
||||
|
||||
let failures = 0
|
||||
const ok = (label: string, cond: boolean): void => {
|
||||
console.log(`${cond ? '✓' : '✗ FEHLER'}: ${label}`)
|
||||
if (!cond) failures++
|
||||
}
|
||||
|
||||
interface Rig {
|
||||
eng: GameEngine
|
||||
tower: Tower
|
||||
spawn(x: number, y: number, kind?: 'normal' | 'flyer' | 'boss'): Enemy
|
||||
}
|
||||
|
||||
/** fresh engine with one max-level tower in the center */
|
||||
function rig(kind: TowerKind, targetLevel = 9): Rig {
|
||||
const eng = new GameEngine()
|
||||
eng.startGame('easy')
|
||||
eng.money = 1_000_000
|
||||
eng.startWave(true)
|
||||
eng.placeTower(kind, 8, 5)
|
||||
const tower = eng.towerAt(8, 5)!
|
||||
while (tower.level < targetLevel) eng.upgradeTower(tower)
|
||||
const spawn = (x: number, y: number, k: 'normal' | 'flyer' | 'boss' = 'normal'): Enemy => {
|
||||
;(eng as unknown as { spawnEnemy: (kk: 'normal' | 'flyer' | 'boss') => void }).spawnEnemy(k)
|
||||
const e = eng.enemies[eng.enemies.length - 1]
|
||||
e.x = x
|
||||
e.y = y
|
||||
e.speedBase = 0
|
||||
e.wp = 1
|
||||
return e
|
||||
}
|
||||
return { eng, tower, spawn }
|
||||
}
|
||||
|
||||
function forceFire(r: Rig, ticks = 12): void {
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
r.tower.cooldown = 0
|
||||
r.eng.update(1 / 30)
|
||||
}
|
||||
}
|
||||
|
||||
const CX = 8 * 48 + 24
|
||||
const CY = 5 * 48 + 24
|
||||
|
||||
// ---------- arrow: multishot + poison ----------
|
||||
{
|
||||
const r = rig('arrow')
|
||||
ok('Bogenturm L9', r.tower.level === 9)
|
||||
const targets = [r.spawn(CX, CY - 30), r.spawn(CX, CY), r.spawn(CX, CY + 30), r.spawn(CX, CY + 60)]
|
||||
let arrows = 0
|
||||
for (let i = 0; i < 12; i++) {
|
||||
r.tower.cooldown = 0
|
||||
r.eng.update(1 / 30)
|
||||
arrows = Math.max(arrows, r.eng.projectiles.filter((p) => p.kind === 'arrow').length)
|
||||
}
|
||||
ok(`Multishot: mehrere Pfeile gleichzeitig (${arrows})`, arrows >= 3)
|
||||
ok('Pfeile treffen (Schaden)', targets.every((e) => e.hp < e.maxHp || e.dead))
|
||||
ok('Gift-DoT wirkt', r.eng.enemies.some((e) => e.dotDps === 28 && e.dotUntil > r.eng.time) || targets.every((e) => e.dead))
|
||||
}
|
||||
|
||||
// ---------- frost: freeze + shatter ----------
|
||||
{
|
||||
const r = rig('frost')
|
||||
ok('Eisturm L9', r.tower.level === 9)
|
||||
const e = r.spawn(CX, CY - 20, 'boss')
|
||||
for (let i = 0; i < 3; i++) {
|
||||
r.tower.cooldown = 0
|
||||
r.eng.update(1 / 30)
|
||||
}
|
||||
ok('Einfrieren nach 3 Pulsen (Faktor 0)', e.slowFactor === 0 && !e.dead)
|
||||
const hp = e.hp
|
||||
r.eng.damageEnemy(e, 100)
|
||||
ok('Zerbrechlich: +50% Schaden an Eingefrorenen', Math.abs(hp - e.hp - 150) < 1)
|
||||
}
|
||||
|
||||
// ---------- tesla: stun + storm ----------
|
||||
{
|
||||
const r = rig('tesla')
|
||||
ok('Tesla L9', r.tower.level === 9)
|
||||
const group = [r.spawn(CX - 24, CY), r.spawn(CX + 24, CY), r.spawn(CX, CY + 24)]
|
||||
r.tower.counter = 3 // nächster Schuss = Gewitter (jeder 4.)
|
||||
r.tower.cooldown = 0
|
||||
r.eng.update(1 / 30)
|
||||
const stunned = group.filter((e) => e.slowFactor === 0)
|
||||
ok(`Gewitter trifft + betäubt mehrere (${stunned.length}/3)`, stunned.length >= 2)
|
||||
}
|
||||
|
||||
// ---------- laser: pierce ----------
|
||||
{
|
||||
const r = rig('laser')
|
||||
ok('Laser L9', r.tower.level === 9)
|
||||
const line = [r.spawn(CX + 40, CY), r.spawn(CX + 75, CY), r.spawn(CX + 110, CY), r.spawn(CX + 145, CY)]
|
||||
forceFire(r)
|
||||
ok('Durchschlag trifft alle in der Linie', line.every((e) => e.hp < e.maxHp || e.dead))
|
||||
}
|
||||
|
||||
// ---------- laser: prism (L4-6 only) ----------
|
||||
{
|
||||
const r = rig('laser', 5)
|
||||
ok('Laser L5 (Prisma-Stufe)', r.tower.level === 5)
|
||||
r.spawn(CX + 40, CY)
|
||||
const b = r.spawn(CX + 60, CY + 10)
|
||||
const c = r.spawn(CX + 80, CY - 10)
|
||||
forceFire(r)
|
||||
ok('Prisma: Nebenziele nehmen Schaden', (b.hp < b.maxHp || b.dead) && (c.hp < c.maxHp || c.dead))
|
||||
}
|
||||
|
||||
// ---------- cannon: flak + burn ----------
|
||||
{
|
||||
const r = rig('cannon')
|
||||
ok('Kanone L9', r.tower.level === 9)
|
||||
ok('Flak-Upgrade aktiv (trifft Flieger)', TOWERS.cannon.flak![8] === true)
|
||||
const flyer = r.spawn(CX + 60, CY, 'flyer')
|
||||
forceFire(r)
|
||||
ok('Flak: Kanone beschädigt Flieger', flyer.hp < flyer.maxHp || flyer.dead)
|
||||
const boss = r.spawn(CX + 60, CY, 'boss')
|
||||
forceFire(r)
|
||||
ok('Brand-DoT wirkt', boss.dotDps > 0 && boss.dotColor === '#ff9c40')
|
||||
}
|
||||
|
||||
console.log('---')
|
||||
if (failures === 0) console.log('Alle Evolutions-Mechaniken funktionieren.')
|
||||
else {
|
||||
console.log(`${failures} FEHLER!`)
|
||||
process.exit(1)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue