From 171a40a4a8f88598daf5fa7f5e82783c86d05627 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Mon, 9 Mar 2026 11:37:06 -0400 Subject: [PATCH] Fixes Relay and Practice room and implements Hivemind boss --- scenes/enemies/hivemind.tscn | 26 ++ scripts/enemies/hivemind.gd | 575 +++++++++++++++++++++++++++++++++++ scripts/enemies/relay.gd | 53 ++-- scripts/hub/hub_room.gd | 13 + scripts/rooms/room.gd | 3 +- 5 files changed, 643 insertions(+), 27 deletions(-) create mode 100644 scenes/enemies/hivemind.tscn create mode 100644 scripts/enemies/hivemind.gd diff --git a/scenes/enemies/hivemind.tscn b/scenes/enemies/hivemind.tscn new file mode 100644 index 0000000..05136d0 --- /dev/null +++ b/scenes/enemies/hivemind.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/enemies/hivemind.gd" id="1"] + +[sub_resource type="CircleShape2D" id="1"] +radius = 35.0 + +[sub_resource type="CircleShape2D" id="2"] +radius = 40.0 + +[node name="Hivemind" type="CharacterBody2D"] +collision_layer = 8 +collision_mask = 1 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape2D" parent="."] +shape = SubResource("1") + +[node name="Visual" type="Node2D" parent="."] + +[node name="ContactArea" type="Area2D" parent="."] +collision_layer = 0 +collision_mask = 2 + +[node name="ContactCollision" type="CollisionShape2D" parent="ContactArea"] +shape = SubResource("2") diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd new file mode 100644 index 0000000..c9c6739 --- /dev/null +++ b/scripts/enemies/hivemind.gd @@ -0,0 +1,575 @@ +extends EnemyBase + +# --------------------------------------------------------------------------- +# Hivemind — Floor 3 Final Boss +# Phase 1: Assembly (spawn waves, enemies join core on death) +# Phase 2: Active (mimics Repeater spread, Warden sweep, Drifter projectiles) +# Phase 3: Unique (rotating beam, room pulse, homing projectiles) +# --------------------------------------------------------------------------- + +enum Phase { ASSEMBLY, ACTIVE, UNIQUE, CLIMAX } + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +const ROOM_HW := 480.0 +const ROOM_HH := 270.0 +const WALL_T := 32.0 + +# Assembly phase +const WAVE_COUNT := 3 +const ENEMIES_PER_WAVE := 5 +const WAVE_DELAY := 2.0 +const ASSEMBLY_COMPLETE := 15 # kills needed to complete assembly + +# Phase 2 attacks +const ATTACK_CD_P2 := 3.5 +const SPREAD_COUNT := 5 +const SPREAD_ANGLE := 0.6 # radians total spread +const SPREAD_SPEED := 120.0 +const SWEEP_SPEED := 60.0 +const SWEEP_PROJ_SPACE := 22.0 +const DRIFT_COUNT := 4 +const DRIFT_SPEED := 45.0 +const DRIFT_CURVE := 0.8 + +# Phase 3 attacks +const ATTACK_CD_P3 := 2.5 +const BEAM_ROT_SPEED := 1.2 # rad/sec +const BEAM_DURATION := 3.0 +const BEAM_LENGTH := 500.0 +const BEAM_WIDTH := 28.0 +const PULSE_FORCE := 350.0 +const PULSE_CD := 5.0 +const HOMING_COUNT := 3 +const HOMING_SPEED := 70.0 +const HOMING_TURN := 1.5 # rad/sec + +const FREEZE_DURATION := 2.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") + +# --------------------------------------------------------------------------- +# State +# --------------------------------------------------------------------------- +var _phase: Phase = Phase.ASSEMBLY +var _room_center: Vector2 + +# Assembly +var _wave_index: int = 0 +var _wave_timer: float = 1.5 +var _kills: int = 0 +var _assembly_parts: Array[Node2D] = [] +var _spawned_enemies: Array[Node] = [] + +# Core visual +var _core_visual: Node2D = null +var _core_poly: Polygon2D = null + +# Phase 2/3 attacks +var _attack_timer: float = 2.0 +var _attack_index: int = 0 +var _projectiles: Array[Dictionary] = [] # {node, vel, type, ...} + +# Beam state +var _beam_active: bool = false +var _beam_angle: float = 0.0 +var _beam_timer: float = 0.0 +var _beam_node: Node2D = null + +# Pulse state +var _pulse_timer: float = 3.0 + +# Climax +var _climax_timer: float = 0.0 + +# --------------------------------------------------------------------------- +# Lifecycle +# --------------------------------------------------------------------------- +func _ready() -> void: + super._ready() + max_health = 350.0 + current_health = max_health + contact_damage = 1.0 + contact_cooldown = 0.8 + + _room_center = global_position + _build_core_visual() + _core_visual.visible = false # hidden during assembly + + +func _physics_process(delta: float) -> void: + _tick_spawn_delay(delta) + if not is_active(): + return + + match _phase: + Phase.ASSEMBLY: + _tick_assembly(delta) + Phase.ACTIVE: + _tick_active(delta) + Phase.UNIQUE: + _tick_unique(delta) + Phase.CLIMAX: + _tick_climax(delta) + + _tick_projectiles(delta) + _tick_flash(delta) + velocity = Vector2.ZERO + move_and_slide() + + +# --------------------------------------------------------------------------- +# Core Visual +# --------------------------------------------------------------------------- +func _build_core_visual() -> void: + _core_visual = Node2D.new() + _core_visual.position = Vector2.ZERO + + # Main body — irregular polygon + _core_poly = Polygon2D.new() + _core_poly.polygon = PackedVector2Array([ + Vector2(-35, -25), Vector2(-15, -40), Vector2(20, -35), + Vector2(40, -10), Vector2(35, 20), Vector2(10, 40), + Vector2(-25, 30), Vector2(-40, 5) + ]) + _core_poly.color = Color(0.25, 0.15, 0.35) + _core_visual.add_child(_core_poly) + + # Eye/core indicator + var eye := Polygon2D.new() + var pts := PackedVector2Array() + for i in 8: + var a := TAU * i / 8.0 + pts.append(Vector2(cos(a), sin(a)) * 12.0) + eye.polygon = pts + eye.color = Color(0.8, 0.2, 0.5) + _core_visual.add_child(eye) + + add_child(_core_visual) + + +# --------------------------------------------------------------------------- +# Phase 1 — Assembly +# --------------------------------------------------------------------------- +func _tick_assembly(delta: float) -> void: + _wave_timer -= delta + if _wave_timer <= 0.0 and _wave_index < WAVE_COUNT: + _spawn_wave() + _wave_index += 1 + _wave_timer = WAVE_DELAY + randf_range(0.0, 1.0) + + # Check if assembly complete + if _kills >= ASSEMBLY_COMPLETE: + _enter_active() + + +func _spawn_wave() -> void: + var scenes := [DRIFTER_SCENE, REPEATER_SCENE, ANCHOR_SCENE] + var hw := ROOM_HW - WALL_T - 80.0 + var hh := ROOM_HH - WALL_T - 80.0 + + for i in ENEMIES_PER_WAVE: + var scene = scenes[randi() % scenes.size()] + var enemy = scene.instantiate() + var pos := Vector2( + randf_range(-hw, hw), + randf_range(-hh, hh) + ) + # Avoid spawning too close to center + if pos.length() < 100.0: + pos = pos.normalized() * 100.0 + get_parent().add_child(enemy) + enemy.global_position = _room_center + pos + + # Track enemy for assembly phase + _spawned_enemies.append(enemy) + enemy.tree_exited.connect(_on_enemy_died.bind(enemy), CONNECT_ONE_SHOT) + + +func _on_enemy_died(enemy: Node) -> void: + if _phase != Phase.ASSEMBLY: + return + + # Remove from tracked list + _spawned_enemies.erase(enemy) + _kills += 1 + + # Create visual part flying to center (use last known position offset) + var part := Polygon2D.new() + var pts := PackedVector2Array() + for i in 6: + var a := TAU * i / 6.0 + pts.append(Vector2(cos(a), sin(a)) * randf_range(8.0, 14.0)) + part.polygon = pts + part.color = Color(0.4, 0.2, 0.5, 0.8) + # Position near center since enemy is already freed by now + part.global_position = _room_center + Vector2(randf_range(-100, 100), randf_range(-100, 100)) + get_parent().add_child(part) + _assembly_parts.append(part) + + # Tween to center + var tw := get_tree().create_tween() + tw.tween_property(part, "global_position", _room_center, 0.8).set_ease(Tween.EASE_IN) + tw.tween_property(part, "modulate:a", 0.0, 0.3) + tw.tween_callback(part.queue_free) + + +func _enter_active() -> void: + _phase = Phase.ACTIVE + _core_visual.visible = true + _attack_timer = 1.5 + _attack_index = 0 + + # Clean up any remaining assembly parts + for part in _assembly_parts: + if is_instance_valid(part): + part.queue_free() + _assembly_parts.clear() + + # Emit boss health bar start + EventBus.boss_health_changed.emit(current_health, max_health) + + +# --------------------------------------------------------------------------- +# Phase 2 — Active (mimic attacks) +# --------------------------------------------------------------------------- +func _tick_active(delta: float) -> void: + _find_player() + _tick_contact(delta) + + _attack_timer -= delta + if _attack_timer <= 0.0: + _do_active_attack() + _attack_index = (_attack_index + 1) % 3 + _attack_timer = ATTACK_CD_P2 + + +func _do_active_attack() -> void: + match _attack_index: + 0: _attack_spread() + 1: _attack_sweep() + 2: _attack_drift() + + +func _attack_spread() -> void: + # Repeater-style spread shot toward player + if _player == null: + return + + var base_dir := ((_player.global_position) - global_position).normalized() + var start_angle := base_dir.angle() - SPREAD_ANGLE * 0.5 + var step := SPREAD_ANGLE / float(SPREAD_COUNT - 1) if SPREAD_COUNT > 1 else 0.0 + + for i in SPREAD_COUNT: + var angle := start_angle + step * i + var dir := Vector2.from_angle(angle) + _spawn_projectile(global_position, dir * SPREAD_SPEED, "spread") + + +func _attack_sweep() -> void: + # Warden-style horizontal sweep with gaps + var inner_left := -ROOM_HW + WALL_T + var inner_right := ROOM_HW - WALL_T + var sweep_dir := 1.0 if randf() < 0.5 else -1.0 + var start_y := _room_center.y + (-ROOM_HH + WALL_T) * -sweep_dir + var vel := Vector2(0.0, SWEEP_SPEED * sweep_dir) + + # One gap + var gap_center := randf_range(inner_left + 80.0, inner_right - 80.0) + var gap_width := 90.0 + + var x := inner_left + while x <= inner_right: + if abs(x - gap_center) > gap_width * 0.5: + _spawn_projectile(Vector2(_room_center.x + x, start_y), vel, "sweep") + x += SWEEP_PROJ_SPACE + + +func _attack_drift() -> void: + # Drifter-style curving projectiles + for i in DRIFT_COUNT: + var angle := TAU * i / float(DRIFT_COUNT) + randf_range(-0.2, 0.2) + var dir := Vector2.from_angle(angle) + var curve := DRIFT_CURVE if randf() < 0.5 else -DRIFT_CURVE + _spawn_projectile(global_position, dir * DRIFT_SPEED, "drift", {"curve": curve}) + + +# --------------------------------------------------------------------------- +# Phase 3 — Unique attacks +# --------------------------------------------------------------------------- +func _enter_unique() -> void: + _phase = Phase.UNIQUE + _attack_timer = 1.0 + _attack_index = 0 + _pulse_timer = PULSE_CD * 0.5 + + # Visual change — cracking/glowing + _core_poly.color = Color(0.5, 0.15, 0.25) + + +func _tick_unique(delta: float) -> void: + _find_player() + _tick_contact(delta) + + # Rotating beam + if _beam_active: + _tick_beam(delta) + else: + _attack_timer -= delta + if _attack_timer <= 0.0: + _do_unique_attack() + _attack_index = (_attack_index + 1) % 3 + _attack_timer = ATTACK_CD_P3 + + # Pulse on separate timer + _pulse_timer -= delta + if _pulse_timer <= 0.0: + _attack_pulse() + _pulse_timer = PULSE_CD + + +func _do_unique_attack() -> void: + match _attack_index: + 0: _start_beam() + 1: _attack_homing() + 2: _attack_homing() # double up on homing since beam takes time + + +func _start_beam() -> void: + _beam_active = true + _beam_timer = BEAM_DURATION + _beam_angle = randf() * TAU + + _beam_node = Node2D.new() + _beam_node.position = Vector2.ZERO + + var poly := Polygon2D.new() + var hw := BEAM_WIDTH * 0.5 + poly.polygon = PackedVector2Array([ + Vector2(0, -hw), Vector2(BEAM_LENGTH, -hw), + Vector2(BEAM_LENGTH, hw), Vector2(0, hw) + ]) + poly.color = Color(1.0, 0.3, 0.4, 0.7) + _beam_node.add_child(poly) + + var area := Area2D.new() + area.collision_layer = 0 + area.collision_mask = 2 + var cs := CollisionShape2D.new() + var rs := RectangleShape2D.new() + rs.size = Vector2(BEAM_LENGTH, BEAM_WIDTH) + cs.shape = rs + cs.position = Vector2(BEAM_LENGTH * 0.5, 0) + area.add_child(cs) + area.body_entered.connect(_on_beam_hit) + _beam_node.add_child(area) + + add_child(_beam_node) + + +func _tick_beam(delta: float) -> void: + _beam_timer -= delta + _beam_angle += BEAM_ROT_SPEED * delta + if _beam_node != null: + _beam_node.rotation = _beam_angle + + if _beam_timer <= 0.0: + _end_beam() + + +func _end_beam() -> void: + _beam_active = false + if _beam_node != null: + _beam_node.queue_free() + _beam_node = null + + +func _on_beam_hit(body: Node) -> void: + if body.is_in_group("player") and body.has_method("take_damage"): + body.take_damage(1.0) + + +func _attack_pulse() -> void: + # Push player toward walls + if _player == null or not is_instance_valid(_player): + return + + var dir := (_player.global_position - global_position).normalized() + if _player.has_method("apply_knockback"): + _player.apply_knockback(dir * PULSE_FORCE) + + # Visual pulse ring + var ring := Polygon2D.new() + var pts := PackedVector2Array() + for i in 24: + var a := TAU * i / 24.0 + pts.append(Vector2(cos(a), sin(a)) * 50.0) + ring.polygon = pts + ring.color = Color(0.8, 0.3, 0.5, 0.5) + ring.global_position = global_position + get_parent().add_child(ring) + + var tw := get_tree().create_tween() + tw.tween_property(ring, "scale", Vector2(8.0, 8.0), 0.4) + tw.parallel().tween_property(ring, "modulate:a", 0.0, 0.4) + tw.tween_callback(ring.queue_free) + + +func _attack_homing() -> void: + for i in HOMING_COUNT: + var angle := TAU * i / float(HOMING_COUNT) + randf_range(-0.3, 0.3) + var dir := Vector2.from_angle(angle) + _spawn_projectile(global_position + dir * 30.0, dir * HOMING_SPEED, "homing") + + +# --------------------------------------------------------------------------- +# Projectile system +# --------------------------------------------------------------------------- +func _spawn_projectile(pos: Vector2, vel: Vector2, type: String, extra: Dictionary = {}) -> void: + var proj := Node2D.new() + proj.global_position = pos + + var poly := Polygon2D.new() + var size := 10.0 if type != "sweep" else 14.0 + var hs := size * 0.5 + poly.polygon = PackedVector2Array([ + Vector2(-hs, -hs), Vector2(hs, -hs), + Vector2(hs, hs), Vector2(-hs, hs) + ]) + + match type: + "spread": poly.color = Color(0.9, 0.5, 0.2) + "sweep": poly.color = Color(0.7, 0.3, 0.5) + "drift": poly.color = Color(0.5, 0.4, 0.8) + "homing": poly.color = Color(1.0, 0.2, 0.4) + + proj.add_child(poly) + + var area := Area2D.new() + area.collision_layer = 0 + area.collision_mask = 2 + var cs := CollisionShape2D.new() + var circle := CircleShape2D.new() + circle.radius = hs + cs.shape = circle + area.add_child(cs) + area.body_entered.connect(_on_proj_hit.bind(proj)) + proj.add_child(area) + + get_parent().add_child(proj) + + var data := {"node": proj, "vel": vel, "type": type} + data.merge(extra) + _projectiles.append(data) + + +func _on_proj_hit(body: Node, proj: Node2D) -> void: + if body.is_in_group("player") and body.has_method("take_damage"): + body.take_damage(1.0) + if is_instance_valid(proj): + proj.queue_free() + + +func _tick_projectiles(delta: float) -> void: + var still_valid: Array[Dictionary] = [] + + for p in _projectiles: + var node: Node2D = p["node"] + if not is_instance_valid(node): + continue + + var vel: Vector2 = p["vel"] + + # Apply curve for drift projectiles + if p["type"] == "drift" and p.has("curve"): + var curve_rate: float = p["curve"] + vel = vel.rotated(curve_rate * delta) + p["vel"] = vel + + # Homing behavior + if p["type"] == "homing" and _player != null and is_instance_valid(_player): + var to_player := (_player.global_position - node.global_position).normalized() + var current_dir := vel.normalized() + var new_dir := current_dir.rotated( + clampf(current_dir.angle_to(to_player), -HOMING_TURN * delta, HOMING_TURN * delta) + ) + vel = new_dir * vel.length() + p["vel"] = vel + + node.global_position += vel * delta + + # Bounds check + var rel := node.global_position - _room_center + if absf(rel.x) > ROOM_HW + 20.0 or absf(rel.y) > ROOM_HH + 20.0: + node.queue_free() + else: + still_valid.append(p) + + _projectiles = still_valid + + +func _clear_projectiles() -> void: + for p in _projectiles: + if is_instance_valid(p["node"]): + p["node"].queue_free() + _projectiles.clear() + + +# --------------------------------------------------------------------------- +# Damage / Phase transitions +# --------------------------------------------------------------------------- +func take_damage(amount: float) -> void: + if _phase == Phase.ASSEMBLY or _phase == Phase.CLIMAX: + return # invulnerable during assembly and climax + + current_health -= amount + _flash_timer = 0.1 + EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health) + + if current_health <= 0.0: + _enter_climax() + return + + if _phase == Phase.ACTIVE and current_health <= max_health * 0.3: + _enter_unique() + + +func _tick_flash(delta: float) -> void: + if _flash_timer > 0.0: + _flash_timer -= delta + _core_poly.color = Color(2.5, 2.5, 2.5) + else: + match _phase: + Phase.ACTIVE: + _core_poly.color = Color(0.25, 0.15, 0.35) + Phase.UNIQUE: + _core_poly.color = Color(0.5, 0.15, 0.25) + + +# --------------------------------------------------------------------------- +# Climax / Death +# --------------------------------------------------------------------------- +func _enter_climax() -> void: + _phase = Phase.CLIMAX + _climax_timer = FREEZE_DURATION + velocity = Vector2.ZERO + _end_beam() + + +func _tick_climax(delta: float) -> void: + _climax_timer -= delta + var blink := fmod(_climax_timer, 0.25) < 0.125 + _core_poly.color = Color(2.0, 0.3, 0.3) if blink else Color(0.2, 0.05, 0.08) + + if _climax_timer <= 0.0: + _die() + + +func _die() -> void: + _clear_projectiles() + _end_beam() + EventBus.boss_died.emit() + queue_free() diff --git a/scripts/enemies/relay.gd b/scripts/enemies/relay.gd index 8cfffa7..768a9a9 100644 --- a/scripts/enemies/relay.gd +++ b/scripts/enemies/relay.gd @@ -50,9 +50,8 @@ var _visual : Node2D var _contact_area : Area2D var _teleport_timer : float = 3.0 # initial delay before first teleport -var _in_transit : bool = false var _transit_timer : float = 0.0 -var _fade_out : bool = false +var _fade_timer : float = 0.0 var _teleport_count : int = 0 var _next_drifter_threshold : int = 0 @@ -94,35 +93,37 @@ func _physics_process(delta: float) -> void: move_and_slide() # --------------------------------------------------------------------------- -# Teleport cycle +# Teleport cycle (fully timer-based, no tween callbacks) # --------------------------------------------------------------------------- +enum TeleportState { IDLE, FADING_OUT, IN_TRANSIT } +var _tp_state: int = TeleportState.IDLE + func _tick_teleport(delta: float) -> void: - if _in_transit: - _transit_timer -= delta - if _transit_timer <= 0.0: - _reappear() - return + match _tp_state: + TeleportState.IDLE: + _teleport_timer -= delta + if _teleport_timer <= 0.0: + _start_fade_out() + TeleportState.FADING_OUT: + _fade_timer -= delta + _visual.modulate.a = _fade_timer / FADE_OUT_TIME + if _fade_timer <= 0.0: + _enter_transit() + TeleportState.IN_TRANSIT: + _transit_timer -= delta + if _transit_timer <= 0.0: + _reappear() - _teleport_timer -= delta - if _teleport_timer <= 0.0: - _start_teleport() +func _start_fade_out() -> void: + _tp_state = TeleportState.FADING_OUT + _fade_timer = FADE_OUT_TIME -func _start_teleport() -> void: - _in_transit = true - _fade_out = true - - # Quick fade out - var tw := create_tween() - tw.tween_property(_visual, "modulate:a", 0.0, FADE_OUT_TIME) - tw.finished.connect(_on_fade_finished) - -func _on_fade_finished() -> void: - if not _fade_out: - return - _fade_out = false +func _enter_transit() -> void: + _tp_state = TeleportState.IN_TRANSIT _contact_area.monitoring = false _contact_area.monitorable = false _visual.visible = false + _visual.modulate.a = 0.0 _transit_timer = TRANSIT_TIME func _reappear() -> void: @@ -147,7 +148,7 @@ func _reappear() -> void: var base_color := Color(0.3, 0.8, 1.0) if _phase == Phase.TWO else Color(1, 1, 1, 1) tw.tween_property(_visual, "modulate", base_color, 0.15) - _in_transit = false + _tp_state = TeleportState.IDLE _teleport_count += 1 # Fire burst @@ -255,7 +256,7 @@ func _spawn_drifters() -> void: # Damage / Phase # --------------------------------------------------------------------------- func take_damage(amount: float) -> void: - if _in_transit or _phase == Phase.CLIMAX: + if _tp_state != TeleportState.IDLE or _phase == Phase.CLIMAX: return current_health -= amount diff --git a/scripts/hub/hub_room.gd b/scripts/hub/hub_room.gd index 49edad6..77e2ba8 100644 --- a/scripts/hub/hub_room.gd +++ b/scripts/hub/hub_room.gd @@ -491,6 +491,19 @@ func _reset_practice() -> void: _practice_boss.queue_free() _practice_boss = null + # Clear all enemies (drifters spawned by Relay, etc.) + for enemy in get_tree().get_nodes_in_group("enemies"): + if is_instance_valid(enemy): + enemy.queue_free() + + # Clear any projectiles/nodes in contents that aren't permanent + for child in contents.get_children(): + # Keep labels, polygons, and areas (the UI elements) + if child is Label or child is Polygon2D or child is Area2D: + continue + # Remove everything else (projectiles, spawned enemies, etc.) + child.queue_free() + _practice_active = false _reset_prompt.visible = false diff --git a/scripts/rooms/room.gd b/scripts/rooms/room.gd index 77af538..e94d617 100644 --- a/scripts/rooms/room.gd +++ b/scripts/rooms/room.gd @@ -21,6 +21,7 @@ const ANCHOR_SCENE = preload("res://scenes/enemies/anchor.tscn") const SUPERVISOR_SCENE = preload("res://scenes/enemies/supervisor.tscn") const WARDEN_SCENE = preload("res://scenes/enemies/warden.tscn") const RELAY_SCENE = preload("res://scenes/enemies/relay.tscn") +const HIVEMIND_SCENE = preload("res://scenes/enemies/hivemind.tscn") const BODY_PART_PICKUP = preload("res://scenes/upgrades/body_part_pickup.tscn") const RUN_ITEM_PICKUP = preload("res://scenes/items/run_item_pickup.tscn") const SHOP_PEDESTAL_SCR = preload("res://scripts/items/shop_item_pedestal.gd") @@ -267,7 +268,7 @@ func _spawn_boss() -> void: match RunManager.current_floor: 1: scene = WARDEN_SCENE 2: scene = RELAY_SCENE - _: scene = SUPERVISOR_SCENE + _: scene = HIVEMIND_SCENE var boss = scene.instantiate() contents.add_child(boss) boss.global_position = to_global(Vector2.ZERO) # room centre