From 20f1d1789b9f3c87c4842be23ced2564bdd8b319 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Wed, 11 Mar 2026 16:15:52 -0400 Subject: [PATCH] Small update fixes --- scripts/enemies/hivemind.gd | 25 ++++++++++++++++++----- scripts/hub/hub_room.gd | 25 ++++++++++------------- scripts/player/player.gd | 17 ++++++++++++++-- scripts/rooms/room.gd | 37 +++++++++++++++++++++++------------ scripts/rooms/station_door.gd | 13 +++++++++++- 5 files changed, 81 insertions(+), 36 deletions(-) diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd index d10d6c5..3e7813c 100644 --- a/scripts/enemies/hivemind.gd +++ b/scripts/enemies/hivemind.gd @@ -50,6 +50,7 @@ const BEAM_LENGTH := 500.0 const BEAM_WIDTH := 28.0 const PULSE_FORCE := 350.0 const PULSE_CD := 5.0 +const PULSE_RADIUS := 200.0 const HOMING_COUNT := 4 const HOMING_SPEED := 120.0 const HOMING_TURN := 1.8 # rad/sec @@ -747,9 +748,12 @@ func _attack_pulse() -> void: 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) + # Only apply knockback if player is within pulse radius + var dist := global_position.distance_to(_player.global_position) + if dist <= PULSE_RADIUS: + 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() @@ -812,13 +816,24 @@ func _on_homing_hit(body: Node, proj: Node2D) -> void: func _tick_homing_projs(delta: float) -> void: if _player == null or not is_instance_valid(_player): + # Player died or invalid — clear all homing projectiles + for p in _homing_projs: + var raw_node = p.get("node") + if raw_node != null and is_instance_valid(raw_node): + raw_node.queue_free() + _homing_projs.clear() return var still_valid: Array[Dictionary] = [] for p in _homing_projs: - var node: Node2D = p["node"] - if not is_instance_valid(node): + # Check validity on raw value before typed assignment to avoid error + var raw_node = p.get("node") + if raw_node == null or not is_instance_valid(raw_node): + continue + + var node := raw_node as Node2D + if node == null: continue # Age check diff --git a/scripts/hub/hub_room.gd b/scripts/hub/hub_room.gd index d2b8e45..a7f9696 100644 --- a/scripts/hub/hub_room.gd +++ b/scripts/hub/hub_room.gd @@ -24,6 +24,7 @@ const DOOR_W := 80.0 const DOOR_H := 80.0 const WALL_COLOR := Color(0.18, 0.18, 0.25) +const STATION_DOOR_SCRIPT = preload("res://scripts/rooms/station_door.gd") const ROOM_NAMES := [ "STAGING AREA", # 0 @@ -272,33 +273,27 @@ func _build_armory_workbench() -> void: # Staging Area run portal # --------------------------------------------------------------------------- func _build_run_portal() -> void: - # Visual — bright green rectangle - var poly := Polygon2D.new() - poly.polygon = PackedVector2Array([-40, -30, 40, -30, 40, 30, -40, 30]) - poly.color = Color(0.2, 0.9, 0.4) - poly.position = Vector2(0, -150) - contents.add_child(poly) - - # Label above portal - var lbl := Label.new() - lbl.text = "START RUN" - lbl.position = Vector2(-36, -195) - contents.add_child(lbl) + # Station door with proper frame graphics + var portal_door := Node2D.new() + portal_door.set_script(STATION_DOOR_SCRIPT) + portal_door.position = Vector2(0, -150) + contents.add_child(portal_door) + portal_door.setup_hub_portal() _portal_prompt = Label.new() _portal_prompt.text = "[E] Start Run" - _portal_prompt.position = Vector2(-50, -210) + _portal_prompt.position = Vector2(-50, -85) # below the door label _portal_prompt.visible = false contents.add_child(_portal_prompt) - # Trigger zone + # Trigger zone for interaction var area := Area2D.new() area.collision_layer = 0 area.collision_mask = 2 var cs := CollisionShape2D.new() var rs := RectangleShape2D.new() - rs.size = Vector2(80, 60) + rs.size = Vector2(80, 100) cs.shape = rs area.add_child(cs) area.position = Vector2(0, -150) diff --git a/scripts/player/player.gd b/scripts/player/player.gd index 74be5d2..2334b15 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -31,6 +31,7 @@ var _slick_count: int = 0 # Knockback — applied by enemies (e.g., Hivemind pulse) var _knockback_velocity: Vector2 = Vector2.ZERO +var _knockback_flash_timer: float = 0.0 func _ready() -> void: add_to_group("player") @@ -62,8 +63,9 @@ func _handle_movement() -> void: input_dir = input_dir.normalized() if _slick_count > 0: - # Oil slick — momentum-based: velocity slowly drifts toward desired direction - velocity = velocity.lerp(input_dir * stats.speed, 0.08) + # Oil slick — 40% slower + momentum-based movement + var slick_speed := stats.speed * 0.6 + velocity = velocity.lerp(input_dir * slick_speed, 0.12) else: velocity = input_dir * stats.speed @@ -168,6 +170,12 @@ func _handle_iframes(delta: float) -> void: body.modulate = Color(1, 1, 1, 1) return + # Knockback flash — magenta pulse when pushed by enemy ability + if _knockback_flash_timer > 0.0: + _knockback_flash_timer -= delta + body.modulate = Color(1.0, 0.4, 0.7, 1.0) + return + # Shield active — cyan tint if _shield_active: body.modulate = Color(0.4, 0.9, 1.0) @@ -216,6 +224,10 @@ func take_damage(amount: float) -> void: func _die() -> void: if RunManager.run_active: + # Disable player so they can't move during death screen + set_physics_process(false) + set_process_input(false) + visible = false RunManager.end_run(false) else: # In hub/practice — signal for practice room handling, then heal @@ -277,6 +289,7 @@ func exit_slick() -> void: # --------------------------------------------------------------------------- func apply_knockback(force: Vector2) -> void: _knockback_velocity = force + _knockback_flash_timer = 0.25 # brief magenta flash to show player was pushed # --------------------------------------------------------------------------- # Room event handlers diff --git a/scripts/rooms/room.gd b/scripts/rooms/room.gd index 103aa9f..4758467 100644 --- a/scripts/rooms/room.gd +++ b/scripts/rooms/room.gd @@ -324,6 +324,9 @@ func _spawn_boss() -> void: _enemy_death_connections.append({"node": boss, "signal": "tree_exited", "callable": callable}) _enemy_count = 1 + # Spawn exit door on top wall (visible but closed until boss defeated) + _spawn_floor_exit_door() + func _on_enemy_died() -> void: # Guard against deferred callback running after room is freed @@ -335,30 +338,34 @@ func _on_enemy_died() -> void: AudioManager.play("room_clear") if data.type == RoomData.RoomType.BOSS: _spawn_body_part_drop() - _spawn_floor_exit() + _open_floor_exit() room_cleared.emit() EventBus.room_cleared.emit(data.id) _update_door_locks() -func _spawn_floor_exit() -> void: +func _spawn_floor_exit_door() -> void: + # Create the exit door on the top wall (closed until boss defeated) var is_final_floor := RunManager.current_floor >= 3 - # Station door for floor transition - positioned to the right of body part drop _floor_exit_door = StationDoor.new() - _floor_exit_door.position = Vector2(150, 0) + # Position on top wall, centered + _floor_exit_door.position = Vector2(0, -ROOM_H / 2.0 + 80) if is_final_floor: _floor_exit_door.setup_evacuation_exit() else: - # Next sector: floor 1 -> SECTOR 2, floor 2 -> SECTOR 3 _floor_exit_door.setup_sector_exit(RunManager.current_floor + 1) _floor_exit_door.door_entered.connect(_on_floor_exit_entered) contents.add_child(_floor_exit_door) + # Door starts closed (setup functions already call close()) + +func _open_floor_exit() -> void: # Open the door now that boss is dead - _floor_exit_door.open() + if _floor_exit_door != null: + _floor_exit_door.open() func _on_floor_exit_entered(body: Node) -> void: @@ -426,20 +433,24 @@ func _make_oil_slick(pos: Vector2) -> void: slick.collision_mask = 2 | 8 # player (2) + enemies (8) slick.position = pos - var radius: float = randf_range(44.0, 68.0) + # Oval dimensions — wider than tall + var radius_x: float = randf_range(55.0, 85.0) + var radius_y: float = randf_range(35.0, 50.0) + + # Use rectangle shape for collision (approximates oval) var cs := CollisionShape2D.new() - var sh := CircleShape2D.new() - sh.radius = radius - cs.shape = sh + var sh := RectangleShape2D.new() + sh.size = Vector2(radius_x * 1.6, radius_y * 1.6) # slightly smaller than visual + cs.shape = sh slick.add_child(cs) - # Visual — circle matching collision shape + # Visual — oval shape var poly := Polygon2D.new() var pts := PackedVector2Array() - var segs := 14 + var segs := 16 for i in segs: var a := i * TAU / segs - pts.append(Vector2(cos(a) * radius, sin(a) * radius)) + pts.append(Vector2(cos(a) * radius_x, sin(a) * radius_y)) poly.polygon = pts poly.color = Color(0.04, 0.07, 0.14, 0.78) slick.add_child(poly) diff --git a/scripts/rooms/station_door.gd b/scripts/rooms/station_door.gd index d049ff3..1cca9b8 100644 --- a/scripts/rooms/station_door.gd +++ b/scripts/rooms/station_door.gd @@ -26,7 +26,7 @@ const EXIT_LIGHT := Color(0.2, 0.95, 0.4) # bright green exit # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- -enum DoorType { ENTRY, SECTOR_EXIT, EVACUATION_EXIT } +enum DoorType { ENTRY, SECTOR_EXIT, EVACUATION_EXIT, HUB_PORTAL } var door_type: DoorType = DoorType.ENTRY var sector_label: String = "" # "SECTOR 2", "SECTOR 3", or "EXIT" @@ -78,6 +78,17 @@ func setup_evacuation_exit() -> void: _build_trigger() close() # starts closed until boss dies +func setup_hub_portal() -> void: + # Hub staging room portal - decorative open door with "START RUN" label + door_type = DoorType.HUB_PORTAL + sector_label = "START RUN" + is_functional = false # interaction handled by hub_room.gd + _build_frame() + _build_panels() + _build_indicator() + _build_label() + open() # always open in hub + # --------------------------------------------------------------------------- # Build components # ---------------------------------------------------------------------------