181 lines
9.1 KiB
Markdown
181 lines
9.1 KiB
Markdown
# 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.
|