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
This commit is contained in:
+6
-1
@@ -21,11 +21,16 @@ config/icon="res://assets/icon.svg"
|
|||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
|
# ===== AUTOLOAD INITIALIZATION ORDER =====
|
||||||
|
# Order is critical! Dependencies must load before dependents.
|
||||||
|
# Layer 0: Event system (no dependencies)
|
||||||
EventBus="*res://scripts/event_bus.gd"
|
EventBus="*res://scripts/event_bus.gd"
|
||||||
|
# Layer 1: Core managers (depend on EventBus only)
|
||||||
GameManager="*res://scripts/game_manager.gd"
|
GameManager="*res://scripts/game_manager.gd"
|
||||||
RunManager="*res://scripts/run_manager.gd"
|
|
||||||
SaveManager="*res://scripts/save_manager.gd"
|
SaveManager="*res://scripts/save_manager.gd"
|
||||||
UpgradeManager="*res://scripts/upgrades/upgrade_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"
|
CardCollection="*res://scripts/cards/card_collection.gd"
|
||||||
NPCManager="*res://scripts/npcs/npc_manager.gd"
|
NPCManager="*res://scripts/npcs/npc_manager.gd"
|
||||||
LoreManager="*res://scripts/lore/lore_manager.gd"
|
LoreManager="*res://scripts/lore/lore_manager.gd"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
State.ANCHORED:
|
State.ANCHORED:
|
||||||
_do_anchored(delta)
|
_do_anchored(delta)
|
||||||
|
|
||||||
velocity *= _slow_factor
|
velocity *= get_slow_factor()
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
if not is_active():
|
if not is_active():
|
||||||
return
|
return
|
||||||
_do_arc_chase(delta)
|
_do_arc_chase(delta)
|
||||||
velocity *= _slow_factor
|
velocity *= get_slow_factor()
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ const SPAWN_DELAY: float = 0.5
|
|||||||
|
|
||||||
var _contact_timer: float = 0.0
|
var _contact_timer: float = 0.0
|
||||||
var _flash_timer: float = 0.0
|
var _flash_timer: float = 0.0
|
||||||
var _slow_factor: float = 1.0
|
var _slow_effects: Array[Dictionary] = [] # [{factor, timer}] - stacking slows
|
||||||
var _slow_timer: float = 0.0
|
|
||||||
var _spawn_timer: float = SPAWN_DELAY
|
var _spawn_timer: float = SPAWN_DELAY
|
||||||
var _player: Node2D = null
|
var _player: Node2D = null
|
||||||
|
|
||||||
@@ -62,14 +61,24 @@ func _tick_flash(delta: float) -> void:
|
|||||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||||
|
|
||||||
func apply_slow(duration: float, factor: float) -> void:
|
func apply_slow(duration: float, factor: float) -> void:
|
||||||
_slow_factor = factor
|
# Stack slows by adding new effect - factors multiply together
|
||||||
_slow_timer = duration
|
_slow_effects.append({"factor": factor, "timer": duration})
|
||||||
|
|
||||||
func _tick_slow(delta: float) -> void:
|
func _tick_slow(delta: float) -> void:
|
||||||
if _slow_timer > 0.0:
|
# Tick down all slow effects and remove expired ones
|
||||||
_slow_timer -= delta
|
var i := _slow_effects.size() - 1
|
||||||
if _slow_timer <= 0.0:
|
while i >= 0:
|
||||||
_slow_factor = 1.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
|
# Damage / death
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ const HOMING_LIFETIME := 7.0 # seconds before homing projectiles expire
|
|||||||
|
|
||||||
const FREEZE_DURATION := 2.0
|
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 DRIFTER_SCENE = preload("res://scenes/enemies/drifter.tscn")
|
||||||
const REPEATER_SCENE = preload("res://scenes/enemies/repeater.tscn")
|
const REPEATER_SCENE = preload("res://scenes/enemies/repeater.tscn")
|
||||||
const ANCHOR_SCENE = preload("res://scenes/enemies/anchor.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:
|
func _on_orbiter_hit(body: Node, _proj: Node2D) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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:
|
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:
|
func _on_sweep_hit(body: Node, proj: Node2D) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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):
|
if is_instance_valid(proj):
|
||||||
proj.queue_free()
|
proj.queue_free()
|
||||||
|
|
||||||
@@ -738,7 +743,7 @@ func _end_beam() -> void:
|
|||||||
|
|
||||||
func _on_beam_hit(body: Node) -> void:
|
func _on_beam_hit(body: Node) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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:
|
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:
|
func _on_homing_hit(body: Node, proj: Node2D) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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):
|
if is_instance_valid(proj):
|
||||||
proj.queue_free()
|
proj.queue_free()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ const DRIFTER_OFFSET_MAX := 80.0
|
|||||||
|
|
||||||
const FREEZE_DURATION := 1.5
|
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")
|
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:
|
func _on_burst_hit_player(body: Node2D, proj: Node2D) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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):
|
if is_instance_valid(proj):
|
||||||
proj.queue_free()
|
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:
|
func _on_homing_hit_player(body: Node2D, proj: Node2D) -> void:
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
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):
|
if is_instance_valid(proj):
|
||||||
proj.queue_free()
|
proj.queue_free()
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
_do_fire(delta)
|
_do_fire(delta)
|
||||||
velocity *= _slow_factor
|
velocity *= get_slow_factor()
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ const WIND_UP_DURATION := 0.65 # seconds of warning before beam fires
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
const FREEZE_DURATION := 2.0
|
const FREEZE_DURATION := 2.0
|
||||||
|
|
||||||
|
const BEAM_DAMAGE := 1.0
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Phase 2 Drifter spawning — one at a time, over a fixed window
|
# 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.add_child(cs)
|
||||||
area.body_entered.connect(func(body: Node) -> void:
|
area.body_entered.connect(func(body: Node) -> void:
|
||||||
if body.is_in_group("player"):
|
if body.is_in_group("player"):
|
||||||
body.take_damage(1.0))
|
body.take_damage(BEAM_DAMAGE))
|
||||||
beam.add_child(area)
|
beam.add_child(area)
|
||||||
|
|
||||||
get_parent().call_deferred("add_child", beam)
|
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)
|
# Damage / death — override base to emit boss signals; no drop (room handles it)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
func take_damage(amount: float) -> void:
|
func take_damage(amount: float) -> void:
|
||||||
|
if _phase == Phase.CLIMAX:
|
||||||
|
return # already dying
|
||||||
current_health -= amount
|
current_health -= amount
|
||||||
_flash_timer = 0.1
|
_flash_timer = 0.1
|
||||||
EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE SUPERVISOR")
|
EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE SUPERVISOR")
|
||||||
if current_health <= 0.0:
|
if current_health <= 0.0:
|
||||||
_die()
|
_enter_climax()
|
||||||
|
|
||||||
func _die() -> void:
|
func _die() -> void:
|
||||||
EventBus.boss_died.emit()
|
EventBus.boss_died.emit()
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ const MINE_AOE := 70.0
|
|||||||
|
|
||||||
const FREEZE_DURATION := 1.5
|
const FREEZE_DURATION := 1.5
|
||||||
|
|
||||||
|
const SWEEP_PROJ_DAMAGE := 1.0
|
||||||
|
const MINE_DAMAGE := 1.0
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# State
|
# State
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -215,7 +218,7 @@ func _spawn_sweep_proj(world_pos: Vector2, vel: Vector2) -> void:
|
|||||||
area.add_child(cs)
|
area.add_child(cs)
|
||||||
area.body_entered.connect(func(body: Node) -> void:
|
area.body_entered.connect(func(body: Node) -> void:
|
||||||
if body.is_in_group("player"):
|
if body.is_in_group("player"):
|
||||||
body.take_damage(1.0)
|
body.take_damage(SWEEP_PROJ_DAMAGE)
|
||||||
proj.queue_free())
|
proj.queue_free())
|
||||||
proj.add_child(area)
|
proj.add_child(area)
|
||||||
|
|
||||||
@@ -321,7 +324,7 @@ func _explode_mine(entry: Dictionary) -> void:
|
|||||||
if _player != null and is_instance_valid(_player):
|
if _player != null and is_instance_valid(_player):
|
||||||
var dist := node.global_position.distance_to(_player.global_position)
|
var dist := node.global_position.distance_to(_player.global_position)
|
||||||
if dist <= MINE_AOE:
|
if dist <= MINE_AOE:
|
||||||
_player.take_damage(1.0)
|
_player.take_damage(MINE_DAMAGE)
|
||||||
|
|
||||||
# Brief visual flash for explosion (optional: could add particle later)
|
# Brief visual flash for explosion (optional: could add particle later)
|
||||||
var flash := Polygon2D.new()
|
var flash := Polygon2D.new()
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
extends Node
|
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
|
# Save System
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const RunEndScreenScene = preload("res://scripts/ui/run_end_screen.gd")
|
|||||||
var _run_end_screen: CanvasLayer = null
|
var _run_end_screen: CanvasLayer = null
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
EventBus.assert_ready() # Verify autoload order
|
||||||
EventBus.run_started.connect(_on_run_started)
|
EventBus.run_started.connect(_on_run_started)
|
||||||
EventBus.run_ended.connect(_on_run_ended)
|
EventBus.run_ended.connect(_on_run_ended)
|
||||||
call_deferred("_load_game")
|
call_deferred("_load_game")
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ func _draw() -> void:
|
|||||||
func _on_body_entered(body: Node) -> void:
|
func _on_body_entered(body: Node) -> void:
|
||||||
if not body.is_in_group("player") or item == null:
|
if not body.is_in_group("player") or item == null:
|
||||||
return
|
return
|
||||||
RunManager.collect_item(item)
|
RunManager.collect_item(item) # Triggers item_collected signal -> player_stats recalculates
|
||||||
if item.heal_on_pickup > 0.0:
|
if item.heal_on_pickup > 0.0:
|
||||||
body.stats.heal(item.heal_on_pickup)
|
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()
|
_show_pickup_label()
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|||||||
@@ -73,10 +73,9 @@ func _attempt_purchase() -> void:
|
|||||||
if player == null:
|
if player == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
RunManager.collect_item(item)
|
RunManager.collect_item(item) # Triggers item_collected signal -> player_stats recalculates
|
||||||
if item.heal_on_pickup > 0.0:
|
if item.heal_on_pickup > 0.0:
|
||||||
player.stats.heal(item.heal_on_pickup)
|
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
|
_sold = true
|
||||||
|
|||||||
@@ -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
|
# Oil slick — count of slick zones currently overlapping player
|
||||||
var _slick_count: int = 0
|
var _slick_count: int = 0
|
||||||
|
|
||||||
|
# Knockback — applied by enemies (e.g., Hivemind pulse)
|
||||||
|
var _knockback_velocity: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("player")
|
add_to_group("player")
|
||||||
EventBus.room_entered.connect(_on_room_entered)
|
EventBus.room_entered.connect(_on_room_entered)
|
||||||
@@ -42,6 +45,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
_handle_shooting(delta)
|
_handle_shooting(delta)
|
||||||
_handle_iframes(delta)
|
_handle_iframes(delta)
|
||||||
_handle_shield(delta)
|
_handle_shield(delta)
|
||||||
|
# Decay knockback
|
||||||
|
_knockback_velocity = _knockback_velocity.move_toward(Vector2.ZERO, 800.0 * delta)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -62,6 +67,9 @@ func _handle_movement() -> void:
|
|||||||
else:
|
else:
|
||||||
velocity = input_dir * stats.speed
|
velocity = input_dir * stats.speed
|
||||||
|
|
||||||
|
# Apply knockback on top of movement
|
||||||
|
velocity += _knockback_velocity
|
||||||
|
|
||||||
if input_dir.x != 0.0:
|
if input_dir.x != 0.0:
|
||||||
body.scale.x = sign(input_dir.x)
|
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):
|
if _shield_ready and Input.is_physical_key_pressed(KEY_SPACE):
|
||||||
_shield_active = true
|
_shield_active = true
|
||||||
|
_shield_ready = false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Iframes — flash body during invincibility
|
# Iframes — flash body during invincibility
|
||||||
@@ -239,12 +248,18 @@ func _spawn_coolant_zone() -> void:
|
|||||||
|
|
||||||
zone.body_entered.connect(func(b: Node) -> void:
|
zone.body_entered.connect(func(b: Node) -> void:
|
||||||
if b.has_method("apply_slow"):
|
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_parent().call_deferred("add_child", zone)
|
||||||
get_tree().create_timer(1.0).timeout.connect(func() -> void:
|
timer.call_deferred("start")
|
||||||
if is_instance_valid(zone):
|
|
||||||
zone.queue_free())
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Oil slick — called by hazard Area2D nodes
|
# Oil slick — called by hazard Area2D nodes
|
||||||
@@ -255,6 +270,12 @@ func enter_slick() -> void:
|
|||||||
func exit_slick() -> void:
|
func exit_slick() -> void:
|
||||||
_slick_count = maxi(_slick_count - 1, 0)
|
_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
|
# Room event handlers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ const BASE_FIRE_RATE := 3.5 # shots per second
|
|||||||
const BASE_RANGE := 400.0
|
const BASE_RANGE := 400.0
|
||||||
const BASE_PROJ_SPEED := 520.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)
|
# Live stats (recalculated from base + equipped parts + run items)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -52,6 +58,7 @@ var shield_projector: bool = false
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
EventBus.body_part_equipped.connect(_on_part_equipped)
|
EventBus.body_part_equipped.connect(_on_part_equipped)
|
||||||
EventBus.save_loaded.connect(_on_save_loaded)
|
EventBus.save_loaded.connect(_on_save_loaded)
|
||||||
|
EventBus.item_collected.connect(_on_item_collected)
|
||||||
recalculate()
|
recalculate()
|
||||||
if RunManager.player_health_carry > 0.0:
|
if RunManager.player_health_carry > 0.0:
|
||||||
current_health = minf(RunManager.player_health_carry, max_health)
|
current_health = minf(RunManager.player_health_carry, max_health)
|
||||||
@@ -61,6 +68,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func _on_save_loaded() -> void:
|
func _on_save_loaded() -> void:
|
||||||
var old_max := max_health
|
var old_max := max_health
|
||||||
|
_stats_dirty = true
|
||||||
recalculate()
|
recalculate()
|
||||||
# Grant bonus HP from body parts on fresh start
|
# Grant bonus HP from body parts on fresh start
|
||||||
if RunManager.player_health_carry < 0.0 and max_health > old_max:
|
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)
|
current_health = minf(current_health, max_health)
|
||||||
EventBus.player_health_changed.emit(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:
|
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")
|
speed = BASE_SPEED + UpgradeManager.get_stat_modifier("speed")
|
||||||
max_health = BASE_MAX_HEALTH + UpgradeManager.get_stat_modifier("max_health")
|
max_health = BASE_MAX_HEALTH + UpgradeManager.get_stat_modifier("max_health")
|
||||||
damage = BASE_DAMAGE + UpgradeManager.get_stat_modifier("damage")
|
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:
|
func _on_part_equipped(_part: Resource, _slot: String) -> void:
|
||||||
var old_max := max_health
|
var old_max := max_health
|
||||||
|
_stats_dirty = true
|
||||||
recalculate()
|
recalculate()
|
||||||
# If max_health increased, grant the extra HP immediately
|
# If max_health increased, grant the extra HP immediately
|
||||||
if max_health > old_max:
|
if max_health > old_max:
|
||||||
|
|||||||
+38
-4
@@ -57,6 +57,12 @@ var _enemy_count: int = 0
|
|||||||
var _doors: Dictionary = {} # direction -> Area2D trigger
|
var _doors: Dictionary = {} # direction -> Area2D trigger
|
||||||
var _physical_doors: Dictionary = {} # direction -> RoomDoor
|
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)
|
# Node refs (set by room_base.tscn)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -219,7 +225,9 @@ func _build_door_triggers() -> void:
|
|||||||
area.position = cfg["pos"]
|
area.position = cfg["pos"]
|
||||||
|
|
||||||
# Use call_deferred so the transition runs outside the physics callback
|
# 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)
|
door_triggers.add_child(area)
|
||||||
_doors[dir] = area
|
_doors[dir] = area
|
||||||
|
|
||||||
@@ -291,7 +299,9 @@ func _spawn_enemies() -> void:
|
|||||||
var local_pos := Vector2(randf_range(-hw, hw), randf_range(-hh, hh))
|
var local_pos := Vector2(randf_range(-hw, hw), randf_range(-hh, hh))
|
||||||
contents.add_child(enemy)
|
contents.add_child(enemy)
|
||||||
enemy.global_position = to_global(local_pos)
|
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
|
_enemy_count += 1
|
||||||
|
|
||||||
|
|
||||||
@@ -304,11 +314,16 @@ func _spawn_boss() -> void:
|
|||||||
var boss = scene.instantiate()
|
var boss = scene.instantiate()
|
||||||
contents.add_child(boss)
|
contents.add_child(boss)
|
||||||
boss.global_position = to_global(Vector2.ZERO) # room centre
|
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
|
_enemy_count = 1
|
||||||
|
|
||||||
|
|
||||||
func _on_enemy_died() -> void:
|
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
|
_enemy_count -= 1
|
||||||
if _enemy_count <= 0:
|
if _enemy_count <= 0:
|
||||||
data.cleared = true
|
data.cleared = true
|
||||||
@@ -353,7 +368,8 @@ func _spawn_floor_exit() -> void:
|
|||||||
RunManager.end_run(true)
|
RunManager.end_run(true)
|
||||||
else:
|
else:
|
||||||
RunManager.advance_floor()
|
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)
|
contents.add_child(area)
|
||||||
|
|
||||||
|
|
||||||
@@ -555,3 +571,21 @@ func _update_door_locks() -> void:
|
|||||||
door.lock()
|
door.lock()
|
||||||
else:
|
else:
|
||||||
door.unlock()
|
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()
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
EventBus.assert_ready() # Verify autoload order
|
||||||
|
|
||||||
var current_floor: int = 0
|
var current_floor: int = 0
|
||||||
var current_room_id: String = ""
|
var current_room_id: String = ""
|
||||||
var run_active: bool = false
|
var run_active: bool = false
|
||||||
|
|||||||
@@ -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 HEART_SPACING := 52 # px between heart centers
|
||||||
const ORIGIN := Vector2(32, 32) # center of first heart (screen px)
|
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
|
# State
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -43,12 +36,6 @@ var _slots: Array = []
|
|||||||
var _credits_label: Label
|
var _credits_label: Label
|
||||||
var _mini_map: Control
|
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
|
# Boot
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -56,9 +43,6 @@ func _ready() -> void:
|
|||||||
EventBus.player_health_changed.connect(_on_health_changed)
|
EventBus.player_health_changed.connect(_on_health_changed)
|
||||||
EventBus.credits_changed.connect(_on_credits_changed)
|
EventBus.credits_changed.connect(_on_credits_changed)
|
||||||
EventBus.save_loaded.connect(_on_save_loaded)
|
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_credits_label()
|
||||||
_build_mini_map()
|
_build_mini_map()
|
||||||
# Initialise once the scene is fully set up
|
# 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)
|
_rebuild(max_hp, current_hp)
|
||||||
else:
|
else:
|
||||||
_update_display(current_hp)
|
_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),
|
|
||||||
# ])
|
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ func _process(_delta: float) -> void:
|
|||||||
if _rooms_data.is_empty():
|
if _rooms_data.is_empty():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find player if not cached
|
# Find player if not cached or freed
|
||||||
if _player == null:
|
if _player == null or not is_instance_valid(_player):
|
||||||
_player = get_tree().get_first_node_in_group("player")
|
_player = get_tree().get_first_node_in_group("player")
|
||||||
if _player == null:
|
if _player == null:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user