123 lines
5.7 KiB
Markdown
123 lines
5.7 KiB
Markdown
# 06 – Refactoring & Performance (Advisory für `Aegis-Labyrinth.html`)
|
||
|
||
> **Advisory-Dokumentation** – Empfehlungen, `Aegis-Labyrinth.html` (~707 KB, Single-File-Game)
|
||
> sauberer, schneller und wartbarer zu machen, **ohne** das sichtbare Spielverhalten zu ändern.
|
||
>
|
||
> ⚠️ Diese Datei gehört **nicht** zur Test-Suite selbst – sie ist ein **Beratungs-Backlog** für die
|
||
> extern eingelegte Game-Datei. Die Suite (`tests/*`) prüft nur, dass Änderungen daran die 3 Audits
|
||
> nicht brechen (→ `02_Audits.md`) und die Performance nicht einbricht (→ `04_Resultate_Report.md`).
|
||
|
||
---
|
||
|
||
## 1. Ist-Zustand (kurz)
|
||
|
||
| Aspekt | Status |
|
||
|---|---|
|
||
| Struktur | Eine HTML-Datei: CSS + ~4.500 Zeilen JS + UI |
|
||
| Rendering | Canvas 2D, `requestAnimationFrame`-Loop, Partikel-/Projectile-Arrays |
|
||
| State | Globale `Game`-Objekte + freie Funktionen im Global Scope |
|
||
| Persistenz | `localStorage` (Skilltree, Audio, Unlocks, Custom Levels) |
|
||
| Debug | `window.__AEGIS_DEBUG__` (Audits, State, Audio, Levels) → `01_Debug_API.md` |
|
||
| Tests | 3 eingebauten Audits + externe Suite (`tests/`) |
|
||
| Bundle | Base64-PNG-Logo (ca. 350 KB) inline – bereits optimiert |
|
||
|
||
---
|
||
|
||
## 2. Priorisierte Maßnahmen (P0 = mach heute, P2 = Backlog)
|
||
|
||
### P0 – Hohe Wirkung, geringes Risiko
|
||
|
||
| # | Maßnahme | Wo | Erwarteter Effekt |
|
||
|---|---|---|---|
|
||
| 1 | **Partikel-/Projectile-Pooling** statt `push`/`filter` pro Frame | `updateProjectiles`, `updateEnemies`, Partikel-Arrays | −30–50 % GC-Pressure |
|
||
| 2 | **Spatial Hash** (Grid 32 px) für `towerFire`-Zielwahl (heute O(N·M) alle Paare) | `towerFire(t,dt)` | −50–80 % CPU bei 50+ Türmen |
|
||
| 3 | **Canvas-Layer-Trennung**: `bgCanvas`, `midCanvas`, `fxCanvas` + `ctx.imageSmoothingEnabled=false` | `render()` | Weniger Re-Draws, stabilerer 60 fps |
|
||
| 4 | **Offscreen-Canvas für statische Route** (1× pro Level zeichnen, dann `drawImage`) | Route-Drawing | −10–20 % Framezeit |
|
||
| 5 | **Delta-Clamp**: `dt = Math.min(dt, 1/30)` am Loop-Start | Game-Loop | Keine Explosionen nach Tab-Wechsel |
|
||
| 6 | **`willReadFrequently:true`** nur bei `getImageData`-Puffern, `desynchronized:true` bei FX-Canvas | Canvas-Kontexte | Geringere GPU-Roundtrips |
|
||
| 7 | **Event-Debounce** auf `mousemove` (Tooltip) & `resize` | HUD/Tooltip | Weniger Layout-Thrashing |
|
||
|
||
### P1 – Mittlere Wirkung
|
||
|
||
| # | Maßnahme |
|
||
|---|---|
|
||
| 8 | **Modularisierung** in IIFE/Module: `core/loop`, `core/state`, `units/tower`, `units/enemy`, `systems/pathflow`, `systems/aura`, `systems/skills`, `ui/*`, `audio`, `persistence`, `debug` |
|
||
| 9 | **State-Reduktion**: `Game`-Objekt durch `store`-Muster mit `subscribe/get` ersetzen (~60 Zeilen) |
|
||
| 10 | **Pure-Function-Tests für Pfadlogik**: `flowField`, `diagnoseRouteState`, `computePath` extrahieren → unit-testbar |
|
||
| 11 | **Konstanten-Block** (`CELL`, `MAP_W`, `NANITE_LIMIT`, `PARTICLE_MAX`, …) in `constants` |
|
||
| 12 | **Magic-Number-Audit**: `1/60`, `0.05`, `180` etc. in Konstanten umbenennen |
|
||
| 13 | **`Symbol()`-Keys** für interne `Game.towers[i]._internal*` statt String-Props |
|
||
|
||
### P2 – Backlog
|
||
|
||
| # | Maßnahme |
|
||
|---|---|
|
||
| 14 | Web Worker für Flow-Field-Berechnung bei großen Maps |
|
||
| 15 | WebGL2-Option (`<canvas>`-Fallback bleibt) für Partikel > 5.000 |
|
||
| 16 | `requestIdleCallback` für UI-Rebuilds (Tower-Panel, Skilltree) |
|
||
| 17 | Code-Splitting via `import()` hinter „Menu" |
|
||
| 18 | `Intl.NumberFormat` für Zahlen im HUD |
|
||
| 19 | i18n-Datei (DE/EN) statt inline-Strings |
|
||
| 20 | PWA-Shell (`manifest.json`, `sw.js`) für Offline |
|
||
|
||
---
|
||
|
||
## 3. Rendering-Optimierung (Details)
|
||
|
||
### 3.1 Frame-Pipeline (empfohlen)
|
||
|
||
```
|
||
[1] input → pointer events, key state
|
||
[2] update(dt) → enemies → towers → projectiles → nanites → particles
|
||
[3] dirty check → bgDirty / midDirty / fxDirty (Boolean pro Layer)
|
||
[4] render() → if bgDirty: drawImage(bgCanvas);
|
||
if midDirty: draw towers+enemies;
|
||
fxCanvas always (additive)
|
||
```
|
||
|
||
### 3.2 Konkrete Canvas-Techniken
|
||
|
||
```js
|
||
const fx = document.createElement('canvas');
|
||
const fctx = fx.getContext('2d', { alpha: true, desynchronized: true });
|
||
fctx.globalCompositeOperation = 'lighter'; // Partikel additiv
|
||
// pro Frame:
|
||
fctx.clearRect(0, 0, fx.width, fx.height); // oder 'destination-out' Fade für Trails
|
||
```
|
||
|
||
- **Batching**: Alle Partikel gleicher Farbe in einen `beginPath()` → eine `fill()`.
|
||
- **`roundRect`** nativ (Chrome 99+) statt Hand-Rotation.
|
||
- **`createPattern`** für Tile-Boden (1× erzeugen).
|
||
- **Sprite-Atlas** für die 9 Türme: 1 Atlas 512×512, `drawImage(atlas, sx,sy,sw,sh, x,y,w,h)`.
|
||
|
||
### 3.3 Zielwerte
|
||
|
||
| Metrik | Vorher (Messung in DevTools) | Nachher (Ziel) |
|
||
|---|---|---|
|
||
| Framezeit @ 4K, 80 Türme, 150 Feinde | 18–24 ms | ≤ 12 ms |
|
||
| GC-Pausen/Minute | ~12 | ≤ 4 |
|
||
| Heap-Wachstum (10 Min) | ~40 MB | ≤ 10 MB |
|
||
|
||
---
|
||
|
||
## 4. Refactoring-Checkliste (Definition of Done)
|
||
|
||
- [ ] Alle drei Audits (`self`, `extension`, `redesign`) vor & nach Refactoring grün
|
||
- [ ] `window.__AEGIS_DEBUG__.getState()` liefert identisches Schema (nur interne Keys dürfen sich ändern)
|
||
- [ ] Kein sichtbarer Frame-Drop bei 60 fps Test-Level
|
||
- [ ] `localStorage`-Schema unverändert (oder Migration in `persistence.js`)
|
||
- [ ] Editor-Map & Custom-Level import/export byte-identisch
|
||
- [ ] Lighthouse-Performance ≥ 95 (mobile emulation)
|
||
|
||
---
|
||
|
||
## 5. Vorschlag: Refactoring in 3 Sprints
|
||
|
||
| Sprint | Inhalt | Aufwand |
|
||
|---|---|---|
|
||
| S1 (Tag 1–2) | P0 1–7 (Pooling, Spatial Hash, Layer-Cache, dt-Clamp) | ~6 h |
|
||
| S2 (Tag 3–4) | P1 8–11 (Modularisierung + Konstanten + Store) | ~8 h |
|
||
| S3 (Tag 5) | P1 12–13 + P2 14 (Worker) + Lighthouse-Polish | ~5 h |
|
||
|
||
**Regressionsschutz:** Vor jedem Sprint einen „Golden Frame" (Screenshot + `getState()`-Dump) mit der
|
||
Test-Suite erzeugen (`tests/audit.mjs` + `tests/perf-test.mjs`, siehe `03_Automatisierung_CI.md`). |