Files
Wackelpeter/AGENTS.md
T

77 lines
11 KiB
Markdown
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.
# AGENTS.md
## Git-Regeln
- Git-Commits nur auf explizite Anforderung des Users. Niemals eigenständig committen.
- Vor einem Commit: `git status`, `git diff` und `git log --oneline -10` prüfen; nur beabsichtigte Dateien stagen, keine Secrets.
- Commit-Message kurz und im Stil des Repos (bisher englisch, imperative).
## Build & Verifikation
```bash
cd web
npx tsc --noEmit # Typcheck (Pflicht nach Änderungen)
npm run build # tsc + vite build → web/dist
npm run dev # Dev-Server mit HMR
```
- Stack: Three.js 0.170 + TypeScript + Vite. Keine weiteren Laufzeit-Dependencies (kein Postprocessing verfügbar — Screen-Effekte als CSS-Overlays).
- `Makefile`: `make web`/`make dev`/`make build` (Container-Build via Podman).
- UI-Sprache: Deutsch (Skill-Namen, Level-up-Karten); Code-Kommentare bisher gemischt, Git-Messages englisch.
## Modell-Viewer (visuelle Modell-Checks)
- `web/viewer.html` = eigener Vite-Entry (auch im Build): rendert ein Gegner-/Boss-Modell
isoliert auf Grid-Bühne mit hellem Licht, Turntable-Kamera (Auto-Fit per Box3) und
Mini-UI zum Umschalten. Modelle laufen gegen ein Stub-`Game` (Stub-Spieler als grüner
Blob 8 m entfernt als Blick-/Angriffsziel).
- URL-Params: `?m=<id>` (ids wie `dragon`, `krake`, `spider`, … siehe Registry in
`viewer.ts`), `&still=1` = statische Pose (nur `updateAnimation`, kein `update`).
- Automatisierte Screenshots: `scripts/model_shot.sh <id> [shots=4] [intervall=1.5] [still]`
startet eigenen Dev-Server auf Port 3100, öffnet Viewer-Fenster (Titel
„Wackelpeter Modell-Viewer"), minimiert die Konsole per KWin, macht `shots`
Turntable-Screenshots nach `/tmp/modelshot_*_crop.png`, blendet die Konsole wieder
ein, schließt das Viewer-Fenster und stoppt den 3100er-Server. Pfade kommen auf stdout.
- Workflow für Modell-Überarbeitung: Code editieren → `scripts/model_shot.sh <id>`
Crops lesen → iterieren. Keine Cheats, Klicks oder Bosskämpfe nötig.
## Architektur (web/src)
| Datei | Inhalt |
|---|---|
| `Game.ts` | Zentrale Klasse: State-Machine, Render-Loop (`animate()`), Input, UI-Verdrahtung, Spawning, Partikel-/Effekt-Arrays (bloodParticles, traps, gasClouds, slashEffects, projectiles), Hotbar |
| `Player.ts` | Blob-Spieler: Movement, Sprung, `damage()`/`heal()` (Schild-Absorption), Stats (`applySkillSystem`), Hurt-Feedback (Flash + Blut via `game.onPlayerHurt`) |
| `Enemy.ts` | Abstrakte Basis: `body` (THREE.Group), `takeDamage` (Sound + XP/Lifesteal via `game.onDamageDealt`), Fade-Death, Felder `knockback`, `rootTime` |
| `Skills.ts` | Alle Skill-Definitionen (`SKILLS`-Array: id, name, type, maxLevel, describe) + Level-Wertetabellen (z. B. `auraRadius(level)`). `SkillSystem` = XP/Level/Angebote. `HOTBAR_SKILLS` = aktive Skills in Erwerbs-Reihenfolge |
| `SkillIcons.ts` | SVG-Icons (64×64, Template-Strings) für die Level-up-Karten |
| `Weapon.ts` | Basis für aktive Skills: `cooldown1-3` + `reduceCooldowns()` |
| `Sword.ts` / `Staff.ts` | Linksklick-Schwert (Bogen-Trefferzone via `SwordTrail.ts`), Rechtsklick-Feuerball + Teleport |
| `ChainLightning.ts` | Kettenblitz (Q-Slot) |
| `Trapper.ts` | Falle (skill1) + Gaswolke (skill2) |
| `Aura.ts` / `Boomerang.ts` | Passive Skills, lesen Level live aus `game.skillSystem` |
| `Trap.ts` / `GasCloud.ts` | Platzierbare Effekte; `update(dt, game): boolean` (false = entfernen), `dispose()` |
| `Golem.ts` / `Krake.ts` / `Minotaurus.ts` / `SpiderQueen.ts` / `FrostGiant.ts` / `Necromancer.ts` / `Dragon.ts` | Bosse (Welle 10 / 20 / 30 / 40 / 50 / 60 / 70), registriert in `Game.bossWaves` (`{name, spawn}`). Krake: stationär (`spawnAtCenter`), Tinte (`InkBlob.ts`/`InkPuddle.ts`: Slow + DoT), Tentakel-Griff (5 rote Boden-Marken um den Oktopus → Pull via `player.pullTime`), Sweep (roter Tentakel kreist um den Oktopus, überspringbar), Enrage <30% HP. Spinnenkönigin: stationär, Netze (`WebPatch.ts`: fesselt via `player.rootTime`), Spinngeschwister (Mini-Spiders), Säuresalven. Frostriese: Eispanzer (70% Schadensreduktion, `armorHp`, bricht + Stagger, regeneriert nach Timer), Eisberst-Ring (nacheinander explodierende Marken), Eiszapfen-Regen, Stampf (überspringbar). Erz-Nekromant: schwebend, Teleport mit Glow-Ring-Telegraf + Ankunftssalve, Beschwörungen mit wachsendem Boden-Glow. Drache: fliegt und ist nur im Lande-Zustand verletzbar (`takeDamage`-Override blockt im Flug), Sturzangriff mit wachsendem Schatten-Telegraf, Feuerspeer (Telegraf-Bogen, überspringbar), Feuerbälle im Flug, Enrage mit Doppel-Sturz. Minotaurus: nur von hinten verletzbar (vorn „BLOCK"), Turn-Lag, Dash → Wand-Stun (Sterne) + Wandrisse, Axt-Schwung (überspringbar), Sprung+Stampf (Stun `player.stunTime` + `FallingRock.ts`), Enrage <40% (Doppel-Dash, Kamera-Shake) |
| `Maze.ts` / `MazeGuard.ts` / `Gnome.ts` | Labyrinth-Zwischenboss (Welle 5/15/25/…, `Game.MAZE_WAVES`): randomisierter Prim-Generator (11×11, `collidePlayer`-Kollision, `hasLineOfSight`), Wächter-Geister aktivieren sich erst bei Spielernähe (`MazeGuard.chase`), Gnom als Ziel (`Gnome.ts`, `isBoss`, nur mit Sichtlinie verletzbar, ab Level 2 doppeltes Ziel). Spieler-Kollision nur während `mazeActive`; `#maze-arrow`-Overlay zeigt den Weg; Boss-Modus-Buttons „Labyrinth (Welle X)" |
| `InkBlob.ts` / `InkPuddle.ts` / `FallingRock.ts` | Boss-Effekte: Tintenprojektil → Pfütze mit Slow (`player.slowTimer`) + DoT + `game.triggerInkVignette()`; fallender Fels mit Schatten-Telegraf |
| `*Enemy*.ts` (Spider, Ghost, Frog, Bat, BeeSwarm, Slime, Brute, Crab, Turret, Shaman, Archer, Bomber, Wisp, Turtle, SporeMushroom) | Prozedurale Gegner-Modelle; je `update(dt, game)`. Schamane heilt Gegner im Radius (Prioritätsziel), Bogenschütze hält Distanz + Pfeil mit Boden-Telegraf, Bomber explodiert bei Nähe (AoE), Irrlicht schießt Homing-Sporen (Slow), Schildkröte blockt vorn komplett + Panzer-Rückzug (unverwundbar), Sporen-Pilz wirft Bogen-Sporen mit DoT-Wolke |
| `SoundManager.ts` | Vorab geladene .ogg-Buffer (`playSound(name, volume)`); Sounds: damage, spawn, fireball, explosion, teleport, gameover, sword_hit1, spiderwalking |
| `MeshLoader.ts` | `loadGLB(name)``web/public/models/*.glb`; Texturen in `web/public/textures/` |
## Wichtige Konventionen & Fallstricke
- **Blickrichtung = lokale +Z**: Three.js `Object3D.lookAt` richtet +Z zum Ziel aus. Augen/Gesichter von Gegnern und Spieler müssen auf **+Z** liegen (sonst laufen sie rückwärts — war schon mal ein Bug bei Frosch/Golem/Brute/Slime/Bat/Crab).
- **UI-Overlays**: Alle Panels liegen absolut in `#ui-overlay` (`pointer-events: none`). Versteckte Panels brauchen **drei** Dinge: `opacity: 0` + `pointerEvents: none` + `visibility: hidden`. Achtung: `.menu-button { pointer-events: auto }` übersteuert `pointer-events: none` des Parents — ohne `visibility: hidden` sind unsichtbare Buttons klickbar (verursachte „stille Neustarts" mitten im Spiel). Neues Panel → immer `visibility` mitsetzen.
- **State-Machine**: `menu | playing | levelup | gameover` + Flag `paused` (ESC). `animate()` rendert bei Pause nur UI. `showMenu()` hat einen Guard (nur aus menu/gameover/pause).
- **Hotbar**: Aktive Skills (Kettenblitz, Teleport, Falle, Gaswolke) werden beim Erwerb auf Q, E, 1, 2, 3, 4 verteilt (`HOTBAR_KEY_CODES`). Dispatch über `triggerHotbarSkill()`. Cooldown-Labels zeigen Taste + Name (`syncSkillVisibility`).
- **Skill-Arten**: Aktive = `Weapon`-Subklassen mit Cooldowns (UI-Balken in `index.html`, Update in `updateUI`). Passive = Stats in `Player.applySkillSystem` ODER Game-seitige Objekte (Aura/Boomerang), die in `syncSkillVisibility` erzeugt/zersstört und im Loop geupdated werden.
- **Effekt-Lifecycle**: Neue Effekte mit eigenem `update(dt, game)`-Rückgabewert (false = aufräumen) und `dispose()`; in `Game.resetWorld()` ALLES aufräumen (sonst Geister-Objekte nach „Nochmal spielen").
- **Gegner-Festhalten**: `enemy.rootTime > 0` friert `enemy.update()` ein (siehe `Game.updateEnemies`) — so funktioniert die Falle, ohne jede Gegner-Klasse anzufassen.
- **Boss-Wellen & Boss-Modus**: `Game.bossWaves` (Map: Welle → `{name, spawn}`) steuert Boss-Wellen (10, 20 …) — Boss-Welle startet erst, wenn die Arena geräumt ist (`enemies.every(e => e.dead)`); nach Boss-Tod läuft die nächste normale Welle an. Cheat-Code **„idkfa"** im Hauptmenü blendet den Boss-Modus ein (`#boss-mode`, Buttons werden aus `bossWaves` erzeugt) → `startBossFight(wave)` mit Zufalls-Skill-Boost (`grantRandomSkills`). `spawnAtCenter`-Flag lässt stationäre Bosse in der Arena-Mitte spawnen. Effekte, die ein Boss selbst verwaltet (Tinten-Blobs/-Pfützen, Ringe), müssen in `onDeath`/`dispose` aufgeräumt werden (Dead-Enemy-Removal ruft `dispose` NICHT auf).
- **Balance**: XP-Kurve `30 + 15·Level` (steiler, Skills maxen langsamer); Level-Ups geben genau **1** Skill-Punkt (Multi-Level nur bei XP-Überschuss, `pendingPicks`); Gegner-HP skaliert erst NACH den Bossen (jede 10. Welle = 1 Boss-Gate → +20% HP, nur Nicht-Bosse, in `spawnEnemyInstance`); neue Gegner erscheinen in `enemyWeights` nur nach Boss-Wellen (vor Golem nur Spinnen/Frösche, dann nach Golem/Krake/Minotaurus/Spinnenkönigin/Frostriese/Nekromant/Drache); Boss-Modus-Grants `wave·0.4` Skills. Arena-Theme: `Arena.applyTheme(stage)` (8 Stufen, Boden/Wände/Nebel/Licht) wird in `Game` nach jedem Boss-Sieg (`arenaStage++`) getriggert, weiches Lerpen in `arena.update(dt)`.
- **Gegner-Spawn im Sichtbereich**: `spawnEnemy`/`spawnBossForWave` spawnen im Ring 1024 m um den Spieler; stationäre Gegner (`immovable`), die >8 s außerhalb des Bildschirms sind (NDC-Projektion `isOnScreen`), werden wieder in Sicht teleportiert (`updateOffscreenReposition`).
- **Labyrinth-Zwischenboss**: `Game.MAZE_WAVES` (5, 15, 25, …) ersetzt die Welle komplett (`startMazeWave`/`updateMaze`/`endMazeWave`); Zeitlimit (75 s 5 s/Level, Min 50 s) → bei Ablauf wird das Labyrinth neu generiert und der Spieler zur Startecke gesetzt; Gnome nur mit Sichtlinie verletzbar (`hasLineOfSight`); `mazeActive` pausiert Spawn-Logik und Off-Screen-Reposition; Aufräumen über `disposeMaze()` in `resetWorld`; `#maze-arrow`-Overlay zeigt zum Ziel.
- **Spieler-Status-Effekte** (in `Player.ts` + `Game.updatePlayerMovement`): `slowTimer`/`slowFactor` (Tinten-Slow, Lila-Tönung), `pullTime`/`pullDir` (Tentakel-Griff: Eingabe wird ignoriert, Geschwindigkeit = Pull-Richtung), `stunTime` (Stampf-Schockwelle: keine Eingaben, gelbe Sterne kreisen um den Kopf) und `rootTime` (Netz-Fessel: keine Bewegung, Angriffe bleiben möglich). Alles wird in `Player.reset()` zurückgesetzt.
- **Kamera-Shake**: `game.triggerCameraShake(duration, amp)` → in `animate()` wird die Kameraposition kurz zufällig versetzt (kein Postprocessing nötig).
- **Kontaktschaden** (Gegner berührt Spieler) läuft in `Game.ts` (Close-Encounter-Loop) mit `contactDps * dt`; Dornen-Skill ebendort als Gegenschlag.
- **`playSound`** erzeugt pro Aufruf einen AudioBufferSourceNode — bei hochfrequenten Effekten (Aura-Ticks, Bumerang-Hits) ggf. throtteln.