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)