c742c4ae0e
The lobby hub replaced the between-fights map, leaving the whole map subsystem unused. Remove it: - Delete MapScreen.tscn + map_screen.gd (the dead screen) and map_node.gd (the MapNode class). - Strip map generation + navigation from run_state.gd (~90 lines: _generate_map/_link_rows/reachable/select_node/node_at/current_node/ run_complete/complete_current_level, plus map/player_row/col/pending_row/col). ensure_run() now guards on a _run_active flag instead of the map array, so the shell reloading for each fight still can't wipe the run in progress. - Simplify active_level() to the lobby flow (override -> lobby -> gate -> arena). - Drop the now-unread LevelDef.color / is_boss fields and the _make_level args. - Remove the map test from logic_test.gd. Live flow unchanged (lobby gates via choose_level, wins via return_to_lobby). Full headless suite green; menu + shell boot into the lobby clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
157 lines
6.0 KiB
GDScript
157 lines
6.0 KiB
GDScript
extends Node
|
|
## Persistent run progression across scene changes — which hub/level the player is in.
|
|
## The level scene is reloaded for every fight, so this state must outlive scene changes:
|
|
## genuinely global run state earns an autoload (`Run`).
|
|
##
|
|
## Flow: menu Play → start_new_run() → lobby hub → enter a gate → that level → win →
|
|
## return_to_lobby() (clearing a level opens its follow-on gates, e.g. clearing the arena
|
|
## unlocks the bear). Adding a level is a new LevelDef in the catalog plus a lobby gate.
|
|
|
|
signal run_started
|
|
|
|
# Every level loads the same reusable shell (player, camera, HUD, PS1 filter); the shell
|
|
# then instances the level's own `level_scene` module into its LevelRoot.
|
|
const _SHELL_SCENE := "res://levels/game_shell.tscn"
|
|
const _ARENA_LEVEL := preload("res://levels/arena_level.tscn")
|
|
const _BEAR_LEVEL := preload("res://levels/bear_level.tscn")
|
|
const _LOBBY_LEVEL := preload("res://levels/lobby_level.tscn")
|
|
|
|
## Debug override — when set, active_level() returns this regardless of run state.
|
|
## Set via the console `level` command; cleared on start_new_run().
|
|
var debug_level_override: LevelDef = null
|
|
## The lobby is the hub the run starts and returns to: a walk-around room whose gates lead
|
|
## into the fights. While set, active_level() serves the lobby; cleared once a gate is
|
|
## entered (choose_level) and set again on a win (return_to_lobby).
|
|
var enter_lobby: bool = false
|
|
## Level picked at a lobby gate and being fought now — active_level() serves it while
|
|
## enter_lobby is false. Null in the lobby itself.
|
|
var lobby_choice: LevelDef = null
|
|
## Ids of levels the player has cleared this run (StringName -> true). Drives which lobby
|
|
## gates are open: the bear gate stays shut until the arena is beaten.
|
|
var cleared: Dictionary = {}
|
|
## Run-long bonus health (yellow pips) earned from lobby rewards. Persists across level
|
|
## loads — the player reads it on spawn — and is capped so it can't stack past BONUS_HP_CAP.
|
|
var bonus_hp: int = 0
|
|
const BONUS_HP_CAP: int = 2
|
|
## True once start_new_run() has built a run. Lets ensure_run() avoid wiping an active run
|
|
## when the shell reloads for each fight.
|
|
var _run_active: bool = false
|
|
|
|
# Level catalog. Every level shares the shell scene; what changes is the level module
|
|
# (its geometry + spawners) and the opponent. The arena (matador wave, normal + boss
|
|
# flavours) and the bear (a lone boss enemy) live here. Future levels — china shop — slot
|
|
# in the same way: a new module + a LevelDef entry.
|
|
const _BEAR_SCENE := preload("res://actors/bear/bear.tscn")
|
|
|
|
var _arena: LevelDef
|
|
var _arena_boss: LevelDef
|
|
var _bear: LevelDef
|
|
var _lobby: LevelDef
|
|
|
|
|
|
func _ready() -> void:
|
|
_arena = _make_level(&"arena", "The Arena", _ARENA_LEVEL)
|
|
_arena_boss = _make_level(&"arena_boss", "Grand Arena", _ARENA_LEVEL)
|
|
_bear = _make_level(&"bear", "Bear's Den", _BEAR_LEVEL)
|
|
_bear.enemy_scene = _BEAR_SCENE
|
|
# The lobby is a fightless hub — no enemy_scene, so no wave spawns and no win/lose.
|
|
_lobby = _make_level(&"lobby", "Lobby", _LOBBY_LEVEL)
|
|
# It's a roomy hub with nothing to fight — pull the camera back so more of it reads.
|
|
_lobby.camera_zoom = 1.6
|
|
|
|
|
|
func _make_level(id: StringName, level_name: String, level_scene: PackedScene) -> LevelDef:
|
|
var l := LevelDef.new()
|
|
l.id = id
|
|
l.display_name = level_name
|
|
l.scene_path = _SHELL_SCENE
|
|
l.level_scene = level_scene
|
|
return l
|
|
|
|
|
|
## The level being served right now: debug override wins, then the lobby hub or the level
|
|
## picked at a gate, falling back to the plain arena for a direct scene launch.
|
|
func active_level() -> LevelDef:
|
|
if debug_level_override != null:
|
|
return debug_level_override
|
|
if enter_lobby:
|
|
return _lobby
|
|
if lobby_choice != null:
|
|
return lobby_choice
|
|
return _arena
|
|
|
|
|
|
## Return a level by id, or null if unknown. Used by the debug console `level` command.
|
|
func get_level(id: StringName) -> LevelDef:
|
|
match id:
|
|
&"arena": return _arena
|
|
&"arena_boss": return _arena_boss
|
|
&"bear": return _bear
|
|
&"lobby": return _lobby
|
|
return null
|
|
|
|
|
|
## All known level ids — shown by `level` in the console.
|
|
func level_ids() -> Array[StringName]:
|
|
return [&"arena", &"arena_boss", &"bear", &"lobby"]
|
|
|
|
|
|
## Begin a fresh run: reset progression and open in the lobby hub. Called from the main-menu
|
|
## Play button, not from a level scene (which reloads for every fight).
|
|
func start_new_run() -> void:
|
|
debug_level_override = null
|
|
# The run opens in the lobby hub; its gates lead into the fights.
|
|
enter_lobby = true
|
|
lobby_choice = null
|
|
cleared = {}
|
|
bonus_hp = 0
|
|
_run_active = true
|
|
run_started.emit()
|
|
|
|
|
|
## Build a run if none is active — a safety net for entering a level directly (e.g. running
|
|
## game_shell.tscn straight from the editor) without coming through the menu. Guarded so the
|
|
## shell reloading for each fight doesn't wipe the run in progress.
|
|
func ensure_run() -> void:
|
|
if not _run_active:
|
|
start_new_run()
|
|
|
|
|
|
## Send the player to the lobby hub instead of a fight — active_level() serves the lobby
|
|
## until a gate is entered.
|
|
func go_to_lobby() -> void:
|
|
enter_lobby = true
|
|
lobby_choice = null
|
|
|
|
|
|
## Enter the fight behind a lobby gate: active_level() serves it until the player wins and
|
|
## returns to the lobby. Ignored for an unknown id.
|
|
func choose_level(id: StringName) -> void:
|
|
var l := get_level(id)
|
|
if l == null:
|
|
return
|
|
enter_lobby = false
|
|
lobby_choice = l
|
|
|
|
|
|
## Land back in the lobby after a win, banking the cleared level so its follow-on gates
|
|
## open (clearing the arena unlocks the bear gate).
|
|
func return_to_lobby() -> void:
|
|
if lobby_choice != null:
|
|
cleared[lobby_choice.id] = true
|
|
go_to_lobby()
|
|
|
|
|
|
## Whether the bear gate should be open — the arena must be cleared first.
|
|
func bear_gate_open() -> bool:
|
|
return cleared.has(&"arena")
|
|
|
|
|
|
## Bank bonus health for the rest of the run, clamped to BONUS_HP_CAP. Returns how many
|
|
## pips were actually added (0 once the cap is reached) so the HUD only heals the bull when
|
|
## there's headroom — a repeat barrel clear still cheers but grants nothing.
|
|
func add_bonus_hp(amount: int) -> int:
|
|
var before := bonus_hp
|
|
bonus_hp = clampi(bonus_hp + amount, 0, BONUS_HP_CAP)
|
|
return bonus_hp - before
|