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

9.1 KiB
Raw Blame History

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.

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

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

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.