slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
# 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.