Files
alpacaman edc40f9008 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>
2026-04-21 14:38:09 +02:00

368 lines
13 KiB
GDScript

extends Node2D
# ─── State Machine ───────────────────────────────────────────────────────────
enum State { MAIN_MENU, SELECT, SELECT_P2, LAUNCHING, PLAYING, RETURNING, WAVE_CLEAR, SHOP, GAMEOVER, PAUSED }
var state: State = State.SELECT
# ─── Ship data ───────────────────────────────────────────────────────────────
const SHIPS := [
{ "id": "classic", "name": "NOVA-1",
"nose": Color("#ffffff"), "bright": Color("#dddddd"), "mid": Color("#cccccc"),
"dim": Color("#aaaaaa"), "accent": Color("#88aaff"), "edge": Color("#888888"),
"shadow": Color("#666688"), "trail": Color(0.533,0.667,1.0,0.251),
"thrustHot": Color("#ffcc44"), "thrustCool": Color(1.0,0.533,0.267,0.533) },
{ "id": "inferno", "name": "INFERNO",
"nose": Color("#ffeecc"), "bright": Color("#ffaa66"), "mid": Color("#ee6633"),
"dim": Color("#aa3322"), "accent": Color("#ff4444"), "edge": Color("#882211"),
"shadow": Color("#551111"), "trail": Color(1.0,0.4,0.251,0.251),
"thrustHot": Color("#ffee44"), "thrustCool": Color(1.0,0.267,0.0,0.533) },
{ "id": "aurora", "name": "AURORA",
"nose": Color("#ddffee"), "bright": Color("#aaffcc"), "mid": Color("#66cc99"),
"dim": Color("#338866"), "accent": Color("#44ccff"), "edge": Color("#226655"),
"shadow": Color("#114433"), "trail": Color(0.267,0.8,1.0,0.251),
"thrustHot": Color("#aaffcc"), "thrustCool": Color(0.267,0.8,1.0,0.533) },
{ "id": "titan", "name": "TITAN",
"nose": Color("#ffffdd"), "bright": Color("#ddcc88"), "mid": Color("#aa9944"),
"dim": Color("#776633"), "accent": Color("#ffdd44"), "edge": Color("#554422"),
"shadow": Color("#332211"), "trail": Color(1.0,0.85,0.25,0.251),
"thrustHot": Color("#ffffff"), "thrustCool": Color(1.0,0.67,0.0,0.533) },
]
# ─── Game state ───────────────────────────────────────────────────────────────
var selected_ship_p1 := 0
var selected_ship_p2 := 0
var is_multiplayer := false
var launch_timer := 0.0
var return_timer := 0.0
var blink_phase := 0.0
# ─── Run state (roguelite) ────────────────────────────────────────────────────
const MAX_LIVES := 3
var lives_p1: int = MAX_LIVES
var credits_p1: int = 0
var stats_p1: ShipStats = null
var owned_items_p1: Array = []
var credits_p2: int = 0
var stats_p2: ShipStats = null
var owned_items_p2: Array = []
var wave_number: int = 1
var _shop_player: int = 1
# Reroll-Counter persistiert über den gesamten Run (nicht pro Shop zurückgesetzt).
# Jeder Reroll verteuert den nächsten auch welleübergreifend — Snowball-Schutz.
var reroll_count_p1: int = 0
var reroll_count_p2: int = 0
# ─── References ──────────────────────────────────────────────────────────────
@onready var game_world: Node2D = $GameWorld
@onready var ship_select_ui: Node2D = $ShipSelectUI
@onready var hud: CanvasLayer = $HUD
@onready var shop_ui: Node2D = $ShopUI
@onready var pause_menu: Node2D = $PauseMenu
@onready var main_menu: Node2D = $MainMenu
@onready var atlas_ui: Node2D = $AtlasUI
@onready var music_player: Node = $MusicPlayer
@onready var touch_controls: Node2D = $HUD/TouchControls
func start_boss_music(is_final: bool = false) -> void:
if music_player:
if is_final:
music_player.play_boss_leviathan()
else:
music_player.play_boss()
func stop_boss_music() -> void:
if music_player:
music_player.play_normal()
func boss_phase_changed(new_phase: int) -> void:
if music_player and new_phase == 2:
music_player.enter_phase2()
func _ready() -> void:
game_world.main_node = self
hud.game_world = game_world
shop_ui.shop_closed.connect(_on_shop_closed)
pause_menu.resume_requested.connect(_on_pause_resume)
pause_menu.quit_to_menu_requested.connect(_on_pause_quit_menu)
main_menu.mode_selected.connect(_on_mode_selected)
main_menu.atlas_requested.connect(_on_atlas_requested)
main_menu.quit_requested.connect(func(): get_tree().quit())
atlas_ui.closed.connect(_on_atlas_closed)
touch_controls.game_world = game_world
touch_controls.direct_touch_ui = shop_ui
stats_p1 = ShipStats.new()
stats_p2 = ShipStats.new()
_set_state(State.MAIN_MENU)
# Sets ship-specific base stats (e.g. TITAN starts slower + has boost).
# Only applied on fresh ShipStats (no previously-bought items yet).
static func _apply_ship_base_stats(stats: ShipStats, ship_id: String) -> void:
if stats == null: return
stats.ship_id = ship_id
match ship_id:
"classic":
stats.shield_charges = 1
"inferno":
stats.speed_mult = 1.28
stats.turn_mult = 0.72
stats.fire_rate_mult = 1.55
"aurora":
stats.speed_mult = 0.80
stats.turn_mult = 1.55
stats.invuln_mult = 1.80
stats.shield_charges = 2
stats.bh_resist = 0.55
"titan":
stats.speed_mult = 0.72
stats.turn_mult = 0.85
stats.has_boost = true
stats.boost_cooldown_max = 5.0
func _set_state(new_state: State) -> void:
state = new_state
match state:
State.MAIN_MENU:
main_menu.visible = true
ship_select_ui.visible = false
shop_ui.visible = false
pause_menu.visible = false
hud.visible = false
game_world.visible = true
game_world.init_menu_mode()
touch_controls.set_mode(touch_controls.Mode.MENU)
State.SELECT:
main_menu.visible = false
ship_select_ui.visible = true
shop_ui.visible = false
game_world.visible = true
hud.visible = false
game_world.init_menu_mode()
ship_select_ui.start_select(false, selected_ship_p1, SHIPS)
touch_controls.set_mode(touch_controls.Mode.MENU)
State.SELECT_P2:
ship_select_ui.start_select(true, selected_ship_p2, SHIPS)
touch_controls.set_mode(touch_controls.Mode.MENU)
State.LAUNCHING:
ship_select_ui.visible = false
shop_ui.visible = false
game_world.visible = true
hud.visible = true
hud.is_gameover = false
hud.is_returning = false
hud.show_countdown_flag = false
hud.credits = credits_p1
hud.lives = lives_p1
launch_timer = 3.0
# Apply ship-base-stats once on first launch (wave 1). Later waves
# preserve the stats the player earned from Werkstatt upgrades.
if wave_number == 1:
_apply_ship_base_stats(stats_p1, SHIPS[selected_ship_p1]["id"])
if is_multiplayer:
_apply_ship_base_stats(stats_p2, SHIPS[selected_ship_p2]["id"])
var s2 = SHIPS[selected_ship_p2] if is_multiplayer else null
game_world.init_world(SHIPS[selected_ship_p1], s2, is_multiplayer, stats_p1, stats_p2, wave_number)
touch_controls.set_mode(touch_controls.Mode.GAME)
State.PLAYING:
get_tree().paused = false
pause_menu.visible = false
game_world.start_playing()
touch_controls.set_mode(touch_controls.Mode.GAME)
State.PAUSED:
pause_menu.open()
touch_controls.set_mode(touch_controls.Mode.MENU)
State.RETURNING:
lives_p1 -= 1
hud.is_returning = true
hud.lives = lives_p1
return_timer = 0.85
game_world.on_player_died()
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
State.WAVE_CLEAR:
hud.wave_cleared = true
return_timer = 2.0
game_world.on_player_died()
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
State.SHOP:
ship_select_ui.visible = false
hud.visible = false
game_world.visible = true
shop_ui.visible = true
if _shop_player == 1:
shop_ui.open(lives_p1, credits_p1, stats_p1, owned_items_p1,
"SPIELER 1" if is_multiplayer else "",
SHIPS[selected_ship_p1], wave_number, reroll_count_p1, 1)
else:
shop_ui.open(lives_p1, credits_p2, stats_p2, owned_items_p2, "SPIELER 2",
SHIPS[selected_ship_p2], wave_number, reroll_count_p2, 2)
touch_controls.set_mode(touch_controls.Mode.MENU)
State.GAMEOVER:
shop_ui.visible = false
hud.show_gameover(game_world.get_score_data())
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
func _process(delta: float) -> void:
blink_phase += delta
match state:
State.LAUNCHING:
launch_timer -= delta
hud.show_countdown(ceil(launch_timer))
if launch_timer <= 0.0:
_set_state(State.PLAYING)
State.RETURNING:
return_timer -= delta
if return_timer <= 0.0:
if lives_p1 > 0:
_set_state(State.SHOP)
else:
_set_state(State.GAMEOVER)
State.WAVE_CLEAR:
return_timer -= delta
if return_timer <= 0.0:
hud.wave_cleared = false
_set_state(State.SHOP)
# Detect which type of input device the player is currently using.
# Drives adaptive UI labels via Tr.hint().
func _input(event: InputEvent) -> void:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
if Settings.last_input_device != "pad":
Settings.last_input_device = "pad"
elif event is InputEventKey or event is InputEventMouseButton:
if Settings.last_input_device != "keyboard":
Settings.last_input_device = "keyboard"
# "touch" is set by touch_controls.gd when a screen touch arrives.
func _unhandled_input(event: InputEvent) -> void:
match state:
State.PLAYING:
if event.is_action_pressed("ui_cancel"):
get_tree().paused = true
_set_state(State.PAUSED)
return
State.SELECT:
if event.is_action_pressed("ui_left"):
selected_ship_p1 = (selected_ship_p1 - 1 + SHIPS.size()) % SHIPS.size()
ship_select_ui.set_selection(selected_ship_p1)
elif event.is_action_pressed("ui_right"):
selected_ship_p1 = (selected_ship_p1 + 1) % SHIPS.size()
ship_select_ui.set_selection(selected_ship_p1)
elif event.is_action_pressed("ui_accept"):
if is_multiplayer:
_set_state(State.SELECT_P2)
else:
_set_state(State.LAUNCHING)
elif event.is_action_pressed("ui_cancel"):
_set_state(State.MAIN_MENU)
State.SELECT_P2:
if event.is_action_pressed("ui_left"):
selected_ship_p2 = (selected_ship_p2 - 1 + SHIPS.size()) % SHIPS.size()
ship_select_ui.set_selection(selected_ship_p2)
elif event.is_action_pressed("ui_right"):
selected_ship_p2 = (selected_ship_p2 + 1) % SHIPS.size()
ship_select_ui.set_selection(selected_ship_p2)
elif event.is_action_pressed("ui_accept"):
_set_state(State.LAUNCHING)
elif event.is_action_pressed("ui_cancel"):
_set_state(State.SELECT)
State.GAMEOVER:
if event.is_pressed():
_reset_game()
func _reset_game() -> void:
stop_boss_music() # Boss-Musik stoppen falls aktiv
selected_ship_p1 = 0
selected_ship_p2 = 0
is_multiplayer = false
hud.is_gameover = false
hud.is_returning = false
hud.show_countdown_flag = false
# Reset roguelite run state
lives_p1 = MAX_LIVES
credits_p1 = 0
stats_p1 = ShipStats.new()
owned_items_p1 = []
credits_p2 = 0
stats_p2 = ShipStats.new()
owned_items_p2 = []
reroll_count_p1 = 0
reroll_count_p2 = 0
_shop_player = 1
wave_number = 1
hud.wave_number = 1
hud.wave_cleared = false
_set_state(State.MAIN_MENU)
func add_credits(player_idx: int, amount: int) -> void:
if player_idx == 0:
credits_p1 += amount
hud.credits = credits_p1
elif player_idx == 1:
credits_p2 += amount
func on_wave_complete() -> void:
_set_state(State.WAVE_CLEAR)
func _on_shop_closed(remaining_credits: int, new_stats: ShipStats, items: Array, reroll_count: int) -> void:
if _shop_player == 1:
credits_p1 = remaining_credits
stats_p1 = new_stats
owned_items_p1 = items
reroll_count_p1 = reroll_count
if is_multiplayer:
_shop_player = 2
_set_state(State.SHOP)
return
else:
credits_p2 = remaining_credits
stats_p2 = new_stats
owned_items_p2 = items
reroll_count_p2 = reroll_count
_shop_player = 1
wave_number += 1
hud.wave_number = wave_number
_set_state(State.LAUNCHING)
func _on_mode_selected(multi: bool) -> void:
is_multiplayer = multi
main_menu.visible = false
# Reset run state + apply fresh ship-base stats (TITAN starts slower etc.).
# The base stats are applied AFTER ship selection in _set_state(LAUNCHING).
lives_p1 = MAX_LIVES
credits_p1 = 0
stats_p1 = ShipStats.new()
owned_items_p1 = []
credits_p2 = 0
stats_p2 = ShipStats.new()
owned_items_p2 = []
reroll_count_p1 = 0
reroll_count_p2 = 0
_shop_player = 1
wave_number = 1
hud.wave_number = 1
_set_state(State.SELECT)
func _on_atlas_requested() -> void:
main_menu.visible = false
atlas_ui.visible = true
if atlas_ui.has_method("open"):
atlas_ui.open()
touch_controls.set_mode(touch_controls.Mode.MENU)
func _on_atlas_closed() -> void:
atlas_ui.visible = false
main_menu.visible = true
touch_controls.set_mode(touch_controls.Mode.MENU)
func _on_pause_resume() -> void:
get_tree().paused = false
state = State.PLAYING
func _on_pause_quit_menu() -> void:
get_tree().paused = false
_reset_game()
# Called by game_world when all players are dead
func on_game_over() -> void:
stop_boss_music() # Boss-Musik stoppen falls aktiv
_set_state(State.RETURNING)