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