slop game oder so
This commit is contained in:
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 |
Reference in New Issue
Block a user