From e5439f71835555ec0c5de7e6f8b22e62d85b1f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 4 Sep 2026 18:40:58 +0200 Subject: [PATCH] Refine boss reveal and dash motion --- boss/BossEncounter.gd | 15 ++++++++------- tests/test_boss_planet.gd | 10 +++++++--- tests/test_energy_propulser_dash.gd | 10 ++++++++++ vessels/Fleet.gd | 24 +++++++++++++++++++++++- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/boss/BossEncounter.gd b/boss/BossEncounter.gd index 485da54..4cf4bde 100644 --- a/boss/BossEncounter.gd +++ b/boss/BossEncounter.gd @@ -63,7 +63,7 @@ func _process(delta: float) -> void: return if not reveal_started: _update_indicator() - if _boss_reaches_view(): + if _boss_is_fully_visible(): begin_reveal() return if encounter_active: @@ -98,17 +98,18 @@ func _set_combat_ui_visible(shown: bool) -> void: control.visible = shown -func _boss_reaches_view() -> bool: +func _boss_is_fully_visible() -> bool: # World-space math is immediate. The viewport canvas transform trails a # Camera2D assignment until the render server updates, which made a fast # approach able to cross the threshold one frame before the detector saw - # it. Including the tentacle reach makes the reveal begin when the first - # part of the creature actually touches the view. + # it. The tentacles deliberately reach beyond the framing, so the authored + # body radius is the useful cinematic boundary: the roar must wait until + # the whole central creature is on screen, not fire when a tentacle enters. var half_view: Vector2 = get_viewport_rect().size * 0.5 / camera.zoom - var extent: float = boss.TENTACLE_LENGTH * boss.scale.x + var extent: float = boss.BODY_RADIUS * maxf(boss.scale.x, boss.scale.y) var separation: Vector2 = (boss.global_position - camera.global_position).abs() - return separation.x <= half_view.x + extent \ - and separation.y <= half_view.y + extent + return separation.x + extent <= half_view.x \ + and separation.y + extent <= half_view.y func _update_indicator() -> void: diff --git a/tests/test_boss_planet.gd b/tests/test_boss_planet.gd index e68047e..4ae90dc 100644 --- a/tests/test_boss_planet.gd +++ b/tests/test_boss_planet.gd @@ -124,12 +124,16 @@ func _process(delta: float) -> bool: return _fail("the off-screen boss has no directional border indicator") if state.encounter_locks_movement or boss.combat_active: return _fail("the off-screen approach locked player movement or already started combat") - # Put the camera where the player can carry the fleet; the encounter - # itself must detect the boss crossing into view. + # Seeing a tentacle is not enough to trigger the cinematic or its roar. surface.get_node("Camera2D").global_position = boss.global_position + Vector2(0.0, 1000.0) encounter._process(0.0) + if encounter.reveal_started: + return _fail("a partially visible boss triggered its reveal and scream") + # Carry the fleet close enough to frame the complete central body. + surface.get_node("Camera2D").global_position = boss.global_position + Vector2(0.0, 250.0) + encounter._process(0.0) if not encounter.reveal_started: - return _fail("bringing the boss into view did not trigger its reveal") + return _fail("fully framing the boss did not trigger its reveal") if not state.encounter_locks_movement: return _fail("the reveal did not briefly lock movement for the cinematic") if not encounter.health_bar.visible or encounter.indicator.visible: diff --git a/tests/test_energy_propulser_dash.gd b/tests/test_energy_propulser_dash.gd index 10b7fa8..25b8e80 100644 --- a/tests/test_energy_propulser_dash.gd +++ b/tests/test_energy_propulser_dash.gd @@ -65,6 +65,16 @@ func _process(delta: float) -> bool: return _fail("first dash did not consume %d energy" % DASH_COST) if first_component.is_ready(): return _fail("the Propulser that dashed did not enter cooldown") + # The burst should cover substantially more than a linear quarter of + # its distance at a quarter of its duration, then ease into its stop. + fleet.set_process(false) + fleet._advance_dash(fleet.dash_duration * 0.25) + var early_distance: float = fleet.get_centroid().distance_to(first_origin) + if early_distance <= DASH_DISTANCE * 0.45 \ + or early_distance >= DASH_DISTANCE * 0.7: + return _fail("dash has no useful ease-out curve (quarter-time distance %.1f)" \ + % early_distance) + fleet.set_process(true) dash._process(0.0) if not main.get_node("UI/DashButton").enabled: return _fail("one cooling Propulser disabled a second ready one") diff --git a/vessels/Fleet.gd b/vessels/Fleet.gd index 3a6655f..3e44c4d 100644 --- a/vessels/Fleet.gd +++ b/vessels/Fleet.gd @@ -36,6 +36,9 @@ var steer_direction: Vector2 = Vector2.ZERO # An aimed Propulser burst translates the complete formation as one body. var dash_direction: Vector2 = Vector2.ZERO var dash_remaining: float = 0.0 +var dash_distance: float = 0.0 +var dash_elapsed: float = 0.0 +var dash_duration: float = 0.0 const DASH_SPEED: float = 2800.0 @@ -230,6 +233,9 @@ func teleport_to(world_pos: Vector2) -> void: steer_direction = Vector2.ZERO dash_remaining = 0.0 dash_direction = Vector2.ZERO + dash_distance = 0.0 + dash_elapsed = 0.0 + dash_duration = 0.0 fleet_target = world_pos anchor = world_pos GameState.fleet_position = get_centroid() @@ -259,6 +265,9 @@ func dash(direction: Vector2, distance: float) -> bool: anchor = get_formation_origin() dash_direction = direction.normalized() dash_remaining = distance + dash_distance = distance + dash_elapsed = 0.0 + dash_duration = distance / DASH_SPEED return true @@ -287,7 +296,15 @@ func _process(delta: float) -> void: func _advance_dash(delta: float) -> void: - var step: float = minf(DASH_SPEED * delta, dash_remaining) + var previous_progress: float = dash_elapsed / dash_duration + dash_elapsed = minf(dash_elapsed + delta, dash_duration) + var progress: float = dash_elapsed / dash_duration + # Cubic ease-out gives the Propulser an immediate punch, then lets the + # formation settle smoothly at the aimed point. Applying the difference + # between eased positions keeps the final distance frame-rate independent. + var previous_distance: float = dash_distance * _dash_curve(previous_progress) + var target_distance: float = dash_distance * _dash_curve(progress) + var step: float = minf(target_distance - previous_distance, dash_remaining) var displacement: Vector2 = dash_direction * step dash_remaining -= step anchor += displacement @@ -309,6 +326,11 @@ func _advance_dash(delta: float) -> void: _assign_slot_targets() +func _dash_curve(progress: float) -> float: + var remaining: float = 1.0 - clampf(progress, 0.0, 1.0) + return 1.0 - remaining * remaining * remaining + + func _effective_cruise_speed() -> float: return cruise_speed * GameState.fleet_speed_multiplier