From 3684517d1bd18164e9a6b212cd1c9d79fa3137c1 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Mon, 9 Mar 2026 21:36:35 -0400 Subject: [PATCH] Small overhaul --- scripts/enemies/hivemind.gd | 50 +++++++++----- scripts/enemies/relay.gd | 4 +- scripts/enemies/supervisor.gd | 2 +- scripts/enemies/warden.gd | 2 +- scripts/event_bus.gd | 7 +- scripts/hub/hub_room.gd | 116 +++++++++++++++++++++++++++++++++ scripts/player/player.gd | 3 +- scripts/player/player_stats.gd | 8 ++- scripts/ui/hud.gd | 19 ++++-- 9 files changed, 182 insertions(+), 29 deletions(-) diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd index 8b98aaa..5dfb433 100644 --- a/scripts/enemies/hivemind.gd +++ b/scripts/enemies/hivemind.gd @@ -55,7 +55,7 @@ const PULSE_CD := 5.0 const HOMING_COUNT := 4 const HOMING_SPEED := 80.0 const HOMING_TURN := 1.8 # rad/sec -const HOMING_LIFETIME := 4.0 # seconds before homing projectiles expire +const HOMING_LIFETIME := 7.0 # seconds before homing projectiles expire const FREEZE_DURATION := 2.0 @@ -80,6 +80,7 @@ var _wave_spawned: bool = false # has current wave been spawned? var _core_visual: Node2D = null var _core_poly: Polygon2D = null var _contact_area: Area2D = null +var _collision_shape: CollisionShape2D = null # Teleport state enum TeleportState { IDLE, FADING_OUT, IN_TRANSIT } @@ -128,14 +129,23 @@ func _ready() -> void: contact_cooldown = 0.8 _room_center = global_position + + # Get scene collision nodes + _contact_area = $ContactArea + _collision_shape = $Collision + _build_core_visual() _core_visual.visible = false # hidden during assembly _core_visual.scale = Vector2(_core_scale, _core_scale) - # Disable contact during assembly + # Disable and scale down collision during assembly if _contact_area: _contact_area.monitoring = false _contact_area.monitorable = false + _contact_area.scale = Vector2(_core_scale, _core_scale) + if _collision_shape: + _collision_shape.disabled = true + _collision_shape.scale = Vector2(_core_scale, _core_scale) func _physics_process(delta: float) -> void: @@ -188,20 +198,12 @@ func _build_core_visual() -> void: eye.color = Color(0.8, 0.2, 0.5) _core_visual.add_child(eye) - # Contact area for damage - _contact_area = Area2D.new() - _contact_area.collision_layer = 0 - _contact_area.collision_mask = 2 - var cs := CollisionShape2D.new() - var circle := CircleShape2D.new() - circle.radius = 40.0 - cs.shape = circle - _contact_area.add_child(cs) - _contact_area.body_entered.connect(_on_contact_hit) - _core_visual.add_child(_contact_area) - add_child(_core_visual) + # Connect scene's contact area to damage handler + if _contact_area: + _contact_area.body_entered.connect(_on_contact_hit) + func _on_contact_hit(body: Node) -> void: if _phase == Phase.ASSEMBLY: @@ -296,16 +298,26 @@ func _grow_core() -> void: _core_visual.visible = true _core_visual.scale = Vector2(_core_scale, _core_scale) + # Scale collision shapes to match visual + if _contact_area: + _contact_area.scale = Vector2(_core_scale, _core_scale) + if _collision_shape: + _collision_shape.scale = Vector2(_core_scale, _core_scale) + func _enter_active() -> void: _phase = Phase.ACTIVE _core_visual.visible = true _core_visual.scale = Vector2(1.0, 1.0) - # Enable contact + # Enable and scale up collision if _contact_area: _contact_area.monitoring = true _contact_area.monitorable = true + _contact_area.scale = Vector2(1.0, 1.0) + if _collision_shape: + _collision_shape.disabled = false + _collision_shape.scale = Vector2(1.0, 1.0) _teleport_timer = 2.0 _sweep_timer = 3.0 @@ -315,7 +327,7 @@ func _enter_active() -> void: _spawn_orbiters() # Emit boss health bar start - EventBus.boss_health_changed.emit(current_health, max_health) + EventBus.boss_health_changed.emit(current_health, max_health, "THE HIVEMIND") # --------------------------------------------------------------------------- @@ -362,6 +374,8 @@ func _enter_transit() -> void: _core_visual.modulate.a = 0.0 if _contact_area: _contact_area.monitoring = false + if _collision_shape: + _collision_shape.disabled = true _transit_timer = TRANSIT_TIME @@ -382,6 +396,8 @@ func _reappear() -> void: _core_visual.modulate = Color(2.0, 2.0, 2.0, 1.0) if _contact_area: _contact_area.monitoring = true + if _collision_shape: + _collision_shape.disabled = false var tw := create_tween() var base_color := Color(0.5, 0.15, 0.25) if _phase == Phase.UNIQUE else Color(1, 1, 1, 1) @@ -874,7 +890,7 @@ func take_damage(amount: float) -> void: current_health -= amount _flash_timer = 0.1 - EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health) + EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE HIVEMIND") if current_health <= 0.0: _enter_climax() diff --git a/scripts/enemies/relay.gd b/scripts/enemies/relay.gd index f172565..418fffc 100644 --- a/scripts/enemies/relay.gd +++ b/scripts/enemies/relay.gd @@ -33,7 +33,7 @@ const HOMING_COUNT_P2 := 6 const HOMING_SPEED := 100.0 const HOMING_TURN_SPEED := 1.8 # radians per second const HOMING_DELAY := 0.3 # delay after radial burst before homing fires -const HOMING_LIFETIME := 4.0 # seconds before homing projectiles expire +const HOMING_LIFETIME := 7.0 # seconds before homing projectiles expire const DRIFTER_SPAWN_MIN := 2 const DRIFTER_SPAWN_MAX := 4 @@ -364,7 +364,7 @@ func take_damage(amount: float) -> void: return current_health -= amount - EventBus.boss_health_changed.emit(current_health, max_health) + EventBus.boss_health_changed.emit(current_health, max_health, "THE RELAY") if current_health <= 5.0 and _phase != Phase.CLIMAX: _enter_climax() diff --git a/scripts/enemies/supervisor.gd b/scripts/enemies/supervisor.gd index 090c69f..f0d5198 100644 --- a/scripts/enemies/supervisor.gd +++ b/scripts/enemies/supervisor.gd @@ -316,7 +316,7 @@ func _fire_beam() -> void: func take_damage(amount: float) -> void: current_health -= amount _flash_timer = 0.1 - EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health) + EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE SUPERVISOR") if current_health <= 0.0: _die() diff --git a/scripts/enemies/warden.gd b/scripts/enemies/warden.gd index 3c0713a..6ab1fa7 100644 --- a/scripts/enemies/warden.gd +++ b/scripts/enemies/warden.gd @@ -352,7 +352,7 @@ func take_damage(amount: float) -> void: return # already dying current_health -= amount _flash_timer = 0.1 - EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health) + EventBus.boss_health_changed.emit(maxf(current_health, 0.0), max_health, "THE WARDEN") if current_health <= 0.0: _enter_climax() diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index ee384cc..7f6d804 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -46,9 +46,14 @@ signal card_game_ended(player_won: bool) # --------------------------------------------------------------------------- # Boss # --------------------------------------------------------------------------- -signal boss_health_changed(current: float, maximum: float) +signal boss_health_changed(current: float, maximum: float, boss_name: String) signal boss_died +# --------------------------------------------------------------------------- +# Practice Room Simulation +# --------------------------------------------------------------------------- +signal practice_player_died + # --------------------------------------------------------------------------- # Perspective / View # --------------------------------------------------------------------------- diff --git a/scripts/hub/hub_room.gd b/scripts/hub/hub_room.gd index 84fc90e..d2b8e45 100644 --- a/scripts/hub/hub_room.gd +++ b/scripts/hub/hub_room.gd @@ -66,6 +66,8 @@ var _boss_prompts: Array[Label] = [] var _practice_boss: Node2D = null var _practice_active: bool = false var _reset_prompt: Label = null +var _status_popup: Label = null +var _status_timer: float = 0.0 # --------------------------------------------------------------------------- # Node refs @@ -95,6 +97,18 @@ func setup(room_type: int, connections: Dictionary, hub_ref: Node) -> void: _build_armory_workbench() elif _type == HubRoomType.PRACTICE: _build_practice_room() + # Connect simulation events + EventBus.practice_player_died.connect(_on_practice_player_died) + EventBus.boss_died.connect(_on_practice_boss_defeated) + +# --------------------------------------------------------------------------- +# Process — handle status popup timer +# --------------------------------------------------------------------------- +func _process(delta: float) -> void: + if _status_timer > 0.0: + _status_timer -= delta + if _status_timer <= 0.0 and _status_popup != null: + _status_popup.visible = false # --------------------------------------------------------------------------- # Wall generation (same logic as room.gd) @@ -409,6 +423,14 @@ func _build_practice_room() -> void: _reset_prompt.visible = false contents.add_child(_reset_prompt) + # Status popup (centered, hidden until triggered) + _status_popup = Label.new() + _status_popup.text = "" + _status_popup.position = Vector2(-100, 0) + _status_popup.visible = false + _status_popup.add_theme_font_size_override("font_size", 24) + contents.add_child(_status_popup) + func _create_boss_terminal(boss_idx: int, pos: Vector2) -> void: # Terminal visual — colored rectangle var poly := Polygon2D.new() @@ -464,6 +486,9 @@ func _spawn_practice_boss(boss_idx: int) -> void: p.visible = false _player_at_boss_terminal = -1 + # Lock doors during simulation + _set_doors_locked(true) + # Spawn the boss var scene: PackedScene match boss_idx: @@ -486,6 +511,9 @@ func _on_practice_boss_died() -> void: # Keep reset prompt visible so player can spawn another func _reset_practice() -> void: + # Show disruption message only if simulation was active + var was_active := _practice_active + # Kill current boss if alive if _practice_boss != null and is_instance_valid(_practice_boss): _practice_boss.queue_free() @@ -507,9 +535,97 @@ func _reset_practice() -> void: _practice_active = false _reset_prompt.visible = false + # Unlock doors + _set_doors_locked(false) + # Heal player + _heal_player() + + # Show disruption message if simulation was running + if was_active: + _show_status("SIMULATION DISRUPTED", Color(0.8, 0.6, 0.2)) + + +func _on_practice_boss_defeated() -> void: + if not _practice_active: + return + + _practice_active = false + _practice_boss = null + _reset_prompt.visible = false + + # Clear any spawned enemies (drifters from Relay, etc.) + for enemy in get_tree().get_nodes_in_group("enemies"): + if is_instance_valid(enemy): + enemy.queue_free() + + # Clear projectiles + for child in contents.get_children(): + if child is Label or child is Polygon2D or child is Area2D: + continue + child.queue_free() + + # Unlock doors + _set_doors_locked(false) + + # Heal player + _heal_player() + + # Show success message + _show_status("SIMULATION PASSED", Color(0.2, 0.9, 0.4)) + + +func _on_practice_player_died() -> void: + if not _practice_active: + return + + # Clear boss and enemies + if _practice_boss != null and is_instance_valid(_practice_boss): + _practice_boss.queue_free() + _practice_boss = null + + for enemy in get_tree().get_nodes_in_group("enemies"): + if is_instance_valid(enemy): + enemy.queue_free() + + # Clear projectiles + for child in contents.get_children(): + if child is Label or child is Polygon2D or child is Area2D: + continue + child.queue_free() + + _practice_active = false + _reset_prompt.visible = false + + # Unlock doors + _set_doors_locked(false) + + # Player already healed in player._die(), but ensure full health + _heal_player() + + # Show failure message + _show_status("SIMULATION FAILED", Color(0.9, 0.2, 0.2)) + + +func _set_doors_locked(locked: bool) -> void: + for area in _doors.values(): + (area as Area2D).monitoring = not locked + + +func _heal_player() -> void: var player := get_tree().get_first_node_in_group("player") if player != null: var stats = player.get_node_or_null("Stats") if stats != null and stats.has_method("heal"): stats.heal(999) + EventBus.player_health_changed.emit(stats.current_health, stats.max_health) + + +func _show_status(text: String, color: Color) -> void: + if _status_popup == null: + return + _status_popup.text = text + _status_popup.modulate = color + _status_popup.position = Vector2(-float(text.length()) * 6.0, 0) # rough centering + _status_popup.visible = true + _status_timer = 2.5 # show for 2.5 seconds diff --git a/scripts/player/player.gd b/scripts/player/player.gd index 056fcba..d823da8 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -207,7 +207,8 @@ func _die() -> void: if RunManager.run_active: RunManager.end_run(false) else: - # In hub/practice — just heal to full instead of dying + # In hub/practice — signal for practice room handling, then heal + EventBus.practice_player_died.emit() stats.heal(stats.max_health) # --------------------------------------------------------------------------- diff --git a/scripts/player/player_stats.gd b/scripts/player/player_stats.gd index 12864a1..6c189f7 100644 --- a/scripts/player/player_stats.gd +++ b/scripts/player/player_stats.gd @@ -143,4 +143,10 @@ func heal(amount: float) -> void: current_health = minf(current_health + amount, max_health) func _on_part_equipped(_part: Resource, _slot: String) -> void: - recalculate() \ No newline at end of file + var old_max := max_health + recalculate() + # If max_health increased, grant the extra HP immediately + if max_health > old_max: + current_health += (max_health - old_max) + # Update UI + EventBus.player_health_changed.emit(current_health, max_health) \ No newline at end of file diff --git a/scripts/ui/hud.gd b/scripts/ui/hud.gd index 98297fc..cd62421 100644 --- a/scripts/ui/hud.gd +++ b/scripts/ui/hud.gd @@ -31,7 +31,7 @@ const ORIGIN := Vector2(32, 32) # center of first heart (screen px) # --------------------------------------------------------------------------- const BOSS_BAR_W := 400.0 const BOSS_BAR_H := 18.0 -const BOSS_BAR_POS := Vector2(280.0, 490.0) # top-left corner (960×540 viewport) +const BOSS_BAR_POS := Vector2(280.0, 510.0) # bottom center (960×540 viewport) # --------------------------------------------------------------------------- # State @@ -141,9 +141,16 @@ func _on_health_changed(current_hp: float, max_hp: float) -> void: # --------------------------------------------------------------------------- # Boss health bar # --------------------------------------------------------------------------- -func _on_boss_health_changed(current_hp: float, max_hp: float) -> void: +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) + _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) @@ -153,9 +160,11 @@ func _on_boss_died() -> void: _boss_bar_root = null _boss_bar_fill = null _boss_bar_label = null + _boss_name = "" -func _build_boss_bar(max_hp: float) -> void: +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 @@ -163,7 +172,7 @@ func _build_boss_bar(max_hp: float) -> void: # Name label _boss_bar_label = Label.new() - _boss_bar_label.text = "THE SUPERVISOR" + _boss_bar_label.text = boss_name _boss_bar_label.position = Vector2(0, -22) _boss_bar_root.add_child(_boss_bar_label)