# 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 1–2 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 1–N 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 4–8s 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 | 0–0.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 1–9 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.08–1.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