# 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
Phaser 3 + ES-Module"] -- "GET /, /disclaimer, /static/…" --> Flask Browser -- "POST /api/runs
GET/POST /api/highscores" --> Flask Flask["Flask-App
(passenger_wsgi.py → create_app)"] --> DB[("SQLite
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
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
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.