Initial commit — Godot space roguelite source

- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay)
- ItemDB: static preload list instead of DirAccess scan (export fix)
- All 18 items with EN localisation (name_en, desc_en, category_en)
- Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank
- Quasar: SMBH visual, jet boost, merge, push, BH-eating
- Atlas & UI text updated EN+DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 14:38:09 +02:00
commit edc40f9008
108 changed files with 10068 additions and 0 deletions
+431
View File
@@ -0,0 +1,431 @@
# spacel — Architektur-Dokumentation
## Überblick
**spacel** ist ein 2D-Space-Shooter mit Roguelite-Elementen, gebaut in **Godot 4.6** (Forward Plus, D3D12). Das Spiel unterstützt 12 Spieler (Split-Keyboard). Es gibt keine externen Assets — alles wird prozedural gezeichnet und synthetisiert (keine Sprites, keine Audio-Dateien).
---
## Projektstruktur
```
spacel/
├── project.godot # Projekt-Konfiguration, Autoloads, Input-Map
├── scenes/
│ ├── main.tscn # Root-Scene (enthält alles)
│ ├── game_world.tscn # Spielwelt / Hintergrund-Canvas
│ ├── hud.tscn # HUD (CanvasLayer)
│ └── ship_select.tscn # Schiff-Auswahlbildschirm
├── scripts/
│ ├── main.gd # State Machine — Spielfluss-Controller
│ ├── game_world.gd # Spielwelt: Simulation, Rendering, Kollisionen
│ ├── spaceship.gd # Spieler-Schiff (Datenklasse)
│ ├── enemy_ship.gd # Gegner-Schiff mit KI
│ ├── boss_ship.gd # Bosse: WRAITH (Welle 5) & LEVIATHAN (Welle 8)
│ ├── bullet.gd # Projektil
│ ├── black_hole.gd # Schwarzes Loch mit Gravitation + Supernova
│ ├── big_wipe.gd # Big-Wipe-Event (Bildschirm-Reset)
│ ├── cosmic_objects.gd # Alle Weltraum-Objekte (Star, Planet, ...)
│ ├── ship_stats.gd # Roguelite-Stat-System
│ ├── item_db.gd # Item-Datenbank (20 Items, 4 Seltenheiten)
│ ├── hud.gd # HUD-Logik
│ ├── hud_draw.gd # HUD-Rendering
│ ├── settings.gd # Einstellungen (Autoload)
│ ├── sound_manager.gd # Procedurales Audio (Autoload)
│ ├── music_player.gd # Musik
│ ├── shop_ui.gd # Shop zwischen den Wellen
│ ├── ship_select.gd # Schiff-Auswahl UI
│ ├── main_menu.gd # Hauptmenü
│ ├── pause_menu.gd # Pause-Menü
│ └── tr.gd # Lokalisierung (Autoload, de/en)
└── addons/godot_mcp/ # MCP-Plugin für KI-gestützte Entwicklung
```
---
## Autoloads (Singletons)
| Name | Script | Funktion |
|------|--------|----------|
| `Settings` | `settings.gd` | Lautstärke, Grafik, Sprache — persisted in `user://settings.cfg` |
| `SoundManager` | `sound_manager.gd` | Alle SFX prozedural generiert (Sinus/Säge/Square-Wellen, kein Audio-File) |
| `Tr` | `tr.gd` | Übersetzungen DE/EN |
| MCP-Dienste | `addons/godot_mcp/` | Nur für Entwicklung (Editor-Plugin) |
---
## State Machine (`main.gd`)
`main.gd` ist der zentrale Controller. Er verwaltet alle UI-Panels und schaltet zwischen folgenden Zuständen um:
```
MAIN_MENU → SELECT → (SELECT_P2) → LAUNCHING → PLAYING ⟷ PAUSED
RETURNING → SHOP → LAUNCHING (nächste Welle)
GAMEOVER → MAIN_MENU
WAVE_CLEAR → SHOP
```
**Wichtige Variablen in `main.gd`:**
- `lives_p1 / credits_p1 / stats_p1 / owned_items_p1` — Run-Zustand Spieler 1
- `wave_number` — aktuelle Wellennummer (steigt nach jedem Shop)
- `SHIPS[]` — 4 Schiffe (NOVA-1, INFERNO, AURORA, TITAN) mit Farbpaletten und Basis-Stats
**Schiff-Spielstile** (`_apply_ship_base_stats()` in `main.gd`):
| ID | Name | Besonderheit |
|----|------|--------------|
| `classic` | NOVA-1 | 1 Schutzschild — ausgewogener Einstieg |
| `inferno` | INFERNO | Speed +28 %, Feuerrate +55 %, Kurve 28 % · Passiv: Ramm-Schaden ab ≥ 4.5 px/s |
| `aurora` | AURORA | Kurve +55 %, Speed 20 %, 2 Schilde, BH-Resist 55 %, 2× Unverwundbarkeitszeit |
| `titan` | TITAN | Speed 28 %, Kurve 15 % · Aktiv: Boost-Impuls (SHIFT, 5 s Cooldown) |
`ship_id: String` in `ShipStats` wird beim Spielstart gesetzt und ermöglicht runtime-Verzweigungen (z. B. INFERNO-Rammen in `game_world.gd`).
**Hauptlogik:**
- `_set_state(new_state)` — zeigt/versteckt UI, initialisiert `game_world`
- `_process()` — Countdown vor Spielstart, Return-/Wave-Timer
- `add_credits(player, amount)` — Credits dem Spieler gutschreiben
- `on_game_over() / on_wave_complete()` — von `game_world` aufgerufen
---
## Spielwelt (`game_world.gd`)
Das Herzstück des Spiels. `game_world.gd` ist ein `Node2D` und macht **alles selbst** per `_draw()` — keine Child-Nodes für Spielobjekte.
### Physik-Loop
Fixed-Timestep bei **60 Hz** (`PHYS_DT = 1/60`):
```gdscript
_process(delta):
_phys_accum += delta # akkumuliert reale Zeit
while _phys_accum >= PHYS_DT:
_phys_accum -= PHYS_DT
frame += 1
_tick(PHYS_DT)
queue_redraw()
```
### `_tick(dt)` — Reihenfolge pro Frame
1. `_handle_input(dt)` — Spieler-Input lesen, Schüsse erzeugen
2. `_update_objects(dt)` — Alle kosmischen Objekte updaten, Gegner KI
3. `_update_bullets()` — Projektile bewegen, tote entfernen
4. Boss-Update (falls vorhanden) → erzeugt Bullets
5. `_update_particles(dt)` — Partikeleffekte
6. `_check_collisions()` — Bullet/Enemy, Bullet/Player, Antimatter/Player, Bullets/Boss
7. `_check_big_wipe()` — Threshold prüfen (> 500 Objekte)
8. BigWipe-Update
9. Schwierigkeit + Wave-Timer + Credit-Trickle
10. All-Dead-Check → `main_node.on_game_over()`
11. Kometen- und Antimatter-Spawn-Timer
### Arrays der Spielobjekte
```gdscript
var stars, planets, nebulae, comets, galaxies: Array
var black_holes, quasars, white_holes, neutron_stars: Array
var antimatter, antimatter_stars: Array
var bullets, particles: Array
var players, enemies: Array
```
### Rendering (`_draw()`)
Alles wird mit Godot's Canvas-API gezeichnet — **keine Sprites**:
- `draw_rect()` für Pixel/Rumpfe
- `draw_circle()` für Schwarze Löcher, White Holes
- `draw_arc()` für Akkretionsscheiben, Ringe
- `draw_line()` für Trails
**Zeichenreihenfolge:** Nebulae → Sterne → Galaxien → Planeten → Kometen → Quasare → White Holes → Neutronensterne → Schwarze Löcher → Antimatter → Bullets → Enemies → Boss → Spieler → Partikel → BigWipe-Overlay
---
## Datenklassen (alle `extends RefCounted`)
Alle Spielobjekte sind **keine Nodes** — sie sind reine Datenklassen, die von `game_world` in Arrays verwaltet werden. Das verhindert den Node-Overhead und ermöglicht den eigenen Physik-Loop.
### `Spaceship` (`spaceship.gd`)
Spieler-Schiff.
| Eigenschaft | Wert | Beschreibung |
|-------------|------|--------------|
| `THRUST` | 0.28 | Beschleunigung pro Frame |
| `TURN_SPEED` | 0.08 | Drehgeschwindigkeit (rad/frame) |
| `MAX_SPEED` | 7.5 | Maximale Geschwindigkeit |
| `DRAG` | 0.985 | Trägheit (Reibung) |
| `INVULN_FRAMES` | 90 | ~1.5s Unverwundbarkeit nach Treffer |
| `stats.ship_id` | String | Schiff-ID für runtime-spezifische Mechaniken |
**Methoden:**
- `update(thrust, turn, W, H, delta)` — Bewegung, Screen-Wrap, Trail
- `shoot_burst() → Array[Bullet]` — erzeugt 1N Bullets je nach `stats.bullet_count`
- `draw(canvas, frame)` — malt Rumpf aus `HULL_PIXELS`-Tabelle mit Heading-Rotation
**Screen-Wrap:** Wenn Schiff den Bildschirmrand verlässt, erscheint es auf der anderen Seite (Toroidal).
### `EnemyShip` (`enemy_ship.gd`)
KI-Gegner. 2 Farbvarianten (idx 0 = Rot, idx 1 = Cyan). Jeder Gegner hat eine **Role** (AGGRO / CIRCLE / FLANK), die per `role_id % 3` beim Spawn bestimmt wird und über Respawns hinweg erhalten bleibt. `role_id` wird in `game_world.gd` als laufender Index vergeben.
**Rollen:**
- **AGGRO**: stürmt direkt auf den nächsten Spieler zu
- **CIRCLE**: hält Abstand (~180 px) und kreist kontinuierlich um den Spieler (orbit_offset dreht sich)
- **FLANK**: nähert sich aus ±90°-Flankenwinkel statt frontal
**Gemeinsamkeiten:**
- Schuss immer direkt auf Spieler gezielt (unabhängig von der Bewegungsrichtung) → CIRCLE/FLANK-Gegner bleiben gefährlich
- **Separation Force**: Gegner stoßen sich gegenseitig ab (Radius 110 px) → kein Clumping
- **Patrol**: außerhalb Reichweite navigieren Gegner zu zufälligen Zielpunkten auf dem Bildschirm (nicht mehr reines Heading-Drehen) → natürliche Verteilung
- **Schwarzloch-Ausweichen**: spürt BH-Radius und weicht ab
- Feuer-Timer ist pro `role_id` gestaffelt (FIRE_INTERVAL + rid×17) → Salven verteilt
- Nach Tod: respawnt nach 48s vom zufälligen Bildschirmrand, behält Role bei
**Wellenskalierung:**
- Alte Formel: `2 + (wave-1)/2` (cap 6→8 nach letztem Balancing-Pass)
- **Neue Formel:** `min(2 + (wave - 1), MAX_ENEMEYS)` → Welle 1: 2, Welle 5: 6, Welle 10: 11, Welle 20: 21
### `BossShip` (`boss_ship.gd`)
Zwei Bosse — gleiche Klasse, unterschiedliche Parameter:
| | WRAITH (Welle 5) | LEVIATHAN (Welle 8) |
|--|--|--|
| HP | 20 | 50 |
| Farbe | Magenta | Orange/Feuerrot |
| Orbit-Geschwindigkeit | 0.55 rad/s | 0.44 / 0.72 (Phase 2) |
| Feuer-Intervall | 72 Frames | 60 / 42 (Phase 2) |
| Schüsse | 3-Way | 5-Way / 8-Way |
| Pixel-Größe | 5 | 7 |
**Mechaniken:**
- Orbitiert elliptisch um Bildschirmmitte
- Heading zeigt immer zur Mitte (schießt nach innen)
- LEVIATHAN: Phase-2-Übergang bei ≤ 50% HP → Black Hole spawnt, Musik verdichtet
### `Bullet` (`bullet.gd`)
- Geschwindigkeit: 9.6 px/frame
- Lebensdauer: 240 Frames, Fade ab Frame 210
- Besitzer-Typen: `"p1"`, `"p2"`, `"enemy"`, `"boss"` (bestimmt Kollisions-Check und Farbe)
- `pierce`: true wenn `damage_mult >= 2.0` → trifft bis zu 2 Ziele
### `BlackHole` (`black_hole.gd`)
Komplexestes Objekt im Spiel.
**Parameter:**
- `PULL_RADIUS`: 160px Gravitationsfeld
- `SWALLOW_RADIUS`: 14px Verschluck-Radius
- `FORCE_MULT`: 45.0 (Gravitationsstärke)
- `SUPERNOVA_AT`: 30 verschluckte Objekte → Supernova
**Mechaniken:**
- Wandert langsam umher (Micro-Drift)
- 18% Chance: jagt den nächsten Spieler (Hunting-Mode)
- Wächst mit jedem verschluckten Objekt (radius, pull_radius, gravity)
- Bei 30 Verschluckungen: **Supernova** → stirbt, spawnt Quasar + neue BHs + White/Neutron Star + Sterne/Planeten
- **SMBH**: nach 12 verschluckten Galaxien → Super-Massive BH, nach 45s kollabiert er
- BH-BH-Verschmelzung: größerer frisst kleineren
### `BigWipe` (`big_wipe.gd`)
Notfall-Reset wenn > 500 kosmische Objekte vorhanden sind.
**Phasen:**
1. `COLLAPSE` (2.33s): Bildschirm verdunkelt sich, Spieler müssen Wipe-Taste halten
2. `FLASH` (0.4s): Weißer Flash
3. Alle Objekte außer Planeten/Sternen werden gelöscht
**Spieler-Reaktion:** Signal `p1_wipe` / `p2_wipe` drücken während COLLAPSE → überleben. Wer nicht drückt, stirbt. Belohnung: 25 Credits (+ Credit-Bonus).
### `CosmicObjects` (`cosmic_objects.gd`)
Statische äußere Klasse mit inneren Klassen:
| Klasse | Beschreibung |
|--------|-------------|
| `Star` | Wandert nach oben, kann gravitationell angezogen werden, spiralt ins BH |
| `Planet` | Kreist um Orbit-Punkt, kann von BH gefangen und zerrissen werden |
| `Nebula` | Rein dekorativ, bewegt sich langsam |
| `Comet` | Fliegt von Bildschirmrand zu Bildschirmrand |
| `Galaxy` | Spiral-Galaxie, kann von BH konsumiert werden → SMBH |
| `Quasar` | Entsteht aus Supernova, lebt 30s, rein dekorativ |
| `WhiteHole` | Gegenteil des BH: stößt Spieler ab, ejiziert Sterne/Planeten, lebt 60s |
| `NeutronStar` | Rotierender Pulsar-Strahl, stößt Objekte im Beam-Bereich ab |
| `Antimatter` | Partikel, tötet Spieler/Gegner bei Berührung, ziehen sich an |
| `AntimatterStar` | Entsteht wenn 5+ Antimatter-Partikel clustern, repulsiert Spieler, lebt 50s |
---
## Roguelite-System
### `ShipStats` (`ship_stats.gd`)
Alle Stats sind **multiplikativ** (ausgenommen additive: `bullet_count`, `shield_charges`, `bh_resist`):
| Stat | Standard | Effekt |
|------|----------|--------|
| `speed_mult` | 1.0 | Schub und Max-Speed |
| `turn_mult` | 1.0 | Drehgeschwindigkeit |
| `fire_rate_mult` | 1.0 | Teilt Cooldown (höher = schneller) |
| `damage_mult` | 1.0 | Trefferzone; ≥ 2.0 → Pierce |
| `bullet_speed_mult` | 1.0 | Projektil-Geschwindigkeit |
| `bullet_count` | 1 | Projektile pro Schuss |
| `shield_charges` | 0 | Absorbiert N Treffer |
| `invuln_mult` | 1.0 | Unverwundbarkeitszeit nach Treffer |
| `bh_resist` | 0.0 | 00.9: Anteil des BH-Zugs der negiert wird |
| `wipe_mult` | 1.0 | BigWipe-Haltezeit-Faktor |
| `credit_bonus` | 1.0 | Multiplikator auf alle Credits |
### `ItemDB` (`item_db.gd`)
Zwei Pools:
- **Legacy `ITEMS`** (6 Einträge) — nur noch für Enemy-Boosts ab Welle 10 (`game_world.gd` rollt 1× daraus und wendet es auf jeden Gegner an).
- **Werkstatt-Plugin-Pool** (auto-discover unter `res://items/`, derzeit 17 ItemDefs) — der eigentliche Shop-Pool.
Seltenheiten:
| Seltenheit | Gewicht | Farbe |
|------------|---------|-------|
| STANDARD (Common) | 60 | Grau |
| SELTEN (Uncommon) | 25 | Grün |
| EPISCH (Rare) | 12 | Blau |
| LEGENDÄR (Epic) | 3 | Lila |
Shop zeigt nach jeder Welle 4 zufällige Items (gekaufte werden aus Folge-Rolls ausgeschlossen). Credits kommen aus:
- Kill: 15 Credits × `credit_bonus`
- Passiv: 5 Credits alle 10 Sekunden
- BigWipe überlebt: 25 Credits × `credit_bonus`
- Boss getötet: 150 (Miniboss) / 300 (Boss) Credits
### Werkstatt-Flow (`shop_ui.gd`)
Zwei Phasen pro Shop-Öffnung:
1. **Attribut-Phase** — nur in **ungeraden Wellen** (1, 3, 5, …). 3 zufällige Gratis-Buffs, einer wird gewählt. Werte `+8 % bis +18 %`. Gerade Wellen (2, 4, …) starten direkt in der Shop-Phase → dämpft frühes Snowballing.
2. **Shop-Phase** — 4 Karten aus dem Plugin-Pool. Kauf per ENTER, Reroll per R, SPACE/ESC zum Verlassen.
**Reroll-Kosten:** `60 + 42 × reroll_count` CR → `60 / 102 / 144 / 186 / 228 …`. Der `reroll_count` wird in `main.gd` (`reroll_count_p1` / `_p2`) gehalten und **wellenübergreifend** persistiert — verteuerter Reroll schützt den Rest des Runs vor billigem Ketten-Reroll. Reset nur bei neuem Run / Game Over.
---
## Input-Mapping
| Aktion | Spieler 1 | Spieler 2 |
|--------|-----------|-----------|
| Schub | Pfeil Oben | W |
| Links | Pfeil Links | A |
| Rechts | Pfeil Rechts | D |
| Schießen | Leertaste | F |
| BigWipe | N | E |
---
## Wellen-Progression
| Welle | Dauer | Gegner | Boss |
|-------|-------|--------|------|
| 1 | 20s | 2 | — |
| 3 | 36s | 3 | — |
| 5 | 52s | 4 | WRAITH (Miniboss) |
| 7 | 68s | 5 | — |
| 8 | 76s | 5 | LEVIATHAN (Full Boss) |
| 10+ | ≤120s | 6 (mit Stats!) | — |
| 13+ | 120s | 8 (Cap) | — |
Formel: `enemy_count = min(2 + (wave-1)/2, 8)`. Enemy-Stats bleiben Welle 19 auf Basis; ab Welle 10 bekommt jeder Gegner zusätzlich 1 zufälliges Legacy-Item.
**Schwierigkeits-Ramp:** `difficulty = clamp(game_time / 300.0, 0.0, 1.0)` — steuert Kometen-Spawn-Rate, BH-Cap und mehr.
---
## Balancing-Notizen
Die Power-Kurve ist bewusst **langsam**. Quellen wurden 2026-04-20 global gesenkt, um zu verhindern, dass der Spieler Welle 3 bereits dominiert.
### Plugin-Items (Kern-Multiplikatoren)
| Kategorie | Item | Kern-Effekt | Kosten |
|-----------|------|-------------|--------|
| Waffe | `wk_burst` (EPIC) | fire_rate ×1.80, damage ×0.45 | 160 |
| Waffe | `wk_laser` | fire_rate ×1.35, damage ×0.70 | 115 |
| Waffe | `wk_plasma` | damage ×1.55, proj_speed ×0.60 | 130 |
| Waffe | `wk_ion` | damage ×1.25, +1 Projektil, speed ×0.70 | 140 |
| Waffe | `wk_rail` | proj_speed ×1.50, fire_rate ×0.50 | 125 |
| Waffe | `wk_sniper` | proj_speed ×1.30, damage ×1.18, fire_rate ×0.55 | 115 |
| Waffe | `wk_shotgun` | +2 Projektile, proj_speed ×0.40, damage ×0.80 | 105 |
| Waffe | `wk_scatter` | +1 Projektil, damage ×0.75 | 115 |
| Antrieb | `drive_overdrive` | speed ×1.35, 1 Schild | 95 |
| Antrieb | `drive_quantum` | speed ×1.22, turn ×1.18, proj_speed ×0.80 | 135 |
| Antrieb | `drive_steer` | turn ×1.35, speed ×0.90 | 85 |
| Hülle | `hull_giant` (EPIC) | +2 Schild, invuln ×1.20, speed ×0.70, turn ×0.80 | 220 |
| Hülle | `hull_plating` | +1 Schild, speed ×0.85 | 120 |
| Hülle | `hull_reaktor` | invuln ×1.45, fire_rate ×0.85 | 115 |
| Hülle | `hull_nullfeld` | bh_resist +0.45, speed ×0.80 | 125 |
| Spezial | `special_credit_mag` | credit_bonus ×1.18 | 170 |
| Spezial | `special_wipe_core` | wipe_mult ×0.65 | 150 |
### Legacy-ITEMS (nur Enemy-Boosts Welle 10+)
| id | Effekt | Kosten |
|----|--------|--------|
| thrust_1 | speed ×1.12 | 50 |
| firerate_1 | fire_rate ×1.15 | 50 |
| damage_1 | damage ×1.15 | 50 |
| shield_1 | +1 Schild | 55 |
| firerate_2 | fire_rate ×1.22 | 90 |
| damage_2 | damage ×1.22, proj_speed ×1.10 | 100 |
### Attribute (Gratis, ungerade Wellen)
`ATTR_POOL` in `shop_ui.gd` — Werte: speed/turn/fire/damage/proj je **1.081.12**, invuln 1.18, credit_bonus 1.08, Schild +1.
---
## Audio-System (`sound_manager.gd`)
Kein einziger Audio-File im Projekt. Alle Sounds werden mit `AudioStreamGenerator` pro Frame synthetisiert:
- `_play_tone(freq, duration, wave, vol_db, end_freq)` — Sinus/Säge/Square mit Frequenz-Sweep und Hüllkurve
- `_play_noise(duration, vol_db)` — Weißes Rauschen mit Decay
- `_play_chord_fanfare()` — Drei Töne mit kurzem Delay (BigWipe überlebt)
SFX-Typen: `player_shoot`, `enemy_shoot`, `enemy_die`, `player_die`, `antimatter_hit`, `bh_swallow`, `wipe_start`, `wipe_flash`, `wipe_survived`, `smbh_spawn`, `antimatter_swarm`
---
## Grafik-System
**Viewport:** 960×600px, Stretch-Modus `canvas_items / expand`
**Hintergrundfarbe:** `#0a0a14` (Tiefschwarz-Blau)
**FPS-Cap:** 60
**Pixel-Filter:** Nearest-Neighbor (`default_texture_filter=0`)
Alle Schiffe benutzen `HULL_PIXELS`-Arrays — lokale Koordinaten-Offsets, die per `cos(heading)/sin(heading)` in Weltkoordinaten gedreht werden. Jeder "Pixel" ist ein 3×3-`draw_rect`.
---
## Signalfluss
```
game_world → main_node.on_game_over() # alle Spieler tot
game_world → main_node.on_wave_complete() # Wave-Timer abgelaufen
game_world → main_node.add_credits(player, n) # Credits vergeben
game_world → main_node.start_boss_music(final) # Boss spawnt
game_world → main_node.boss_phase_changed(2) # Leviathan Phase 2
big_wipe.wipe_complete → game_world._on_wipe_complete()
shop_ui.shop_closed → main._on_shop_closed()
pause_menu.resume_requested → main._on_pause_resume()
main_menu.mode_selected → main._on_mode_selected(multi)
```
---
## MCP-Plugin (`addons/godot_mcp/`)
Das Godot-MCP-Pro-Plugin verbindet den Godot-Editor per WebSocket mit externen KI-Tools (Claude Code). Ermöglicht das Lesen/Schreiben von Szenen, Scripts und Eigenschaften direkt aus der KI-Konversation heraus. Nur für die Entwicklung relevant — kein Einfluss auf das Spiel. Referenz dazu in der CLaude.md