From e11eddf1b47529be8c49f12063d55ef604c68017 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Wed, 11 Mar 2026 09:47:37 -0400 Subject: [PATCH] Fix critical bugs from code review - room.gd: Add signal tracking and explicit cleanup in _exit_tree - room.gd: Guard _on_enemy_died against race condition after room freed - player.gd: Set _shield_ready = false on shield activation - player.gd: Fix coolant zone lambda syntax error - player.gd: Add apply_knockback() method for Hivemind pulse - player.gd: Fix coolant zone memory leak (use Timer child) - hud.gd: Remove deprecated boss bar code --- project.godot | 7 ++- scripts/enemies/anchor.gd | 2 +- scripts/enemies/drifter.gd | 2 +- scripts/enemies/enemy_base.gd | 25 ++++++--- scripts/enemies/hivemind.gd | 13 +++-- scripts/enemies/relay.gd | 7 ++- scripts/enemies/repeater.gd | 2 +- scripts/enemies/supervisor.gd | 8 ++- scripts/enemies/warden.gd | 7 ++- scripts/event_bus.gd | 13 +++++ scripts/game_manager.gd | 1 + scripts/items/run_item_pickup.gd | 5 +- scripts/items/shop_item_pedestal.gd | 5 +- scripts/player/player.gd | 29 ++++++++-- scripts/player/player_stats.gd | 25 +++++++++ scripts/rooms/room.gd | 42 +++++++++++++-- scripts/run_manager.gd | 3 ++ scripts/ui/hud.gd | 82 ----------------------------- scripts/ui/mini_map.gd | 4 +- 19 files changed, 162 insertions(+), 120 deletions(-) diff --git a/project.godot b/project.godot index f00edd7..6d6cc61 100644 --- a/project.godot +++ b/project.godot @@ -21,11 +21,16 @@ config/icon="res://assets/icon.svg" [autoload] +# ===== AUTOLOAD INITIALIZATION ORDER ===== +# Order is critical! Dependencies must load before dependents. +# Layer 0: Event system (no dependencies) EventBus="*res://scripts/event_bus.gd" +# Layer 1: Core managers (depend on EventBus only) GameManager="*res://scripts/game_manager.gd" -RunManager="*res://scripts/run_manager.gd" SaveManager="*res://scripts/save_manager.gd" UpgradeManager="*res://scripts/upgrades/upgrade_manager.gd" +# Layer 2: Run systems (depend on Layer 1) +RunManager="*res://scripts/run_manager.gd" CardCollection="*res://scripts/cards/card_collection.gd" NPCManager="*res://scripts/npcs/npc_manager.gd" LoreManager="*res://scripts/lore/lore_manager.gd" diff --git a/scripts/enemies/anchor.gd b/scripts/enemies/anchor.gd index c87607c..ec1843c 100644 --- a/scripts/enemies/anchor.gd +++ b/scripts/enemies/anchor.gd @@ -57,7 +57,7 @@ func _physics_process(delta: float) -> void: State.ANCHORED: _do_anchored(delta) - velocity *= _slow_factor + velocity *= get_slow_factor() move_and_slide() # --------------------------------------------------------------------------- diff --git a/scripts/enemies/drifter.gd b/scripts/enemies/drifter.gd index 7057975..d03dc34 100644 --- a/scripts/enemies/drifter.gd +++ b/scripts/enemies/drifter.gd @@ -25,7 +25,7 @@ func _physics_process(delta: float) -> void: if not is_active(): return _do_arc_chase(delta) - velocity *= _slow_factor + velocity *= get_slow_factor() move_and_slide() # --------------------------------------------------------------------------- diff --git a/scripts/enemies/enemy_base.gd b/scripts/enemies/enemy_base.gd index 01e1d2f..f033570 100644 --- a/scripts/enemies/enemy_base.gd +++ b/scripts/enemies/enemy_base.gd @@ -17,8 +17,7 @@ const SPAWN_DELAY: float = 0.5 var _contact_timer: float = 0.0 var _flash_timer: float = 0.0 -var _slow_factor: float = 1.0 -var _slow_timer: float = 0.0 +var _slow_effects: Array[Dictionary] = [] # [{factor, timer}] - stacking slows var _spawn_timer: float = SPAWN_DELAY var _player: Node2D = null @@ -62,14 +61,24 @@ func _tick_flash(delta: float) -> void: visual.modulate = Color(1.0, 1.0, 1.0) func apply_slow(duration: float, factor: float) -> void: - _slow_factor = factor - _slow_timer = duration + # Stack slows by adding new effect - factors multiply together + _slow_effects.append({"factor": factor, "timer": duration}) func _tick_slow(delta: float) -> void: - if _slow_timer > 0.0: - _slow_timer -= delta - if _slow_timer <= 0.0: - _slow_factor = 1.0 + # Tick down all slow effects and remove expired ones + var i := _slow_effects.size() - 1 + while i >= 0: + _slow_effects[i]["timer"] -= delta + if _slow_effects[i]["timer"] <= 0.0: + _slow_effects.remove_at(i) + i -= 1 + +func get_slow_factor() -> float: + # Multiply all active slow factors together + var combined := 1.0 + for effect in _slow_effects: + combined *= effect["factor"] + return combined # --------------------------------------------------------------------------- # Damage / death diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd index 22fff51..9c3a515 100644 --- a/scripts/enemies/hivemind.gd +++ b/scripts/enemies/hivemind.gd @@ -57,6 +57,11 @@ const HOMING_LIFETIME := 7.0 # seconds before homing projectiles expire const FREEZE_DURATION := 2.0 +const ORBITER_DAMAGE := 1.0 +const SWEEP_PROJ_DAMAGE := 1.0 +const BEAM_DAMAGE := 1.0 +const HOMING_PROJ_DAMAGE := 1.0 + const DRIFTER_SCENE = preload("res://scenes/enemies/drifter.tscn") const REPEATER_SCENE = preload("res://scenes/enemies/repeater.tscn") const ANCHOR_SCENE = preload("res://scenes/enemies/anchor.tscn") @@ -461,7 +466,7 @@ func _create_orbiter(angle: float) -> Dictionary: func _on_orbiter_hit(body: Node, _proj: Node2D) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1.0) + body.take_damage(ORBITER_DAMAGE) func _tick_orbiters(delta: float) -> void: @@ -557,7 +562,7 @@ func _spawn_sweep_proj(pos: Vector2, vel: Vector2) -> void: func _on_sweep_hit(body: Node, proj: Node2D) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1.0) + body.take_damage(SWEEP_PROJ_DAMAGE) if is_instance_valid(proj): proj.queue_free() @@ -738,7 +743,7 @@ func _end_beam() -> void: func _on_beam_hit(body: Node) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1.0) + body.take_damage(BEAM_DAMAGE) func _attack_pulse() -> void: @@ -803,7 +808,7 @@ func _spawn_homing_proj(start_angle: float) -> void: func _on_homing_hit(body: Node, proj: Node2D) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1.0) + body.take_damage(HOMING_PROJ_DAMAGE) if is_instance_valid(proj): proj.queue_free() diff --git a/scripts/enemies/relay.gd b/scripts/enemies/relay.gd index 29f64e6..f18dfe5 100644 --- a/scripts/enemies/relay.gd +++ b/scripts/enemies/relay.gd @@ -46,6 +46,9 @@ const DRIFTER_OFFSET_MAX := 80.0 const FREEZE_DURATION := 1.5 +const BURST_PROJ_DAMAGE := 1.0 +const HOMING_PROJ_DAMAGE := 1.0 + const DRIFTER_SCENE = preload("res://scenes/enemies/drifter.tscn") # --------------------------------------------------------------------------- @@ -228,7 +231,7 @@ func _spawn_burst_proj(dir: Vector2) -> void: func _on_burst_hit_player(body: Node2D, proj: Node2D) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1) + body.take_damage(BURST_PROJ_DAMAGE) if is_instance_valid(proj): proj.queue_free() @@ -298,7 +301,7 @@ func _spawn_homing_proj(start_angle: float) -> void: func _on_homing_hit_player(body: Node2D, proj: Node2D) -> void: if body.is_in_group("player") and body.has_method("take_damage"): - body.take_damage(1) + body.take_damage(HOMING_PROJ_DAMAGE) if is_instance_valid(proj): proj.queue_free() diff --git a/scripts/enemies/repeater.gd b/scripts/enemies/repeater.gd index 374a21b..5444a76 100644 --- a/scripts/enemies/repeater.gd +++ b/scripts/enemies/repeater.gd @@ -65,7 +65,7 @@ func _physics_process(delta: float) -> void: return velocity = Vector2.ZERO _do_fire(delta) - velocity *= _slow_factor + velocity *= get_slow_factor() move_and_slide() # --------------------------------------------------------------------------- diff --git a/scripts/enemies/supervisor.gd b/scripts/enemies/supervisor.gd index f0d5198..94899fc 100644 --- a/scripts/enemies/supervisor.gd +++ b/scripts/enemies/supervisor.gd @@ -38,6 +38,8 @@ const WIND_UP_DURATION := 0.65 # seconds of warning before beam fires # --------------------------------------------------------------------------- const FREEZE_DURATION := 2.0 +const BEAM_DAMAGE := 1.0 + # --------------------------------------------------------------------------- # Phase 2 Drifter spawning — one at a time, over a fixed window # --------------------------------------------------------------------------- @@ -304,7 +306,7 @@ func _fire_beam() -> void: area.add_child(cs) area.body_entered.connect(func(body: Node) -> void: if body.is_in_group("player"): - body.take_damage(1.0)) + body.take_damage(BEAM_DAMAGE)) beam.add_child(area) get_parent().call_deferred("add_child", beam) @@ -314,11 +316,13 @@ func _fire_beam() -> void: # Damage / death — override base to emit boss signals; no drop (room handles it) # --------------------------------------------------------------------------- func take_damage(amount: float) -> void: + if _phase == Phase.CLIMAX: + return # already dying current_health -= amount _flash_timer = 0.1 EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE SUPERVISOR") if current_health <= 0.0: - _die() + _enter_climax() func _die() -> void: EventBus.boss_died.emit() diff --git a/scripts/enemies/warden.gd b/scripts/enemies/warden.gd index 14d99fb..8cccc03 100644 --- a/scripts/enemies/warden.gd +++ b/scripts/enemies/warden.gd @@ -37,6 +37,9 @@ const MINE_AOE := 70.0 const FREEZE_DURATION := 1.5 +const SWEEP_PROJ_DAMAGE := 1.0 +const MINE_DAMAGE := 1.0 + # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- @@ -215,7 +218,7 @@ func _spawn_sweep_proj(world_pos: Vector2, vel: Vector2) -> void: area.add_child(cs) area.body_entered.connect(func(body: Node) -> void: if body.is_in_group("player"): - body.take_damage(1.0) + body.take_damage(SWEEP_PROJ_DAMAGE) proj.queue_free()) proj.add_child(area) @@ -321,7 +324,7 @@ func _explode_mine(entry: Dictionary) -> void: if _player != null and is_instance_valid(_player): var dist := node.global_position.distance_to(_player.global_position) if dist <= MINE_AOE: - _player.take_damage(1.0) + _player.take_damage(MINE_DAMAGE) # Brief visual flash for explosion (optional: could add particle later) var flash := Polygon2D.new() diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index 43552e9..9f57bab 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -1,5 +1,18 @@ extends Node +# --------------------------------------------------------------------------- +# Initialization Order Validation +# --------------------------------------------------------------------------- +# EventBus must be the first autoload. Other autoloads depend on it. +# See project.godot for full dependency graph. +static var _initialized: bool = false + +func _ready() -> void: + _initialized = true + +static func assert_ready() -> void: + assert(_initialized, "EventBus not initialized. Check autoload order in project.godot.") + # --------------------------------------------------------------------------- # Save System # --------------------------------------------------------------------------- diff --git a/scripts/game_manager.gd b/scripts/game_manager.gd index 464eb33..43ee2f2 100644 --- a/scripts/game_manager.gd +++ b/scripts/game_manager.gd @@ -16,6 +16,7 @@ const RunEndScreenScene = preload("res://scripts/ui/run_end_screen.gd") var _run_end_screen: CanvasLayer = null func _ready() -> void: + EventBus.assert_ready() # Verify autoload order EventBus.run_started.connect(_on_run_started) EventBus.run_ended.connect(_on_run_ended) call_deferred("_load_game") diff --git a/scripts/items/run_item_pickup.gd b/scripts/items/run_item_pickup.gd index 9cd19de..476078d 100644 --- a/scripts/items/run_item_pickup.gd +++ b/scripts/items/run_item_pickup.gd @@ -15,11 +15,10 @@ func _draw() -> void: func _on_body_entered(body: Node) -> void: if not body.is_in_group("player") or item == null: return - RunManager.collect_item(item) + RunManager.collect_item(item) # Triggers item_collected signal -> player_stats recalculates if item.heal_on_pickup > 0.0: body.stats.heal(item.heal_on_pickup) - body.stats.recalculate() - EventBus.player_health_changed.emit(body.stats.current_health, body.stats.max_health) + EventBus.player_health_changed.emit(body.stats.current_health, body.stats.max_health) _show_pickup_label() queue_free() diff --git a/scripts/items/shop_item_pedestal.gd b/scripts/items/shop_item_pedestal.gd index 61f1c79..e8734c9 100644 --- a/scripts/items/shop_item_pedestal.gd +++ b/scripts/items/shop_item_pedestal.gd @@ -73,11 +73,10 @@ func _attempt_purchase() -> void: if player == null: return - RunManager.collect_item(item) + RunManager.collect_item(item) # Triggers item_collected signal -> player_stats recalculates if item.heal_on_pickup > 0.0: player.stats.heal(item.heal_on_pickup) - player.stats.recalculate() - EventBus.player_health_changed.emit(player.stats.current_health, player.stats.max_health) + EventBus.player_health_changed.emit(player.stats.current_health, player.stats.max_health) _sold = true _player_in_range = false diff --git a/scripts/player/player.gd b/scripts/player/player.gd index d823da8..08ae2a3 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -29,6 +29,9 @@ var _shield_flash_timer: float = 0.0 # brief white flash on absorb # Oil slick — count of slick zones currently overlapping player var _slick_count: int = 0 +# Knockback — applied by enemies (e.g., Hivemind pulse) +var _knockback_velocity: Vector2 = Vector2.ZERO + func _ready() -> void: add_to_group("player") EventBus.room_entered.connect(_on_room_entered) @@ -42,6 +45,8 @@ func _physics_process(delta: float) -> void: _handle_shooting(delta) _handle_iframes(delta) _handle_shield(delta) + # Decay knockback + _knockback_velocity = _knockback_velocity.move_toward(Vector2.ZERO, 800.0 * delta) move_and_slide() # --------------------------------------------------------------------------- @@ -62,6 +67,9 @@ func _handle_movement() -> void: else: velocity = input_dir * stats.speed + # Apply knockback on top of movement + velocity += _knockback_velocity + if input_dir.x != 0.0: body.scale.x = sign(input_dir.x) @@ -141,6 +149,7 @@ func _handle_shield(delta: float) -> void: if _shield_ready and Input.is_physical_key_pressed(KEY_SPACE): _shield_active = true + _shield_ready = false # --------------------------------------------------------------------------- # Iframes — flash body during invincibility @@ -239,12 +248,18 @@ func _spawn_coolant_zone() -> void: zone.body_entered.connect(func(b: Node) -> void: if b.has_method("apply_slow"): - b.apply_slow(2.0, 0.4)) + b.apply_slow(2.0, 0.4) + ) + + # Use a Timer child for self-cleanup (avoids SceneTreeTimer leak) + var timer := Timer.new() + timer.wait_time = 1.0 + timer.one_shot = true + timer.timeout.connect(zone.queue_free) + zone.add_child(timer) get_parent().call_deferred("add_child", zone) - get_tree().create_timer(1.0).timeout.connect(func() -> void: - if is_instance_valid(zone): - zone.queue_free()) + timer.call_deferred("start") # --------------------------------------------------------------------------- # Oil slick — called by hazard Area2D nodes @@ -255,6 +270,12 @@ func enter_slick() -> void: func exit_slick() -> void: _slick_count = maxi(_slick_count - 1, 0) +# --------------------------------------------------------------------------- +# Knockback — called by enemies (e.g., Hivemind pulse attack) +# --------------------------------------------------------------------------- +func apply_knockback(force: Vector2) -> void: + _knockback_velocity = force + # --------------------------------------------------------------------------- # Room event handlers # --------------------------------------------------------------------------- diff --git a/scripts/player/player_stats.gd b/scripts/player/player_stats.gd index 0606d92..acbd196 100644 --- a/scripts/player/player_stats.gd +++ b/scripts/player/player_stats.gd @@ -10,6 +10,12 @@ const BASE_FIRE_RATE := 3.5 # shots per second const BASE_RANGE := 400.0 const BASE_PROJ_SPEED := 520.0 +# --------------------------------------------------------------------------- +# Stat caching — only recalculate when items/parts change +# --------------------------------------------------------------------------- +var _stats_dirty: bool = true +var _cached_item_count: int = -1 # Track item count to detect external changes + # --------------------------------------------------------------------------- # Live stats (recalculated from base + equipped parts + run items) # --------------------------------------------------------------------------- @@ -52,6 +58,7 @@ var shield_projector: bool = false func _ready() -> void: EventBus.body_part_equipped.connect(_on_part_equipped) EventBus.save_loaded.connect(_on_save_loaded) + EventBus.item_collected.connect(_on_item_collected) recalculate() if RunManager.player_health_carry > 0.0: current_health = minf(RunManager.player_health_carry, max_health) @@ -61,6 +68,7 @@ func _ready() -> void: func _on_save_loaded() -> void: var old_max := max_health + _stats_dirty = true recalculate() # Grant bonus HP from body parts on fresh start if RunManager.player_health_carry < 0.0 and max_health > old_max: @@ -68,7 +76,23 @@ func _on_save_loaded() -> void: current_health = minf(current_health, max_health) EventBus.player_health_changed.emit(current_health, max_health) +func _on_item_collected(_item: Resource) -> void: + _stats_dirty = true + recalculate() + EventBus.player_health_changed.emit(current_health, max_health) + +func mark_dirty() -> void: + _stats_dirty = true + func recalculate() -> void: + # Skip if stats haven't changed (cached) + var current_item_count := RunManager.run_items.size() + if not _stats_dirty and _cached_item_count == current_item_count: + return + + _cached_item_count = current_item_count + _stats_dirty = false + speed = BASE_SPEED + UpgradeManager.get_stat_modifier("speed") max_health = BASE_MAX_HEALTH + UpgradeManager.get_stat_modifier("max_health") damage = BASE_DAMAGE + UpgradeManager.get_stat_modifier("damage") @@ -154,6 +178,7 @@ func heal(amount: float) -> void: func _on_part_equipped(_part: Resource, _slot: String) -> void: var old_max := max_health + _stats_dirty = true recalculate() # If max_health increased, grant the extra HP immediately if max_health > old_max: diff --git a/scripts/rooms/room.gd b/scripts/rooms/room.gd index dd2c4a4..92bbcdd 100644 --- a/scripts/rooms/room.gd +++ b/scripts/rooms/room.gd @@ -57,6 +57,12 @@ var _enemy_count: int = 0 var _doors: Dictionary = {} # direction -> Area2D trigger var _physical_doors: Dictionary = {} # direction -> RoomDoor +# --------------------------------------------------------------------------- +# Signal tracking for explicit cleanup (safer than relying on auto-cleanup) +# --------------------------------------------------------------------------- +var _enemy_death_connections: Array[Dictionary] = [] # [{node, callable}] +var _door_connections: Array[Dictionary] = [] # [{node, callable}] + # --------------------------------------------------------------------------- # Node refs (set by room_base.tscn) # --------------------------------------------------------------------------- @@ -219,7 +225,9 @@ func _build_door_triggers() -> void: area.position = cfg["pos"] # Use call_deferred so the transition runs outside the physics callback - area.body_entered.connect(_on_door_entered.bind(dir)) + var callable := _on_door_entered.bind(dir) + area.body_entered.connect(callable) + _door_connections.append({"node": area, "signal": "body_entered", "callable": callable}) door_triggers.add_child(area) _doors[dir] = area @@ -291,7 +299,9 @@ func _spawn_enemies() -> void: var local_pos := Vector2(randf_range(-hw, hw), randf_range(-hh, hh)) contents.add_child(enemy) enemy.global_position = to_global(local_pos) - enemy.tree_exited.connect(_on_enemy_died, CONNECT_DEFERRED) + var callable := _on_enemy_died + enemy.tree_exited.connect(callable, CONNECT_DEFERRED) + _enemy_death_connections.append({"node": enemy, "signal": "tree_exited", "callable": callable}) _enemy_count += 1 @@ -304,11 +314,16 @@ func _spawn_boss() -> void: var boss = scene.instantiate() contents.add_child(boss) boss.global_position = to_global(Vector2.ZERO) # room centre - boss.tree_exited.connect(_on_enemy_died, CONNECT_DEFERRED) + var callable := _on_enemy_died + boss.tree_exited.connect(callable, CONNECT_DEFERRED) + _enemy_death_connections.append({"node": boss, "signal": "tree_exited", "callable": callable}) _enemy_count = 1 func _on_enemy_died() -> void: + # Guard against deferred callback running after room is freed + if not is_instance_valid(self) or not is_inside_tree(): + return _enemy_count -= 1 if _enemy_count <= 0: data.cleared = true @@ -353,7 +368,8 @@ func _spawn_floor_exit() -> void: RunManager.end_run(true) else: RunManager.advance_floor() - get_tree().call_deferred("change_scene_to_file", "res://scenes/run/floor.tscn")) + get_tree().call_deferred("change_scene_to_file", "res://scenes/run/floor.tscn") + ) contents.add_child(area) @@ -555,3 +571,21 @@ func _update_door_locks() -> void: door.lock() else: door.unlock() + +# --------------------------------------------------------------------------- +# Explicit signal cleanup on exit (safer than relying on Godot auto-cleanup) +# --------------------------------------------------------------------------- +func _exit_tree() -> void: + # Disconnect enemy death signals + for conn in _enemy_death_connections: + var node: Node = conn["node"] + if is_instance_valid(node) and node.is_connected(conn["signal"], conn["callable"]): + node.disconnect(conn["signal"], conn["callable"]) + _enemy_death_connections.clear() + + # Disconnect door trigger signals + for conn in _door_connections: + var node: Node = conn["node"] + if is_instance_valid(node) and node.is_connected(conn["signal"], conn["callable"]): + node.disconnect(conn["signal"], conn["callable"]) + _door_connections.clear() diff --git a/scripts/run_manager.gd b/scripts/run_manager.gd index 7a314ee..777fc56 100644 --- a/scripts/run_manager.gd +++ b/scripts/run_manager.gd @@ -1,5 +1,8 @@ extends Node +func _ready() -> void: + EventBus.assert_ready() # Verify autoload order + var current_floor: int = 0 var current_room_id: String = "" var run_active: bool = false diff --git a/scripts/ui/hud.gd b/scripts/ui/hud.gd index 2e004d4..1a1ef64 100644 --- a/scripts/ui/hud.gd +++ b/scripts/ui/hud.gd @@ -28,13 +28,6 @@ const EMPTY_COLOR := Color(0.18, 0.04, 0.04) # dark maroon (background) const HEART_SPACING := 52 # px between heart centers const ORIGIN := Vector2(32, 32) # center of first heart (screen px) -# --------------------------------------------------------------------------- -# DEPRECATED — Boss bar (commented out) -# --------------------------------------------------------------------------- -#const BOSS_BAR_W := 400.0 -#const BOSS_BAR_H := 18.0 -#const BOSS_BAR_POS := Vector2(760.0, 1040.0) # bottom center (1920×1080 viewport) - # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- @@ -43,12 +36,6 @@ var _slots: Array = [] var _credits_label: Label var _mini_map: Control -# DEPRECATED — Boss bar nodes (commented out) -#var _boss_bar_root: Node2D = null -#var _boss_bar_fill: Polygon2D = null -#var _boss_bar_label: Label = null -#var _boss_max_hp: float = 1.0 - # --------------------------------------------------------------------------- # Boot # --------------------------------------------------------------------------- @@ -56,9 +43,6 @@ func _ready() -> void: EventBus.player_health_changed.connect(_on_health_changed) EventBus.credits_changed.connect(_on_credits_changed) EventBus.save_loaded.connect(_on_save_loaded) - # DEPRECATED — Boss bar signal connections (commented out) - #EventBus.boss_health_changed.connect(_on_boss_health_changed) - #EventBus.boss_died.connect(_on_boss_died) _build_credits_label() _build_mini_map() # Initialise once the scene is fully set up @@ -153,69 +137,3 @@ func _on_health_changed(current_hp: float, max_hp: float) -> void: _rebuild(max_hp, current_hp) else: _update_display(current_hp) - -# --------------------------------------------------------------------------- -# DEPRECATED — Boss health bar (commented out) -# --------------------------------------------------------------------------- -#var _boss_name: String = "" -# -#func _on_boss_health_changed(current_hp: float, max_hp: float, boss_name: String) -> void: -# if _boss_bar_root == null: -# _build_boss_bar(max_hp, boss_name) -# elif _boss_name != boss_name: -# # Different boss, update label -# _boss_name = boss_name -# if _boss_bar_label != null: -# _boss_bar_label.text = boss_name -# _boss_max_hp = max_hp -# _update_boss_bar(current_hp) -# -#func _on_boss_died() -> void: -# if _boss_bar_root != null: -# _boss_bar_root.queue_free() -# _boss_bar_root = null -# _boss_bar_fill = null -# _boss_bar_label = null -# _boss_name = "" -# -#func _build_boss_bar(max_hp: float, boss_name: String) -> void: -# _boss_max_hp = max_hp -# _boss_name = boss_name -# -# _boss_bar_root = Node2D.new() -# _boss_bar_root.position = BOSS_BAR_POS -# add_child(_boss_bar_root) -# -# # Name label -# _boss_bar_label = Label.new() -# _boss_bar_label.text = boss_name -# _boss_bar_label.position = Vector2(0, -22) -# _boss_bar_root.add_child(_boss_bar_label) -# -# # Background -# var bg := Polygon2D.new() -# bg.polygon = PackedVector2Array([ -# Vector2(0, 0), Vector2(BOSS_BAR_W, 0), -# Vector2(BOSS_BAR_W, BOSS_BAR_H), Vector2(0, BOSS_BAR_H), -# ]) -# bg.color = Color(0.15, 0.05, 0.05) -# _boss_bar_root.add_child(bg) -# -# # Fill -# _boss_bar_fill = Polygon2D.new() -# _boss_bar_fill.polygon = PackedVector2Array([ -# Vector2(0, 0), Vector2(BOSS_BAR_W, 0), -# Vector2(BOSS_BAR_W, BOSS_BAR_H), Vector2(0, BOSS_BAR_H), -# ]) -# _boss_bar_fill.color = Color(0.85, 0.1, 0.1) -# _boss_bar_root.add_child(_boss_bar_fill) -# -#func _update_boss_bar(current_hp: float) -> void: -# if _boss_bar_fill == null: -# return -# var pct := clampf(current_hp / _boss_max_hp, 0.0, 1.0) -# var w := BOSS_BAR_W * pct -# _boss_bar_fill.polygon = PackedVector2Array([ -# Vector2(0, 0), Vector2(w, 0), -# Vector2(w, BOSS_BAR_H), Vector2(0, BOSS_BAR_H), -# ]) diff --git a/scripts/ui/mini_map.gd b/scripts/ui/mini_map.gd index c788c1f..7ccedc7 100644 --- a/scripts/ui/mini_map.gd +++ b/scripts/ui/mini_map.gd @@ -83,8 +83,8 @@ func _process(_delta: float) -> void: if _rooms_data.is_empty(): return - # Find player if not cached - if _player == null: + # Find player if not cached or freed + if _player == null or not is_instance_valid(_player): _player = get_tree().get_first_node_in_group("player") if _player == null: return