From 2d5129da0ed0897dbd654baf4d6dc76b13d7e492 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Thu, 12 Mar 2026 16:23:07 -0400 Subject: [PATCH] Hivemind audio and behavior fixes, boss item drops, duplicate run item handling, out of stock handling --- scripts/enemies/hivemind.gd | 28 ++++++++-- scripts/enemies/relay.gd | 24 +++++++-- scripts/enemies/warden.gd | 24 +++++++-- scripts/rooms/room.gd | 101 ++++++++++++++++++++++++++++++------ 4 files changed, 147 insertions(+), 30 deletions(-) diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd index 887b154..44fd53c 100644 --- a/scripts/enemies/hivemind.gd +++ b/scripts/enemies/hivemind.gd @@ -389,6 +389,9 @@ func _tick_active(delta: float) -> void: func _tick_teleport(delta: float) -> void: match _tp_state: TeleportState.IDLE: + # Don't start teleport during beam attack + if _beam_telegraphing or _beam_active: + return _teleport_timer -= delta if _teleport_timer <= 0.0: _start_fade_out() @@ -404,6 +407,7 @@ func _tick_teleport(delta: float) -> void: func _start_fade_out() -> void: + AudioManager.play("hivemind_teleport") _tp_state = TeleportState.FADING_OUT _fade_timer = FADE_OUT_TIME @@ -987,11 +991,24 @@ func _die() -> void: queue_free() func _spawn_item_drop() -> void: - var chosen: String = ALL_ITEM_PATHS[randi() % ALL_ITEM_PATHS.size()] + # Filter out already collected items + var available: Array[String] = [] + for path in ALL_ITEM_PATHS: + var already_collected := false + for collected in RunManager.run_items: + if collected.resource_path == path: + already_collected = true + break + if not already_collected: + available.append(path) + if available.is_empty(): + return + var chosen: String = available[randi() % available.size()] var pickup = RUN_ITEM_PICKUP.instantiate() pickup.item = load(chosen) - pickup.global_position = global_position + Vector2(-30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(-30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) func _spawn_body_part_drop() -> void: var unacquired: Array[String] = [] @@ -1003,5 +1020,6 @@ func _spawn_body_part_drop() -> void: var chosen: String = unacquired[randi() % unacquired.size()] var pickup = BODY_PART_PICKUP.instantiate() pickup.part = load(chosen) - pickup.global_position = global_position + Vector2(30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) diff --git a/scripts/enemies/relay.gd b/scripts/enemies/relay.gd index c0ba3b5..b365e87 100644 --- a/scripts/enemies/relay.gd +++ b/scripts/enemies/relay.gd @@ -466,11 +466,24 @@ func _die() -> void: queue_free() func _spawn_item_drop() -> void: - var chosen: String = ALL_ITEM_PATHS[randi() % ALL_ITEM_PATHS.size()] + # Filter out already collected items + var available: Array[String] = [] + for path in ALL_ITEM_PATHS: + var already_collected := false + for collected in RunManager.run_items: + if collected.resource_path == path: + already_collected = true + break + if not already_collected: + available.append(path) + if available.is_empty(): + return + var chosen: String = available[randi() % available.size()] var pickup = RUN_ITEM_PICKUP.instantiate() pickup.item = load(chosen) - pickup.global_position = global_position + Vector2(-30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(-30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) func _spawn_body_part_drop() -> void: var unacquired: Array[String] = [] @@ -482,5 +495,6 @@ func _spawn_body_part_drop() -> void: var chosen: String = unacquired[randi() % unacquired.size()] var pickup = BODY_PART_PICKUP.instantiate() pickup.part = load(chosen) - pickup.global_position = global_position + Vector2(30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) diff --git a/scripts/enemies/warden.gd b/scripts/enemies/warden.gd index d9488b5..81221d2 100644 --- a/scripts/enemies/warden.gd +++ b/scripts/enemies/warden.gd @@ -420,11 +420,24 @@ func _die() -> void: queue_free() func _spawn_item_drop() -> void: - var chosen: String = ALL_ITEM_PATHS[randi() % ALL_ITEM_PATHS.size()] + # Filter out already collected items + var available: Array[String] = [] + for path in ALL_ITEM_PATHS: + var dominated := false + for collected in RunManager.run_items: + if collected.resource_path == path: + dominated = true + break + if not dominated: + available.append(path) + if available.is_empty(): + return + var chosen: String = available[randi() % available.size()] var pickup = RUN_ITEM_PICKUP.instantiate() pickup.item = load(chosen) - pickup.global_position = global_position + Vector2(-30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(-30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) func _spawn_body_part_drop() -> void: var unacquired: Array[String] = [] @@ -436,8 +449,9 @@ func _spawn_body_part_drop() -> void: var chosen: String = unacquired[randi() % unacquired.size()] var pickup = BODY_PART_PICKUP.instantiate() pickup.part = load(chosen) - pickup.global_position = global_position + Vector2(30, 0) - get_parent().add_child(pickup) + var spawn_pos := global_position + Vector2(30, 0) + get_parent().call_deferred("add_child", pickup) + pickup.set_deferred("global_position", spawn_pos) # --------------------------------------------------------------------------- # Visual — phase-aware tints (overrides EnemyBase._tick_flash) diff --git a/scripts/rooms/room.gd b/scripts/rooms/room.gd index a6dad88..090c15f 100644 --- a/scripts/rooms/room.gd +++ b/scripts/rooms/room.gd @@ -397,19 +397,64 @@ func _spawn_body_part_drop() -> void: contents.add_child(pickup) func _spawn_run_item() -> void: - # Exclude Patch Kit from item rooms (shop-only item) + # Exclude Patch Kit from item rooms (shop-only item) and already collected items const PATCH_KIT_PATH := "res://data/run_items/patch_kit.tres" var item_pool: Array[String] = [] for p in ALL_ITEM_PATHS: - if p != PATCH_KIT_PATH: + if p == PATCH_KIT_PATH: + continue + var already_collected := false + for collected in RunManager.run_items: + if collected.resource_path == p: + already_collected = true + break + if not already_collected: item_pool.append(p) + if item_pool.is_empty(): + _spawn_out_of_stock_marker(Vector2.ZERO) + return + var chosen: String = item_pool[randi() % item_pool.size()] var pickup = RUN_ITEM_PICKUP.instantiate() pickup.item = load(chosen) pickup.position = Vector2.ZERO contents.add_child(pickup) +func _spawn_out_of_stock_marker(pos: Vector2) -> void: + var marker := Node2D.new() + marker.position = pos + + # Grey pedestal + var pedestal := Polygon2D.new() + pedestal.polygon = PackedVector2Array([ + Vector2(-14, -14), Vector2(14, -14), + Vector2(14, 14), Vector2(-14, 14) + ]) + pedestal.color = Color(0.3, 0.3, 0.3) + marker.add_child(pedestal) + + # X mark + var x_line := Line2D.new() + x_line.points = PackedVector2Array([Vector2(-10, -10), Vector2(10, 10)]) + x_line.default_color = Color(0.5, 0.5, 0.5) + x_line.width = 2.0 + marker.add_child(x_line) + var x_line2 := Line2D.new() + x_line2.points = PackedVector2Array([Vector2(10, -10), Vector2(-10, 10)]) + x_line2.default_color = Color(0.5, 0.5, 0.5) + x_line2.width = 2.0 + marker.add_child(x_line2) + + # Label + var lbl := Label.new() + lbl.text = "OUT OF STOCK" + lbl.position = Vector2(-48, -40) + lbl.modulate = Color(0.6, 0.6, 0.6) + marker.add_child(lbl) + + contents.add_child(marker) + # --------------------------------------------------------------------------- # Environmental hazards — spawned in combat rooms # --------------------------------------------------------------------------- @@ -555,11 +600,18 @@ func _make_exposed_wiring() -> void: # Shop spawning — 2 random items + 1 Patch Kit, spaced horizontally # --------------------------------------------------------------------------- func _spawn_shop() -> void: - # Item slot paths (exclude Patch Kit from the random pool) + # Item slot paths (exclude Patch Kit and already collected items from the random pool) const PATCH_KIT_PATH := "res://data/run_items/patch_kit.tres" var item_pool: Array[String] = [] for p in ALL_ITEM_PATHS: - if p != PATCH_KIT_PATH: + if p == PATCH_KIT_PATH: + continue + var already_collected := false + for collected in RunManager.run_items: + if collected.resource_path == p: + already_collected = true + break + if not already_collected: item_pool.append(p) item_pool.shuffle() @@ -570,20 +622,39 @@ func _spawn_shop() -> void: Vector2( 200, 0), ] - var chosen: Array[String] = [ - item_pool[0], - item_pool[1], - PATCH_KIT_PATH, - ] + # Check if Patch Kit was already collected + var patch_kit_collected := false + for collected in RunManager.run_items: + if collected.resource_path == PATCH_KIT_PATH: + patch_kit_collected = true + break + var prices: Array[int] = [20, 20, 10] for i in 3: - var pedestal := Area2D.new() - pedestal.set_script(SHOP_PEDESTAL_SCR) - pedestal.position = positions[i] - pedestal.set("item", load(chosen[i])) - pedestal.set("price", prices[i]) - contents.add_child(pedestal) + var pos: Vector2 = positions[i] + if i == 2: + # Patch Kit slot + if patch_kit_collected: + _spawn_out_of_stock_marker(pos) + else: + var pedestal := Area2D.new() + pedestal.set_script(SHOP_PEDESTAL_SCR) + pedestal.position = pos + pedestal.set("item", load(PATCH_KIT_PATH)) + pedestal.set("price", prices[i]) + contents.add_child(pedestal) + else: + # Random item slots + if i < item_pool.size(): + var pedestal := Area2D.new() + pedestal.set_script(SHOP_PEDESTAL_SCR) + pedestal.position = pos + pedestal.set("item", load(item_pool[i])) + pedestal.set("price", prices[i]) + contents.add_child(pedestal) + else: + _spawn_out_of_stock_marker(pos) # --------------------------------------------------------------------------- # Door locking