edc40f9008
- 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>
368 lines
13 KiB
GDScript
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)
|