- 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>
18 KiB
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 1wave_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, initialisiertgame_world_process()— Countdown vor Spielstart, Return-/Wave-Timeradd_credits(player, amount)— Credits dem Spieler gutschreibenon_game_over() / on_wave_complete()— vongame_worldaufgerufen
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):
_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
_handle_input(dt)— Spieler-Input lesen, Schüsse erzeugen_update_objects(dt)— Alle kosmischen Objekte updaten, Gegner KI_update_bullets()— Projektile bewegen, tote entfernen- Boss-Update (falls vorhanden) → erzeugt Bullets
_update_particles(dt)— Partikeleffekte_check_collisions()— Bullet/Enemy, Bullet/Player, Antimatter/Player, Bullets/Boss_check_big_wipe()— Threshold prüfen (> 500 Objekte)- BigWipe-Update
- Schwierigkeit + Wave-Timer + Credit-Trickle
- All-Dead-Check →
main_node.on_game_over() - Kometen- und Antimatter-Spawn-Timer
Arrays der Spielobjekte
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/Rumpfedraw_circle()für Schwarze Löcher, White Holesdraw_arc()für Akkretionsscheiben, Ringedraw_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, Trailshoot_burst() → Array[Bullet]— erzeugt 1–N Bullets je nachstats.bullet_countdraw(canvas, frame)— malt Rumpf ausHULL_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_idgestaffelt (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 wenndamage_mult >= 2.0→ trifft bis zu 2 Ziele
BlackHole (black_hole.gd)
Komplexestes Objekt im Spiel.
Parameter:
PULL_RADIUS: 160px GravitationsfeldSWALLOW_RADIUS: 14px Verschluck-RadiusFORCE_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:
COLLAPSE(2.33s): Bildschirm verdunkelt sich, Spieler müssen Wipe-Taste haltenFLASH(0.4s): Weißer Flash- 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.gdrollt 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:
- 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. - 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