From 4f8400c480842077af715edced31ac85fa53431e Mon Sep 17 00:00:00 2001 From: Tronax Date: Sun, 16 Aug 2026 13:54:09 +0200 Subject: [PATCH] feat: add 9-level tower evolution system with distinct tier abilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 27 ++-- scripts/sim.mts | 18 +-- scripts/test-evolution.mts | 133 ++++++++++++++++++ src/components/TowerPanel.vue | 17 ++- src/game/config.ts | 74 ++++++++-- src/game/engine.ts | 249 +++++++++++++++++++++++++++------- src/game/render.ts | 53 +++++++- src/game/types.ts | 45 ++++-- 8 files changed, 518 insertions(+), 98 deletions(-) create mode 100644 scripts/test-evolution.mts diff --git a/README.md b/README.md index 64d660e..e90dd1c 100644 --- a/README.md +++ b/README.md @@ -115,18 +115,27 @@ Verteidige deine Basis gegen 20 handgetunte Wellen (Leicht: 30 Leben, Normal: 20 --- -## Türme +## 🌟 9-Level-Evolutionssystem -| Turm | Kosten | Upgrade 2/3 | Besonderheit | +Jeder Turm hat **9 Ausbaustufen**, aufgeteilt in **3 Evolutions-Stufen**: + +| Tier | Level | Farbe | Bedeutung | | --- | --- | --- | --- | -| 🏹 Bogenturm | 50 🪙 | 60 / 110 🪙 | Günstig, schnelle Einzelschüsse, trifft Luft | -| 🧨 Kanone | 110 🪙 | 100 / 180 🪙 | Flächenschaden, trifft **keine** Flieger | -| ❄️ Eisturm | 80 🪙 | 70 / 130 🪙 | Verlangsamt alle Gegner im Umkreis um bis zu 65 % | -| ⚡ Teslaturm | 130 🪙 | 120 / 200 🪙 | Kettenblitz springt auf bis zu 5 Gegner über | -| 💫 Laserturm | 160 🪙 | 150 / 260 🪙 | Soforttreffer, maximale Reichweite & Durchschlag | +| **Basis** | 1–3 | 🟡 Gelb | Höherer Grundschaden, Reichweite & Feuerrate | +| **Evolution I** | 4–6 | 🟠 Orange | Neue Spezialfähigkeit schaltet sich frei | +| **Evolution II** | 7–9 | 🔵 Blau | Finale Evolutionsstufe mit verheerenden Effekten | -- **Verkauf:** 70 % der Gesamtinvestition (Kauf + Upgrades) werden erstattet. -- **Zielmodi:** Erster / Letzter / Stärkster / Nächster. +### Die Evolutionen im Detail + +| Turm | 🟡 Basis (L1–3) | 🟠 Evolution I (L4–6) | 🔵 Evolution II (L7–9) | +| --- | --- | --- | --- | +| 🏹 **Bogenturm** | Schneller Einzelschuss | **Mehrfachschuss:** Feuert 2–3 Pfeile gleichzeitig auf verschiedene Ziele | **Giftpfeile + 4-Fach:** Bis zu 4 Pfeile + starker Gift-DoT (28 Schaden/s) | +| 🧨 **Kanone** | Flächenschaden | **Brandgeschosse:** Hinterlässt brennenden DoT an allen getroffenen Gegnern | **Flak-Geschütz:** Trifft ab L7 auch **Flugeinheiten** mit voller Explosionskraft! | +| ❄️ **Eisturm** | Verlangsamender Puls | **Permafrost:** Jeder 3. Puls **friert alle Gegner komplett ein** (Bewegung = 0) | **Zerbrechlichkeit:** Eingefrorene Ziele nehmen **+50 % Schaden aus ALLEN Quellen**! | +| ⚡ **Teslaturm** | Kettenblitz | **Betäubungs-Schock:** Kettenblitze betäuben getroffene Ziele kurzzeitig | **Gewittersturm:** Jeder 4. Schuss schlägt bei **ALLEN Gegnern in Reichweite** gleichzeitig ein! | +| 💫 **Laserturm** | Soforttreffer | **Prisma-Brechung:** Der Laserstrahl bricht und springt auf 2–3 Nebenziele über | **Durchschlag:** Der Strahl durchdringt ALLES in einer Linie bis zum Rand der Reichweite | + +Türme ab Level 4 erhalten im Spiel eine pulsierende **Evolutions-Aura** (orange ab L4, blau ab L7) und die Sterne im Turm-Panel zeigen die jeweilige Tier-Farbe. --- diff --git a/scripts/sim.mts b/scripts/sim.mts index 51e38c7..3700ca1 100644 --- a/scripts/sim.mts +++ b/scripts/sim.mts @@ -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 = { 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 = { - 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 diff --git a/scripts/test-evolution.mts b/scripts/test-evolution.mts new file mode 100644 index 0000000..39a1e53 --- /dev/null +++ b/scripts/test-evolution.mts @@ -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) +} diff --git a/src/components/TowerPanel.vue b/src/components/TowerPanel.vue index 8d93ff0..2478513 100644 --- a/src/components/TowerPanel.vue +++ b/src/components/TowerPanel.vue @@ -23,8 +23,13 @@ function canAct(): boolean {
{{ store.selected.name }}
- - Level {{ store.selected.level }} + + Level {{ store.selected.level }}/9
@@ -60,7 +65,7 @@ function canAct(): boolean { :disabled="store.selected.maxLevel || store.money < (store.selected.upgradeCost ?? 0) || !canAct()" @click="uiUpgradeSelected()" > - +