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>
This commit is contained in:
+367
@@ -0,0 +1,367 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user