#115: the lancer winds up once, and leaves hard

One back and forth rather than two and a half, over 0.85s rather than 1.2.
The tell is a recoil and a spring; repeating it turned a wind-up into a
fidget and the player stopped reading it as "about to happen".

And the charge is 1650px/s, up from 950, which read as a fast drift rather
than as the thing the whole wind-up was promising.

The test counts how many times the wind-up crosses its anchor: one back and
forth crosses exactly once, two would cross three times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-23 21:38:08 +02:00
commit 7a78d1b1a6
2 changed files with 33 additions and 8 deletions

View file

@ -32,14 +32,21 @@ enum Phase { HUNT, WINDUP, CHARGE, RECOVER }
# The drift in HUNT is deliberately slower than the charge by an order of # The drift in HUNT is deliberately slower than the charge by an order of
# magnitude: the approach is a stalk, the charge is the attack. # magnitude: the approach is a stalk, the charge is the attack.
@export var approach_speed_factor: float = 0.5 @export var approach_speed_factor: float = 0.5
@export var windup_time: float = 1.2 # Shortened with the cycle count (#115): one back-and-forth in the time two
# and a half used to take would be a slow-motion tell.
@export var windup_time: float = 0.85
# #96 to the letter: the trajectory line appears 300ms before it leaves. # #96 to the letter: the trajectory line appears 300ms before it leaves.
@export var telegraph_time: float = 0.3 @export var telegraph_time: float = 0.3
# The "small back and forth" - it recoils along the charge axis, so the # The "small back and forth" - it recoils along the charge axis, so the
# motion reads as winding up to spring rather than as drifting. # motion reads as winding up to spring rather than as drifting.
@export var windup_sway: float = 30.0 @export var windup_sway: float = 34.0
@export var windup_cycles: float = 2.5 # #115: exactly ONE back and forth, not two and a half. The tell is a recoil
@export var charge_speed: float = 950.0 # and a spring; repeating it turned a wind-up into a fidget, and the player
# stopped reading it as "about to happen".
@export var windup_cycles: float = 1.0
# #115: and it leaves hard. At 950 the charge read as a fast drift rather
# than as the thing the whole wind-up was promising.
@export var charge_speed: float = 1650.0
# It flies THROUGH the locked point rather than stopping on it. A charge # It flies THROUGH the locked point rather than stopping on it. A charge
# that halted exactly where the ship used to be would look like it was # that halted exactly where the ship used to be would look like it was
# aiming at empty space on purpose. # aiming at empty space on purpose.
@ -93,7 +100,10 @@ func _tick_hunt(enemy: Node2D, delta: float) -> void:
return return
var to_target: Vector2 = target.global_position - enemy.global_position var to_target: Vector2 = target.global_position - enemy.global_position
var distance: float = to_target.length() var distance: float = to_target.length()
if distance > detection_range or distance < 0.001: if distance < 0.001:
return
# #113: see EnemyPursuitComponent.
if not EnemyTargets.ignores_range(enemy) and distance > detection_range:
return return
var direction: Vector2 = to_target / distance var direction: Vector2 = to_target / distance
if distance <= lock_range: if distance <= lock_range:

View file

@ -17,7 +17,8 @@ extends SceneTree
# B. from beyond lock_range it does NOT charge - it closes slowly, so the # B. from beyond lock_range it does NOT charge - it closes slowly, so the
# player sees it coming # player sees it coming
# C. once locked it holds station for the wind-up and rocks back and forth # C. once locked it holds station for the wind-up and rocks back and forth
# along the charge axis instead of drifting toward the target # along the charge axis instead of drifting toward the target - ONCE
# (#115), because repeating the tell turned a wind-up into a fidget
# D. the trajectory line comes up during the wind-up and lasts about # D. the trajectory line comes up during the wind-up and lasts about
# 300ms - not the whole wind-up, and not nothing # 300ms - not the whole wind-up, and not nothing
# E. the charge is straight, fast, and aimed at where the target WAS: a # E. the charge is straight, fast, and aimed at where the target WAS: a
@ -42,6 +43,8 @@ var windup_seen: bool = false
var windup_anchor: Vector2 = Vector2.ZERO var windup_anchor: Vector2 = Vector2.ZERO
var max_sway: float = 0.0 var max_sway: float = 0.0
var max_lateral: float = 0.0 var max_lateral: float = 0.0
var sway_crossings: int = 0
var last_side: int = 0
var telegraph_time_seen: float = 0.0 var telegraph_time_seen: float = 0.0
var charge_start: Vector2 = Vector2.ZERO var charge_start: Vector2 = Vector2.ZERO
var dodged_to: Vector2 = Vector2.ZERO var dodged_to: Vector2 = Vector2.ZERO
@ -141,7 +144,16 @@ func _process(delta: float) -> bool:
windup_anchor = lunge.charge_anchor windup_anchor = lunge.charge_anchor
charge_locked = lunge.locked_point charge_locked = lunge.locked_point
var offset: Vector2 = lancer.global_position - windup_anchor var offset: Vector2 = lancer.global_position - windup_anchor
max_sway = maxf(max_sway, absf(offset.dot(lunge.charge_direction))) var along: float = offset.dot(lunge.charge_direction)
max_sway = maxf(max_sway, absf(along))
# One back-and-forth crosses the anchor exactly once: out one
# way, back through it, out the other. Two would cross three
# times.
if absf(along) > 2.0:
var side: int = 1 if along > 0.0 else -1
if last_side != 0 and side != last_side:
sway_crossings += 1
last_side = side
var across: Vector2 = Vector2(-lunge.charge_direction.y, lunge.charge_direction.x) var across: Vector2 = Vector2(-lunge.charge_direction.y, lunge.charge_direction.x)
max_lateral = maxf(max_lateral, absf(offset.dot(across))) max_lateral = maxf(max_lateral, absf(offset.dot(across)))
if lunge.is_telegraphing(): if lunge.is_telegraphing():
@ -170,7 +182,10 @@ func _process(delta: float) -> bool:
max_sway, lunge.windup_sway]) max_sway, lunge.windup_sway])
if max_lateral > 1.0: if max_lateral > 1.0:
return _fail("it wandered %.0fpx sideways during the wind-up instead of holding station" % max_lateral) return _fail("it wandered %.0fpx sideways during the wind-up instead of holding station" % max_lateral)
print("wind-up: rocked %.0fpx back and forth, %.0fpx sideways" % [max_sway, max_lateral]) if sway_crossings != 1:
return _fail("the wind-up crossed its anchor %d times - #115 asks for exactly one back and forth" \
% sway_crossings)
print("wind-up: rocked %.0fpx back and forth once, %.0fpx sideways" % [max_sway, max_lateral])
# 300ms of line, give or take the frames either side of the # 300ms of line, give or take the frames either side of the
# threshold. Both bounds matter: a line up for the whole wind-up # threshold. Both bounds matter: a line up for the whole wind-up