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.
+199
View File
@@ -0,0 +1,199 @@
# Assets: Quellen und Lizenzen
Alle Dateien unter `app/static/assets/` sind hier mit Quelle, Autor und Lizenz
aufgeführt. Es werden ausschließlich Assets mit eindeutig freier Lizenz verwendet;
keine Datei zeigt reale Personen, Marken oder Figuren aus anderen Spielen.
Ein automatischer Test (`tests/test_assets.py`) prüft, dass jede Datei im
Asset-Ordner in diesem Dokument vorkommt und jede im Spiel referenzierte Datei
existiert.
> **Ohne Assets spielbar:** Fehlt eine Datei oder schlägt das Laden fehl, erzeugt
> `app/static/js/systems/Placeholders.js` unter demselben Schlüssel eine
> Ersatzgrafik aus einfachen Formen. Klänge werden bei Bedarf synthetisch erzeugt.
## Übersicht
| Kategorie | Quelle | Autor | Lizenz |
|---|---|---|---|
| Figuren, Emblem, Geschütz, Kapsel (SVG) | eigens für dieses Projekt erstellt | Glory-Defense-Projekt | CC0 1.0 |
| Bodenkacheln | [Kenney – Top-down Shooter](https://kenney.nl/assets/top-down-shooter) | Kenney Vleugels (kenney.nl) | CC0 1.0 |
| Partikel-Texturen | [Kenney – Particle Pack](https://kenney.nl/assets/particle-pack) | Kenney Vleugels (kenney.nl) | CC0 1.0 |
| Schriften | Systemschriften (Impact, Segoe UI bzw. Ersatzschriften des Systems), **nicht mitgeliefert** | – | – |
| Soundeffekte und Jingles | Kenney – [Sci-Fi Sounds](https://kenney.nl/assets/sci-fi-sounds), [Impact Sounds](https://kenney.nl/assets/impact-sounds), [Interface Sounds](https://kenney.nl/assets/interface-sounds), [Music Jingles](https://kenney.nl/assets/music-jingles) | Kenney Vleugels (kenney.nl) | CC0 1.0 |
| Hintergrundmusik (Marsch) | eigene Komposition, zur Laufzeit synthetisiert | Glory-Defense-Projekt | CC0 1.0 |
| Spiel-Engine (Code) | [Phaser 3.90.0](https://phaser.io) | Phaser Studio Inc. | MIT |
CC0 verlangt keine Namensnennung. Kenney wird trotzdem im Spiel unter „Credits“
genannt. CC-BY-Assets werden derzeit nicht verwendet; kämen welche hinzu, müssten
sie zwingend in den Credits erscheinen.
## Eigene Vektorgrafiken (`app/static/assets/sprites/`)
Eigens für Glory Defense gezeichnet, Lizenz **CC0 1.0** (gemeinfrei). Alle
Figuren sind frei erfunden. Der Präsident trägt bewusst einen geschlossenen Helm
(kein Gesicht, keine Frisur). Sein Umhang in Rot und Gold und das Emblem der
fiktiven „Vereinigten Erde“ (auch auf dem Umhang) sind eigene Entwürfe ohne
Lorbeerzweige oder Sternenkreis, damit sie keinem realen Emblem ähneln. Die Nachschubkapsel trägt ein Geschenkband statt eines
Kreuzes, weil das Rotkreuz-Zeichen rechtlich geschützt ist.
| Datei | Inhalt |
|---|---|
| `sprites/president.svg` | Präsident Maximilian Valor (Draufsicht) |
| `sprites/krabbler.svg` | Schwärmer: Krabbler (klein) |
| `sprites/sprungkralle.svg` | Schwärmer: Sprungkralle (mittel) |
| `sprites/brutmutter.svg` | Schwärmer: Brutmutter (Elite) |
| `sprites/nieter.svg` | Blechlegion: Nieter-Drohne (klein) |
| `sprites/blechsoldat.svg` | Blechlegion: Blechsoldat (mittel) |
| `sprites/walzmeister.svg` | Blechlegion: Walzmeister (Elite) |
| `sprites/turret.svg` | Geschütz „Wachsamer Nachbar“ |
| `sprites/supply-pod.svg` | Nachschubkapsel „Fürsorgepaket“ |
| `sprites/emblem.svg` | Emblem der Vereinigten Erde (Menü, Bodenmarkierung) |
Auch `app/static/favicon.svg` ist ein eigener Entwurf (CC0).
## Kenney – Top-down Shooter (`app/static/assets/kenney/tiles/`)
- Quelle: <https://kenney.nl/assets/top-down-shooter> (Version im ZIP `kenney_top-down-shooter.zip`)
- Autor: Kenney Vleugels, [kenney.nl](https://kenney.nl)
- Lizenz: Creative Commons Zero (CC0 1.0), Lizenztext: `kenney/tiles/License.txt`
| Datei | Inhalt |
|---|---|
| `kenney/tiles/ground_strip.png` | Ausschnitt aus `Tilesheet/tilesheet_complete.png`, 8 Kacheln à 64 px: Erde A (Zeile 0, Spalte 4), Erde B (0/5), Felsen (8/20, 8/21, 8/22), Büsche (8/18, 8/19), Fleck (11/22). Im Spiel eingefärbt. |
| `kenney/tiles/License.txt` | Originale Lizenzdatei |
## Kenney – Particle Pack (`app/static/assets/kenney/particles/`)
- Quelle: <https://kenney.nl/assets/particle-pack> (Version 1.1)
- Autor: Kenney Vleugels, [kenney.nl](https://kenney.nl), mit Filtervorlagen von Indigo Ray,
Craig Nisbet, Zoltan Erdokovy, Heliagon, ThreeDee, Killst4r und Tim2501 (laut Lizenzdatei)
- Lizenz: Creative Commons Zero (CC0 1.0), Lizenztext: `kenney/particles/License.txt`
- Bearbeitung: von 512 × 512 px auf 128 × 128 px verkleinert
| Datei | Original |
|---|---|
| `kenney/particles/smoke.png` | `PNG (Transparent)/smoke_04.png` |
| `kenney/particles/fire.png` | `PNG (Transparent)/fire_01.png` |
| `kenney/particles/spark.png` | `PNG (Transparent)/spark_05.png` |
| `kenney/particles/scorch.png` | `PNG (Transparent)/scorch_02.png` |
| `kenney/particles/muzzle.png` | `PNG (Transparent)/muzzle_01.png` |
| `kenney/particles/glow.png` | `PNG (Transparent)/light_01.png` |
| `kenney/particles/License.txt` | Originale Lizenzdatei |
## Schriften
Das Spiel liefert **keine Schriftdateien** mit, sondern nutzt Systemschriften:
Impact (bzw. Arial Black oder die Standardschrift) für Titel, Segoe UI bzw.
system-ui für HUD und Texte. Damit gibt es keine Lizenzfragen.
Frühere Versionen verwendeten „Kenney Future“ aus dem Paket „Kenney Fonts“. Das
Paket nennt CC0, die in die Schriftdateien eingebetteten Metadaten aber
„Creative Commons Attribution Share Alike 3.0“ (FontStruct, 2013). Wegen dieses
Widerspruchs und weil die Schrift „Q“ kaum von „O“ und „X/K“ kaum von „H“
unterscheidet (schlecht für Tastenhinweise), wurde sie entfernt.
## Kenney – Klänge (`app/static/assets/kenney/audio/`)
Alle Klänge unverändert übernommen (OGG Vorbis). Lizenz jeweils **Creative Commons Zero (CC0 1.0)**,
Autor Kenney Vleugels ([kenney.nl](https://kenney.nl)). Welcher Klang wofür verwendet wird, steht in
`app/static/js/systems/AudioAssets.js`. Jeder Effekt hat zusätzlich einen synthetischen Ersatz
(`systems/Synth.js`), falls Dateien fehlen oder der Browser OGG nicht abspielen kann.
### Sci-Fi Sounds
- Quelle: <https://kenney.nl/assets/sci-fi-sounds>
- Lizenztext: `kenney/audio/License-SciFiSounds.txt`
| Datei |
|---|
| `kenney/audio/explosionCrunch_000.ogg` |
| `kenney/audio/explosionCrunch_001.ogg` |
| `kenney/audio/explosionCrunch_002.ogg` |
| `kenney/audio/explosionCrunch_003.ogg` |
| `kenney/audio/laserLarge_000.ogg` |
| `kenney/audio/laserLarge_001.ogg` |
| `kenney/audio/laserRetro_000.ogg` |
| `kenney/audio/laserRetro_001.ogg` |
| `kenney/audio/laserRetro_002.ogg` |
| `kenney/audio/laserSmall_000.ogg` |
| `kenney/audio/laserSmall_001.ogg` |
| `kenney/audio/laserSmall_002.ogg` |
| `kenney/audio/laserSmall_003.ogg` |
| `kenney/audio/laserSmall_004.ogg` |
| `kenney/audio/lowFrequency_explosion_000.ogg` |
| `kenney/audio/lowFrequency_explosion_001.ogg` |
| `kenney/audio/slime_000.ogg` |
| `kenney/audio/slime_001.ogg` |
| `kenney/audio/License-SciFiSounds.txt` (Lizenzdatei) |
### Impact Sounds
- Quelle: <https://kenney.nl/assets/impact-sounds>
- Lizenztext: `kenney/audio/License-ImpactSounds.txt`
| Datei |
|---|
| `kenney/audio/impactMetal_light_000.ogg` |
| `kenney/audio/impactMetal_light_001.ogg` |
| `kenney/audio/impactMetal_light_002.ogg` |
| `kenney/audio/impactPlate_light_000.ogg` |
| `kenney/audio/impactPunch_heavy_000.ogg` |
| `kenney/audio/impactPunch_heavy_001.ogg` |
| `kenney/audio/impactSoft_medium_000.ogg` |
| `kenney/audio/impactSoft_medium_001.ogg` |
| `kenney/audio/impactSoft_medium_002.ogg` |
| `kenney/audio/License-ImpactSounds.txt` (Lizenzdatei) |
### Interface Sounds
- Quelle: <https://kenney.nl/assets/interface-sounds>
- Lizenztext: `kenney/audio/License-InterfaceSounds.txt`
| Datei |
|---|
| `kenney/audio/click_001.ogg` |
| `kenney/audio/confirmation_001.ogg` |
| `kenney/audio/confirmation_002.ogg` |
| `kenney/audio/error_001.ogg` |
| `kenney/audio/maximize_001.ogg` |
| `kenney/audio/select_001.ogg` |
| `kenney/audio/tick_001.ogg` |
| `kenney/audio/License-InterfaceSounds.txt` (Lizenzdatei) |
### Music Jingles
- Quelle: <https://kenney.nl/assets/music-jingles>
- Lizenztext: `kenney/audio/License-MusicJingles.txt`
| Datei |
|---|
| `kenney/audio/jingles_HIT11.ogg` |
| `kenney/audio/jingles_HIT15.ogg` |
| `kenney/audio/jingles_PIZZI07.ogg` |
| `kenney/audio/jingles_STEEL07.ogg` |
| `kenney/audio/License-MusicJingles.txt` (Lizenzdatei) |
### Hintergrundmusik
Der Marsch ist eine eigene Komposition (`app/static/js/logic/marchPattern.js`, CC0) und wird
zur Laufzeit synthetisch erzeugt (`app/static/js/systems/Music.js`) – es gibt keine Musikdatei.
## Bibliotheken
| Datei | Quelle | Lizenz |
|---|---|---|
| `app/static/js/lib/phaser.min.js` | Phaser 3.90.0 aus dem npm-Paket `phaser` | MIT, © Phaser Studio Inc., Lizenztext `app/static/js/lib/PHASER_LICENSE.md` |
## Neue Assets hinzufügen
1. Nur Assets mit eindeutig freier Lizenz verwenden (bevorzugt CC0: Kenney.nl,
OpenGameArt.org nur CC0/CC-BY). Die Pixabay-Lizenz ist laut Projektvorgaben
erlaubt, aber keine CC-Lizenz: Nutzungsbedingungen je Datei prüfen und festhalten.
Auch die in Dateien eingebetteten Lizenzangaben prüfen (z. B. Schrift-Metadaten).
2. Datei unter `app/static/assets/` ablegen und in `app/static/js/systems/Assets.js`
eintragen (Klänge: `app/static/js/systems/AudioAssets.js`).
3. Diese Datei ergänzen (Quelle, Autor, Lizenz, Bearbeitung).
4. Bei CC-BY: Namensnennung zusätzlich in `app/static/js/content/texts.js` unter
`CREDITS` eintragen.
5. `pytest tests/test_assets.py` ausführen.
+175
View File
@@ -0,0 +1,175 @@
# Deployment auf cPanel („Setup Python App“ / Phusion Passenger)
Diese Anleitung beschreibt die Installation auf einem Shared-Hosting mit cPanel und
CloudLinux-„Setup Python App“. Die App braucht keine Hintergrundprozesse, keinen
Redis, keine WebSockets und keine kompilierten Pakete: Flask, python-dotenv und
SQLite reichen.
## Überblick
| cPanel-Feld | Wert |
|---|---|
| Python version | **3.10** oder neuer (neueste verfügbare wählen) |
| Application root | Ordner im Home-Verzeichnis, z. B. `glory_defense` |
| Application URL | Domain oder Unterpfad, z. B. `example.ch/` oder `example.ch/glory` |
| Application startup file | `passenger_wsgi.py` |
| Application Entry point | `application` |
## 1. Upload-Paket bauen
Auf dem eigenen Rechner im Projektordner (Windows, Linux oder macOS):
```bash
python scripts/build_deploy_zip.py
```
Das erzeugt `dist/glory_defense.zip` mit genau den Dateien, die der Server braucht:
`passenger_wsgi.py`, `requirements.txt`, `.env.example` und den Ordner `app/`
(inkl. `app/static/js/lib/phaser.min.js` und `app/static/assets/`). **Nicht**
enthalten sind `.env` (Geheimnisse), `instance/` (Datenbank), Tests, `.venv`,
`node_modules` und Caches. Node.js wird auf dem Server nicht benötigt.
> Bitte nicht mit „Komprimierter Ordner“ bzw. `Compress-Archive` aus Windows
> PowerShell 5.1 packen: Das schreibt Pfade mit Backslash (`app\api.py`) ins ZIP,
> und manche Entpacker auf Linux-Servern machen daraus flache Dateinamen. Dann
> startet die App mit `No module named 'app'` nicht. Das Skript verwendet immer `/`.
Alternativ kann das Repository über cPanel → **Git™ Version Control** direkt in den
Application-Root geklont werden.
## 2. Python-App anlegen
1. cPanel → **Software** → **Setup Python App** → **Create Application**.
2. Felder wie in der Tabelle oben ausfüllen.
3. **Create** klicken. cPanel legt den Ordner, eine virtuelle Umgebung
(`~/virtualenv/glory_defense/<version>/`) und eine Beispiel-`passenger_wsgi.py` an.
4. Oben auf der Seite erscheint der Befehl zum Aktivieren der venv, etwa:
```bash
source /home/BENUTZER/virtualenv/glory_defense/3.11/bin/activate && cd /home/BENUTZER/glory_defense
```
Diesen Befehl für Schritt 5 kopieren.
## 3. Dateien hochladen
1. cPanel → **File Manager** → in den Application-Root wechseln (z. B. `~/glory_defense`).
2. Die von cPanel erzeugte `passenger_wsgi.py` löschen oder überschreiben lassen.
3. `glory_defense.zip` hochladen und dort **Extract** wählen.
4. Prüfen: `passenger_wsgi.py` und der Ordner `app/` liegen **direkt** im Application-Root.
## 4. Umgebungsvariablen setzen
In „Setup Python App“ bei der App unter **Environment variables** eintragen und
danach **Save** klicken:
| Name | Wert |
|---|---|
| `SECRET_KEY` | langer Zufallswert, z. B. Ausgabe von `python -c "import secrets; print(secrets.token_hex(32))"` |
| `GLORY_DB_PATH` | optional, Standard `instance/glory_defense.sqlite3` (relativ zum Application-Root) |
| `GLORY_RATE_LIMIT_MAX` | optional, Standard `5` |
| `GLORY_RATE_LIMIT_WINDOW` | optional, Standard `600` (Sekunden) |
| `GLORY_TRUST_PROXY` | optional, Standard `0`. Nur `1`, wenn ein vertrauenswürdiger Proxy (z. B. CDN) davorsteht, siehe „Typische Fehler“ |
| `GLORY_NAME_BLOCKLIST` | optional, Standard `instance/name_blocklist.txt`, siehe „Moderation der Ehrentafel“ |
Alternativ eine Datei `.env` im Application-Root anlegen (Vorlage `.env.example`).
In cPanel gesetzte Variablen haben Vorrang vor der `.env`.
> Ohne `SECRET_KEY` erzeugt die App beim ersten Spielstart `instance/secret_key`
> selbst. Das funktioniert, ein eigener Wert in cPanel ist aber sauberer und
> übersteht Neuinstallationen.
## 5. Abhängigkeiten installieren
**Variante A, über die Oberfläche:** In „Setup Python App“ bei der App unter
*Configuration files* `requirements.txt` eintragen, **Add** und dann
**Run Pip Install** klicken.
**Variante B, per Terminal** (cPanel → **Terminal** oder SSH):
```bash
source /home/BENUTZER/virtualenv/glory_defense/3.11/bin/activate && cd /home/BENUTZER/glory_defense
pip install -r requirements.txt
```
Die Datenbank legt die App beim ersten Aufruf selbst an, ein eigener Schritt ist
nicht nötig.
## 6. Neustarten und prüfen
- In „Setup Python App“ auf **Restart** klicken, oder per Terminal:
```bash
mkdir -p tmp && touch tmp/restart.txt
```
- Im Browser aufrufen:
- `https://example.ch/healthz` muss `{"status":"ok"}` liefern
- `https://example.ch/` zeigt den Disclaimer und danach das Hauptmenü
- `https://example.ch/api/highscores` liefert `{"highscores":[]}`
Läuft die App unter einem Unterpfad (z. B. `/glory`), funktionieren alle Links
automatisch: Das Template erzeugt URLs über `url_for`, und Passenger übergibt den
Pfad als `SCRIPT_NAME`.
## 7. Updates einspielen
1. Lokal `python scripts/build_deploy_zip.py` ausführen, das neue ZIP hochladen und
entpacken (vorhandene Dateien überschreiben). `instance/` dabei **nicht** löschen,
denn dort liegen Highscores, Sperrliste und ggf. der automatisch erzeugte Schlüssel.
2. Falls sich `requirements.txt` geändert hat: **Run Pip Install** bzw. `pip install -r requirements.txt`.
3. **Restart** (oder `touch tmp/restart.txt`).
4. Im Browser mit Strg+F5 neu laden, damit keine alten JavaScript-Dateien aus dem Cache kommen.
## 8. Datensicherung
Zu sichern ist nur der Ordner `instance/`:
- `instance/glory_defense.sqlite3`: Highscores
- `instance/name_blocklist.txt`: Sperrliste, falls angelegt
- `instance/secret_key`: nur vorhanden, wenn kein `SECRET_KEY` gesetzt ist
## 9. Moderation der Ehrentafel
Spielernamen erscheinen öffentlich. Unpassende Einträge (Beleidigungen, Namen realer
Personen) lassen sich im Terminal löschen. Zuerst die venv aktivieren (Befehl aus
Schritt 2.4), dann:
```bash
flask --app passenger_wsgi scores list # Einträge mit ID anzeigen
flask --app passenger_wsgi scores delete 17 23 # Einträge per ID löschen
flask --app passenger_wsgi scores delete-name "Name" # alle Einträge mit diesem Namen löschen
```
Die Befehle brauchen keinen `SECRET_KEY`. Das ist wichtig, denn in cPanel gesetzte
Umgebungsvariablen stehen im Terminal nicht zur Verfügung. Eine `.env` im
Application-Root wird dagegen auch hier gelesen.
**Sperrliste (optional):** Die Datei `instance/name_blocklist.txt` anlegen, ein Begriff
pro Zeile, `#` leitet Kommentare ein. Namen, die einen dieser Begriffe enthalten,
lehnt die API ab. Groß-/Kleinschreibung, Leerzeichen und Satzzeichen spielen dabei
keine Rolle, „Max Muster“ sperrt also auch „max.muster“. Die Datei wird bei jeder
Einreichung neu gelesen, ein Neustart ist nicht nötig.
## Typische Fehler
| Symptom | Ursache und Lösung |
|---|---|
| „Incomplete response received from application“ oder Fehler 500 | Details stehen in `stderr.log` im Application-Root bzw. in cPanel → *Errors*. Meist fehlen die Abhängigkeiten (Schritt 4) oder die Startdatei ist falsch eingetragen. |
| `ModuleNotFoundError: No module named 'app'` | `passenger_wsgi.py` liegt nicht im Application-Root, der Ordner `app/` fehlt bzw. liegt eine Ebene zu tief (ZIP in einen Unterordner entpackt), oder das ZIP wurde mit Backslash-Pfaden erstellt (Dateien heißen dann `app\api.py`). Paket mit `python scripts/build_deploy_zip.py` bauen. |
| `ModuleNotFoundError: No module named 'flask'` | Pip-Install lief in der falschen Umgebung. Befehl aus Schritt 2.4 verwenden (venv aktivieren) und erneut installieren (Schritt 5). |
| `SyntaxError` oder Flask-Importfehler beim Start | Python-Version zu alt. In „Setup Python App“ **3.10 oder neuer** wählen (die App kann dafür neu angelegt werden, Dateien bleiben erhalten). |
| `sqlite3.OperationalError: unable to open database file` | Der Ordner der Datenbank ist nicht beschreibbar. `instance/` muss dem cPanel-Benutzer gehören (Rechte 755). `GLORY_DB_PATH` nicht auf einen fremden Ordner setzen. |
| `database is locked` bei viel Last | SQLite wartet bis zu 10 s auf Sperren. Bei sehr vielen gleichzeitigen Einreichungen hilft ein kleineres Rate-Limit. Für ein Spiel mit Highscore-Liste reicht SQLite in der Regel. |
| Seite lädt, Spiel bleibt schwarz, Konsole zeigt 404 für `phaser.min.js` | `app/static/js/lib/phaser.min.js` wurde nicht mit hochgeladen (manche FTP-Clients überspringen große Dateien). Das Upload-ZIP aus Schritt 1 enthält sie. |
| Änderungen nicht sichtbar | App neu starten (Schritt 6) und Browser-Cache leeren (Strg+F5). |
| Alle Spieler teilen sich ein Rate-Limit | Ein vorgeschalteter Proxy (z. B. CDN) liefert allen dieselbe IP. Wenn der Proxy `X-Forwarded-For` zuverlässig setzt, `GLORY_TRUST_PROXY=1` setzen. Sonst beim Hoster `mod_remoteip` erfragen. |
| `/healthz` ok, aber Highscores werden mit „Run-Token ungültig“ abgelehnt | `SECRET_KEY` wurde zwischen Spielstart und Einreichen geändert (z. B. durch Neustart mit neuem Wert). Einfach eine neue Runde spielen. |
## Sicherheitshinweise
- Die App setzt eine strikte Content-Security-Policy. Das Spiel lädt keine Skripte
von fremden Servern.
- IP-Adressen werden nicht gespeichert, nur ein HMAC-Hash (mit `SECRET_KEY`) für das
Rate-Limit. Einträge werden nach Ablauf des Zeitfensters automatisch gelöscht.
- Spielernamen werden bereinigt (max. 16 Zeichen, keine Steuer- oder Sonderzeichen)
und im Frontend immer als Text ausgegeben, nie als HTML. Unpassende Einträge lassen
sich löschen oder per Sperrliste verhindern (Abschnitt 9).
- Mit `GLORY_TRUST_PROXY=1` übernimmt die App vom Proxy nur Client-IP und Protokoll,
nicht Host oder Pfadpräfix. So lassen sich keine fremden URLs in die Seite schleusen.
+211
View File
@@ -0,0 +1,211 @@
# Game Design
Alle Werte stehen zentral in `app/static/js/config.js`. Die Tabellen hier geben den
Stand zum Zeitpunkt des Schreibens wieder. Bei Abweichungen gilt `config.js`.
Einheiten: Pixel (px), px/s, Millisekunden (ms), Lebenspunkte (LP).
## Welt und Ton
- **Setting:** Die fiktive „Vereinigte Erde“ wird von zwei außerirdischen Fraktionen
angegriffen. Präsident Maximilian Valor verteidigt „den ewigen Frieden“ persönlich.
- **Satire:** Durchsagen des „Ministeriums für Zuversicht“ zwischen den Wellen,
Briefings, Game-Over-Texte. Der Spott gilt Militarismus und Propaganda im
Allgemeinen, nicht realen Personen, Parteien oder Ländern. Namen und Begriffe sind
eigenständig. Ein Test (`tests/test_assets.py`) verhindert Begriffe aus bekannten
Vorbildern.
- **Arena:** 2400 × 1600 px, größer als der Bildschirm (1280 × 720). Die Kamera folgt
dem Präsidenten. Gegner außerhalb des Bildes zeigt das HUD als Randpfeile an.
## Der Präsident
| Wert | |
|---|---|
| Leben | 100 LP |
| Tempo | 240 px/s (diagonal gleich schnell) |
| Hitbox | Kreis, Radius 18 px |
| Unverwundbarkeit nach Treffer | 500 ms (sonst tötet ein Schwarm in einem Frame) |
| Ausweichrolle | 640 px/s für 320 ms, unverwundbar, Abklingzeit 1500 ms. Ohne Bewegung in Blickrichtung. |
| Heilung nach jeder Welle | 15 LP |
Treffer am Präsidenten beenden die Punkteserie. Gegnerprojektile fliegen während der
Rolle hindurch, statt verschluckt zu werden.
## Waffen
| | Karabiner »Zuversicht« | Schrotflinte »Überzeugungskraft« | Dienstpistole »Anstand« |
|---|---|---|---|
| Taste | 1 | 2 | 3 |
| Modus | Dauerfeuer | Einzelschuss | Einzelschuss |
| Schaden | 14 | 8 × 11 (Schrot) | 16 |
| Feuerpause | 110 ms | 650 ms | 260 ms |
| Magazin / Reserve (Start, max.) | 30 / 180, 270 | 6 / 24, 48 | 12 / unbegrenzt |
| Nachladen | 1,5 s | 1,9 s | 1,1 s |
| Streuung | 3° | 24° Fächer | 2° |
| Reichweite | 820 px | 420 px | 700 px |
| Schaden/s (Feuerstoß / mit Nachladen) | 127 / 88 | 135 / 91 | 62 / 45 |
- Leeres Magazin lädt automatisch nach. Ein Waffenwechsel bricht das Nachladen ab.
- Sind Magazin und Reserve leer, wechselt das Spiel automatisch zur Dienstpistole.
Ohne Munition ist man also nie wehrlos.
- Die Nachschubkapsel füllt die Reserve aller Waffen auf das Maximum.
## Gegner
### Die Schwärmer (insektoid, Nahkampf)
| | Krabbler (klein) | Sprungkralle (mittel) | Brutmutter (Elite) |
|---|---|---|---|
| Leben | 20 | 60 | 450 |
| Tempo | 175 | 125 | 58 |
| Kontaktschaden (Abklingzeit) | 6 (700 ms) | 14 (800 ms) | 28 (1000 ms) |
| Punkte | 10 | 30 | 250 |
| Budgetkosten | 1 | 3 | 15 |
| Gruppengröße | 4–8 | 1–3 | 1 |
| Verhalten | Umzingeln | Sprungangriff | Brüten |
- **Umzingeln:** Jeder Krabbler bekommt einen eigenen Anflugwinkel auf einem Kreis
(130 px) um den Präsidenten. Ab 170 px Abstand greift er direkt an. Ein Rudel
kommt dadurch von mehreren Seiten.
- **Sprungangriff:** Verfolgen. Ab 260 px setzt die Sprungkralle 450 ms an: sie steht
still, färbt sich rot und wird größer. Dann springt sie mit 560 px/s für 380 ms in
die beim Ansetzen festgelegte Richtung. Wer auf die Vorwarnung reagiert, kann
ausweichen. Abklingzeit 2,4 s.
- **Brüten:** Langsames Verfolgen, alle 5 s schlüpfen bis zu 3 Krabbler, höchstens 6
gleichzeitig lebende Nachkommen.
### Die Blechlegion (Roboter, Fernkampf)
| | Nieter-Drohne (klein) | Blechsoldat (mittel) | Walzmeister (Elite) |
|---|---|---|---|
| Leben | 30 | 90 | 700 |
| Tempo | 95 | 78 | 44 |
| Wunschabstand | 220–340 px | 200–380 px | 260–460 px |
| Angriff | Einzelschuss 7, alle 1,5 s | 3er-Salve à 8, alle 2,3 s | Fächer aus 7 × 10 (50°), alle 2,7 s + Mörser |
| Projektiltempo | 330 | 400 | 290 |
| Punkte | 20 | 50 | 400 |
| Budgetkosten | 2 | 4 | 18 |
| Gruppengröße | 2–3 | 1–3 | 1 |
| Verhalten | Abstand halten | Flankieren | Belagern |
- **Abstand halten:** Zu weit weg → annähern, zu nah → zurückweichen, dazwischen seitlich
ausweichen. Die Richtung wechselt zufällig alle 1,4–3,2 s. So sind Fernkämpfer
kein stehendes Ziel.
- **Flankieren:** Ziel ist ein Punkt, der gegenüber der Linie Präsident→Gegner um 70°
gedreht ist (Abstand 290 px). Der Blechsoldat schwenkt also seitlich ein, statt
frontal anzulaufen. Dort angekommen, hält er Abstand.
- **Mörser (Walzmeister):** Alle 5,2 s zielt er auf die aktuelle Position des
Präsidenten. Eine rote Bodenmarkierung (Radius 90 px) füllt sich 1,4 s lang, dann
folgt der Einschlag (26 Schaden). Stehenbleiben ist keine Option.
- Frisch gespawnte Gegner schießen frühestens nach etwa 0,5–1,6 s. Das ist fair,
weil sonst Salven aus dem Nichts kämen.
### Allgemeine KI
Alle Bewegungen sind Steuerungsvektoren (`logic/steering.js`), die sich überlagern.
Eine Separation (Radius 34 px + Gegnerradius) schiebt Gegner auseinander und
verhindert Klumpen. Die Geschwindigkeit wird weich nachgeführt (18 % pro Frame),
damit Richtungswechsel nicht ruckeln. Nahkämpfer bleiben am Rand des Präsidenten
stehen, statt sich auf seinen Mittelpunkt zu drängen.
## Orbital-Befehle
Eingabe: **Q gedrückt halten** und die Richtungsfolge mit Pfeiltasten oder WASD
eingeben. Während der Eingabe steht der Präsident still. Das Ziel ist die
Mausposition, begrenzt auf 560 px Entfernung und die Arena. Falsche Eingaben leeren
den Puffer, man kann sofort neu ansetzen. Keine Folge ist der Anfang einer anderen,
und jede beginnt mit einer anderen Richtung (wird getestet). Die Codes sind eigene
Erfindungen. Der Orbitalschlag-Code ↑ → ↓ ↓ stammt aus der Projektvorgabe. Ein Test
stellt sicher, dass keiner der Codes einem Code aus einem bekannten Spiel mit
ähnlicher Mechanik entspricht.
| Befehl | Code | Verzögerung | Abklingzeit | Wirkung |
|---|---|---|---|---|
| Orbitalschlag »Morgenröte« | ↑ → ↓ ↓ | 2,5 s | 30 s | Radius 150 px, 260 Schaden an Gegnern, **40 am Präsidenten**, falls er im Radius steht |
| Nachschubkapsel »Fürsorgepaket« | ← ← ↓ → | 3 s | 45 s | Einschlag: Radius 70 px, 120 Schaden (20 am Präsidenten). Danach liegt 30 s eine Kapsel: +50 LP, volle Reserve. |
| Geschütz »Wachsamer Nachbar« | → ← → ↑ ↑ | 3 s | 60 s | Einschlag: Radius 60 px, 80 Schaden (15 am Präsidenten). Danach 25 s Geschütz: zielt auf den nächsten Gegner (480 px), 12 Schaden alle 150 ms |
Bis zum Einschlag zeigt eine Bodenmarkierung den genauen Wirkungsradius, einen
Strahl aus dem Orbit und einen Countdown. Kapsel und Geschütz blinken in ihren
letzten 3 Sekunden.
## Wellen
- **Budget:** `B(n) = 8 + 5 · (n − 1)^1,15`, gerundet. Gegner „kosten“ Budget
(Tabellen oben).
- **Fraktionen:** Wellen 1–2 nur Schwärmer, 3–4 nur Blechlegion, ab 5 gemischt.
- **Eliten:** jede 5. Welle, Anzahl = Welle / 5 (max. 4). Die erste Elite ist eine
Brutmutter, danach wechselt die Fraktion.
- **Zusammenstellung:** Zuerst werden Eliten gekauft (mindestens 30 % des Budgets
bleiben für Fußvolk). Dann gehen `10 % + 4 % · (n − 1)` des Restbudgets
(max. 50 %) in mittlere Gegner, der Rest in kleine. Gruppengrößen je Typ.
- **Ablauf:** Gruppen erscheinen nacheinander (1,3–3 s Abstand, in späten Wellen bis
zu 55 % dichter) an zufälligen Arenarändern, nie näher als 420 px am Präsidenten.
Leben mehr als 60 Gegner, wartet der Spawner.
- **Skalierung ab Welle 6:** +7 % Leben und +1,2 % Tempo pro Welle (Tempo max. +30 %).
- **Pause:** 8 s zwischen den Wellen mit Propaganda-Durchsage (ohne direkte
Wiederholung), Wellenbonus und Heilung.
| Welle | Budget | Eliten | Leben × | Tempo × | Ø Gegner |
|---|---|---|---|---|---|
| 1 | 8 | 0 | 1,00 | 1,00 | 8 |
| 3 | 19 | 0 | 1,00 | 1,00 | 9 |
| 5 | 33 | 1 | 1,00 | 1,00 | 14 |
| 10 | 71 | 2 | 1,35 | 1,06 | 23 |
| 15 | 112 | 3 | 1,70 | 1,12 | 37 |
| 20 | 156 | 4 | 2,05 | 1,18 | 52 |
(Ø Gegner ohne ausgebrütete Krabbler, gemittelt über 50 Zufalls-Seeds.)
## Punkte
- Abschuss: Punkte des Gegnertyps × Multiplikator
- **Serie:** Jeder Abschuss innerhalb von 3 s nach dem letzten setzt die Serie fort.
Alle 5 Abschüsse steigt der Multiplikator um 0,5, bis maximal ×4.
- Ein Treffer am Präsidenten beendet die Serie.
- **Wellenbonus:** 100 × Wellennummer (ohne Multiplikator)
### Plausibilitätsgrenzen im Backend
`app/config.py` (`GAME_RULE_DEFAULTS`) lehnt Einreichungen ab, wenn
1. die gemeldete Spieldauer länger ist als die Zeit seit Ausgabe des Run-Tokens
(+30 s Toleranz). Pausen zählen nicht zur Spieldauer, daher darf sie kürzer sein.
2. die Welle in der Spieldauer nicht erreichbar ist: `Dauer ≥ (Welle − 1) · 5 s`,
denn jede Pause dauert 8 s.
3. die Punkte über `2000 + 200 · Dauer in s` liegen.
Herleitung der 200 Punkte/s: Testläufe (Bot mit perfektem Zielen) liegen bei etwa
15–20 Punkten/s. Eine grobe Abschätzung für eine sehr starke Runde (Welle 25 nach
20 Minuten, Multiplikator dauerhaft hoch) ergibt 80–120 Punkte/s im Schnitt. 200/s
im **Durchschnitt über die ganze Runde** lässt also Luft, schneidet aber
Fantasiewerte ab. Der Sockel von 2000 deckt kurze Runden mit Glückstreffern ab. Wer
Punktwerte in `config.js` stark erhöht, muss diese Grenze mit anpassen.
Wie jede Prüfung im Backend verhindert das kein gezieltes Cheaten (die Spiellogik
läuft im Browser). Für Ausreißer gibt es die Moderationsbefehle (siehe
`docs/DEPLOYMENT_CPANEL.md`).
## Schwierigkeit
Ein Testbot (perfektes Zielen, weicht nur zurück, ohne Rolle, Orbital-Befehle und
Nachschub) erreicht Welle 5 nach etwa 2 Minuten. Die erste Elite-Welle mit Brutmutter
ist bewusst die erste echte Hürde. Wer Rolle, Orbitalschlag und Nachschub nutzt,
kommt deutlich weiter. Stellschrauben, wenn das Spiel zu schwer oder zu leicht wirkt:
- `WAVES.budgetGrowth` / `budgetExponent`: Größe der Wellen
- `WAVES.hpScalePerWave`: Zähigkeit in späten Wellen
- `PLAYER.waveClearHeal`, `ORBITAL.commands[supply].heal`: Heilung
- `ENEMY_TYPES.*.contactDamage` / `attack.damage`: Schaden der Gegner
## Rückmeldung (Feedback)
- Trefferblitz (weiß), Funken in Fraktionsfarbe, Punkte als schwebender Text
- neue Multiplikatorstufe als „SERIE ×2“ über dem Präsidenten
- Explosionen mit Feuer, Rauch, Lichtblitz, Brandspur und Wirkungsring (zeigt den Radius)
- Bildschirmwackeln bei Treffern und Explosionen, rote Vignette bei Schaden,
pulsierender Rand unter 30 % Leben
- Ausweichrolle mit Nachbild
- Randpfeile für Gegner außerhalb des Bildes (Eliten in Gold)
- Ton: Kenney-Klänge mit leichter Tonhöhenstreuung, synthetischer Ersatz,
Marschmusik (in der Pause gedämpft), Jingles für Wellenstart, Wellenende,
Elite-Alarm und Game Over
+103
View File
@@ -0,0 +1,103 @@
# Tests
Das Projekt hat drei Testebenen:
| Ebene | Werkzeug | Ort | Umfang |
|---|---|---|---|
| Backend, Assets, Deployment | pytest | `tests/*.py` | Routen, API inkl. Validierung, Rate-Limit, Tokens, Datenbank, Konfiguration, Moderation, Passenger-Import, Upload-Paket, Asset-Lizenzen, Inhaltsprüfung |
| Spiellogik (JavaScript) | Vitest | `tests/js/*.test.js` | Wellen, Punkte, Combo-Eingabe, Abklingzeiten, Gegner-KI, Waffen, Formatierung, Einstellungen, API-Client, Audio, Tastaturhelfer, Konfiguration |
| End-to-End | Playwright (pytest) | `tests/e2e/test_smoke.py` | Seite lädt, Disclaimer, Spielstart, Schießen, Pause, Orbital-Befehle per Tastatur, Game Over mit Highscore, Spiel ohne Asset-Dateien, keine JavaScript-Fehler |
## Vorbereitung
```bash
python -m venv .venv
# Windows: .\.venv\Scripts\Activate.ps1 – Linux/macOS: source .venv/bin/activate
python -m pip install -r requirements-dev.txt
npm install # nur für Vitest und ESLint (Node.js 22 ab 22.13 oder 24)
```
## Python-Tests (inkl. End-to-End)
```bash
pytest # alles
pytest -m "not e2e" # ohne Browser
pytest -m e2e # nur End-to-End
pytest tests/test_api_highscores.py -k rate_limit # gezielt
```
Die End-to-End-Tests starten den Flask-Server selbst (zufälliger Port, temporäre
Datenbank). Als Browser nehmen sie das installierte **Microsoft Edge** oder
**Google Chrome**, ein zusätzlicher Download ist nicht nötig. Ist keiner vorhanden:
```bash
python -m playwright install chromium
GLORY_E2E_BROWSER=chromium pytest -m e2e # PowerShell: $env:GLORY_E2E_BROWSER="chromium"; pytest -m e2e
```
`GLORY_E2E_BROWSER` erzwingt einen Kanal (`msedge`, `chrome` oder `chromium`). Findet
Playwright gar keinen Browser, werden die E2E-Tests **übersprungen** (mit
Begründung in der Ausgabe) statt fehlzuschlagen.
Hinweis: Die E2E-Tests warten per `page.evaluate` statt `page.wait_for_function`.
Letzteres nutzt intern `eval`, und das blockiert die strenge Content-Security-Policy
der App zu Recht. So wird mit der echten CSP getestet.
## JavaScript-Tests
```bash
npm test # einmalig
npm run test:watch # bei Änderungen neu
```
Die getestete Logik in `app/static/js/logic/` importiert kein Phaser und läuft direkt
in Node. Systeme mit Browser-Abhängigkeit (Audio, Einstellungen, API-Client) werden mit
kleinen Attrappen getestet (Fake-`AudioContext`, Fake-`fetch`, Speicher statt
`localStorage`).
## Linting
```bash
ruff check .
ruff format --check . # „ruff format .“ formatiert
npm run lint # ESLint für app/static/js, tests/js, scripts
```
Hinweis für Windows PowerShell 5.1: Befehle nicht mit `&&` verketten (das kennt erst
PowerShell 7), sondern nacheinander ausführen. Ist die venv nicht aktiviert, jeden
Befehl über `.\.venv\Scripts\python.exe -m …` aufrufen, z. B.
`.\.venv\Scripts\python.exe -m pytest` oder `.\.venv\Scripts\python.exe -m ruff check .`.
`ruff` prüft mit `target-version = "py310"`, dass kein Code neuere Python-Features
braucht, als cPanel-Hosts typischerweise anbieten.
## Was wo getestet wird
| Anforderung | Tests |
|---|---|
| Alle Routen | `tests/test_routes.py` |
| Highscore-API, Validierung, ungültige Eingaben | `tests/test_api_highscores.py`, `tests/test_validation.py` |
| Rate-Limiting | `tests/test_api_highscores.py` (`test_rate_limit_*`), `tests/test_models.py` |
| Datenbank-Initialisierung | `tests/test_models.py` |
| `passenger_wsgi.py` importiert die App (auch aus fremdem Arbeitsverzeichnis) | `tests/test_passenger_wsgi.py` |
| Wellengenerierung | `tests/js/waves.test.js`, `tests/js/waveDirector.test.js` |
| Punkteberechnung | `tests/js/scoring.test.js` |
| Tastenkombinationen der Orbital-Befehle | `tests/js/comboInput.test.js`, E2E `test_orbital_commands_via_keyboard` |
| Gegner-KI | `tests/js/steering.test.js`, `tests/js/enemyAi.test.js` |
| Seite lädt, Disclaimer, Spielstart, keine JS-Fehler | E2E `test_page_loads_disclaimer_then_game_starts` |
| Spielbar mit Platzhaltern | E2E `test_game_runs_with_placeholders_when_assets_are_missing` |
| Assets dokumentiert, keine fremden Namen | `tests/test_assets.py` |
| Eigene Befehlscodes (nicht aus anderen Spielen) | `tests/js/config.test.js` |
| Moderation (CLI, Sperrliste) | `tests/test_moderation.py` |
| Upload-Paket für cPanel (vollständig, ohne Geheimnisse, startet entpackt) | `tests/test_deploy_zip.py` |
| Proxy-Header, Schlüssel erst bei Bedarf | `tests/test_routes.py`, `tests/test_config.py` |
## Neue Tests schreiben
- Neue Spiellogik möglichst in `app/static/js/logic/` ohne Phaser-Import anlegen und
in `tests/js/` testen.
- Backend-Fixtures (`app`, `client`, `issue_token`, `valid_payload`) stehen in
`tests/conftest.py`. `issue_token(started_ago=…)` simuliert eine bereits laufende
Runde, ohne zu warten.
- Für E2E-Tests den Hilfsbefehl `_accept_disclaimer_and_start(page)` nutzen. Der
Spielzustand ist über `window.gloryDefense` (Phaser-Game-Instanz) erreichbar.
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB