Files
slop-game-opus5.5/docs/ARCHITECTURE.md
T
2026-09-24 17:57:54 +02:00

181 lines
9.1 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.
# Architektur
Glory Defense ist eine monolithische Flask-Anwendung. Das eigentliche Spiel läuft
vollständig im Browser (Phaser 3). Der Server liefert Seiten, statische Dateien und
eine kleine JSON-API für die Ehrentafel.
```mermaid
flowchart LR
Browser["Browser<br/>Phaser 3 + ES-Module"] -- "GET /, /disclaimer, /static/…" --> Flask
Browser -- "POST /api/runs<br/>GET/POST /api/highscores" --> Flask
Flask["Flask-App<br/>(passenger_wsgi.py → create_app)"] --> DB[("SQLite<br/>instance/glory_defense.sqlite3")]
```
## Backend (`app/`)
| Modul | Aufgabe |
|---|---|
| `__init__.py` | App-Factory `create_app()`: Konfiguration laden, Datenbank initialisieren, Blueprints registrieren, Security-Header (CSP), JSON-Fehler für `/api/*` |
| `config.py` | Konfiguration aus Umgebungsvariablen, Pfade relativ zum Projektordner, automatisch erzeugter Schlüssel, Plausibilitätsgrenzen |
| `routes.py` | `GET /`, `GET /disclaimer`, `GET /healthz` |
| `api.py` | `POST /api/runs`, `GET /api/highscores`, `POST /api/highscores` |
| `models.py` | SQLite-Schema und Zugriffsfunktionen (Highscores, Rate-Limit-Protokoll, verbrauchte Run-Tokens), CLI `flask init-db` und `flask scores list/delete/delete-name` (Moderation) |
| `validation.py` | reine Funktionen: Namensbereinigung, Sperrliste, Typprüfung, Plausibilitätsprüfung |
`passenger_wsgi.py` im Projektordner ergänzt `sys.path`, lädt optional `.env` und
stellt `application = create_app()` bereit.
### Warum der Zustand in SQLite liegt
Passenger betreibt je nach Last mehrere Python-Prozesse und beendet sie nach
Leerlauf. Zustand im Arbeitsspeicher, etwa ein Rate-Limit-Zähler, wäre deshalb
weder zwischen den Prozessen geteilt noch dauerhaft. Darum liegen auch
Rate-Limit-Einträge und eingelöste Run-Tokens in SQLite. Zählen und Eintragen laufen
in `BEGIN IMMEDIATE`-Transaktionen, damit parallele Prozesse sich nicht in die
Quere kommen.
### Highscore-Ablauf
```mermaid
sequenceDiagram
participant G as Spiel (Browser)
participant A as Flask-API
participant D as SQLite
G->>A: POST /api/runs (Spielstart)
A-->>G: 201 {run_token} (signiert mit SECRET_KEY, enthält Zeitstempel)
Note over G: Spielen …
G->>A: POST /api/highscores {name, score, wave, duration_s, run_token}
A->>D: Rate-Limit prüfen und Versuch protokollieren
A->>A: Format prüfen, Name bereinigen, optionale Sperrliste prüfen
A->>A: Token prüfen (Signatur, Alter)
A->>A: Plausibilität (Dauer ≤ Token-Alter, Punkte ≤ Obergrenze, Welle erreichbar)
A->>D: Token einlösen + Eintrag speichern (eine Transaktion)
A-->>G: 201 {rank, highscores}
```
Antwortcodes: `400` ungültige Eingaben (mit Feldfehlern), `409` Token schon
eingelöst, `413` zu große Anfrage, `422` Token ungültig/abgelaufen oder Punktestand
unplausibel, `429` Rate-Limit (mit `Retry-After`).
Die Prüfungen verhindern kein Cheaten, denn die Spiellogik läuft im Browser. Sie
filtern aber offensichtlich Unmögliches aus und verhindern, dass eine Runde mehrere
Einträge erzeugt.
### Sicherheit
- **Content-Security-Policy:** `script-src 'self'`, kein `unsafe-eval`, keine fremden Quellen.
`blob:`/`data:` sind für Bilder und Audio erlaubt, weil Phaser geladene Dateien so dekodiert.
- **Namen:** NFKC-Normalisierung, Entfernen von Steuer-, Format- und Bidi-Zeichen, Whitelist,
max. 16 Zeichen. Im Frontend werden Namen ausschließlich als Text ausgegeben
(`textContent` bzw. Canvas-Text), nie als HTML.
- **Datenschutz:** Statt IP-Adressen wird nur ein HMAC-Hash gespeichert, und nur solange
das Rate-Limit-Fenster läuft.
- **Proxy:** Mit `GLORY_TRUST_PROXY=1` übernimmt `ProxyFix` nur Client-IP und Protokoll,
nicht Host oder Präfix (sonst ließen sich per Header fremde URLs in die Seite schleusen).
- **Schlüssel:** Fehlt `SECRET_KEY`, entsteht `instance/secret_key` erst beim ersten Bedarf
(Run-Token, Rate-Limit). CLI-Befehle legen also keine unnötige Schlüsseldatei an.
- **Anfragegröße** begrenzt auf 16 KB.
## Frontend (`app/static/js/`)
Native ES-Module ohne Bundler oder Build-Schritt. Phaser wird vorher als klassisches
Script geladen und ist global verfügbar. Alle URLs (Static-Basis, API) übergibt das
Template per `data-*`-Attribut. Darum funktioniert das Spiel auch unter einer
Unter-URL.
```
main.js Dienste bauen (Settings, Audio, API), Phaser starten
config.js alle Spielwerte
content/texts.js alle Texte
logic/ reine Logik ohne Phaser – vollständig per Vitest getestet
scenes/ Phaser-Szenen
entities/ Spielobjekte (Phaser-Sprites)
systems/ Querschnittsdienste (Assets, Audio, Effekte, Orbital, Overlays …)
ui/ Menüliste, Textstile
```
### Trennung von Logik und Darstellung
Alles, was sich ohne Grafik berechnen lässt, liegt in `logic/` und importiert nie
Phaser. Die Szenen und Entities rufen diese Funktionen auf und setzen das Ergebnis
in Physik und Grafik um.
| Modul | Inhalt |
|---|---|
| `waves.js` | Wellenbudget, Fraktionen, Eliten, Gegnerzusammenstellung, Spawn-Zeitplan |
| `waveDirector.js` | Ablauf: Pause → Welle → Spawns → Welle geschafft (mit Spawn-Stopp bei zu vielen Gegnern) |
| `scoring.js` | Punkte, Serien, Multiplikator, Wellenbonus |
| `comboInput.js` | Eingabe der Orbital-Befehle als Richtungsfolge |
| `cooldowns.js` | Abklingzeiten |
| `steering.js` | Steuerungsverhalten: Verfolgen, Fliehen, Abstand halten, Flankieren, Umzingeln, Separation |
| `enemyAi.js` | Entscheidungen je Gegnertyp: Sprungangriff, Salven, Brut, Ausweichrichtung |
| `weapon.js` | Magazin, Reserve, Feuerrate, Nachladen, Streuung |
| `format.js` | Anzeigeformate (Punkte, Zeiten, Befehlscodes), Namensbereinigung wie im Backend |
| `rng.js` / `math.js` | seedbarer Zufall, Vektorhelfer |
| `marchPattern.js` | Noten der Hintergrundmusik |
### Szenenfluss
```mermaid
flowchart TD
Boot["Boot<br/>Assets laden, Platzhalter erzeugen"] --> Menu
Menu["Menü"] -- "erster Start" --> Disclaimer["Disclaimer (Overlay)"]
Disclaimer -- "bestätigt (localStorage)" --> Menu
Menu -- "Mission starten" --> Briefing
Menu --> Highscores["Ehrentafel"] --> Menu
Menu --> Controls["Steuerung"] --> Menu
Menu --> Credits --> Menu
Menu -- "Disclaimer" --> Disclaimer
Briefing --> Game["Game + HUD (parallel)"]
Game -- "Esc / Fenster verliert Fokus" --> Pause["Pause (Overlay, Lautstärke)"]
Pause -- "Weiter" --> Game
Pause -- "Abbrechen" --> Menu
Game -- "Leben = 0" --> GameOver["Game Over<br/>Name, Top 10"]
GameOver -- "Erneut" --> Game
GameOver -- "Hauptmenü" --> Menu
```
- **Game** und **HUD** laufen gleichzeitig. Das HUD liest in jedem Frame
`GameScene.hudState()` und empfängt Ereignisse (`announce`, `feedback`,
`player-damaged`). So scrollt es nicht mit der Kamera und wackelt nicht mit.
- **Disclaimer, Pause und Game Over** sind HTML-Overlays über dem Canvas
(`systems/Overlay.js`). Formulare und Schieberegler sind so mit Tastatur und
Screenreader bedienbar.
### Zeit, Eingabe und Kollisionen
- **Spielzeit:** Alle Mechaniken nutzen `GameScene.gameTime`, eine eigene Uhr, die nur
bei aktiver Szene läuft. Abklingzeiten laufen in der Pause also nicht weiter, und
die gemeldete Spieldauer enthält keine Pausen. Große Frame-Sprünge werden auf
100 ms begrenzt.
- **Tastatur:** Bewegung wird pro Frame abgefragt. Einmalige Aktionen (Rolle,
Nachladen, Waffenwechsel, Pause) kommen über `keydown`-Ereignisse. Phasers
`JustDown` verliert Tastendrücke, die innerhalb eines Frames gedrückt und
losgelassen werden. Außerdem meldet Phaser 3.90 bei mehreren Tastenereignissen pro
Frame frühere `keydown` erneut. `systems/keyboard.js` (`oncePerEvent`) filtert das
anhand der Ereignisidentität.
- **Kollisionen (Arcade Physics, Kreis-Hitboxen):**
- Spielerprojektile ↔ Gegner: Schaden, Projektil verschwindet
- Gegnerprojektile ↔ Präsident: Schaden. Während der Rolle fliegen Projektile hindurch.
- Gegner ↔ Präsident: Kontaktschaden mit Abklingzeit je Gegner
- Präsident ↔ Kapsel: Heilung und Munition
- Gegner untereinander: keine Physik-Kollision, sondern Separation in der Steuerung.
Das ist günstiger und verhindert, dass Gegner sich an Engstellen verkeilen.
- Flächenschaden (Orbitalschlag, Mörser, Kapsel-Einschlag): Kreis-Test gegen alle
Gegner bzw. den Präsidenten. Kann sich selbst treffen.
- **Pooling:** Projektile werden wiederverwendet statt neu erzeugt (keine Ruckler
durch den Garbage Collector).
### Grafik und Ton
- `systems/Assets.js` lädt eigene SVGs und Kenney-Texturen. Was fehlt, erzeugt
`systems/Placeholders.js` unter demselben Schlüssel aus einfachen Formen. Das Spiel
ist also auch ganz ohne Asset-Dateien spielbar, was ein End-to-End-Test prüft.
- `systems/AudioManager.js`: Web Audio mit drei Kanälen (Gesamt, Musik, Effekte).
Klangdateien werden vorab als Bytes geladen und nach der ersten Nutzeraktion
dekodiert. Jeder Effekt hat einen synthetischen Ersatz (`Synth.js`). Die
Marschmusik wird mit Vorausplanung auf der Audio-Uhr erzeugt (`Music.js`).
- Einstellungen (Disclaimer-Bestätigung, Lautstärken, letzter Name) liegen in
`localStorage`. Ist das blockiert, fällt `systems/Settings.js` auf den
Arbeitsspeicher zurück.