Improve aim assist target stickiness

This commit is contained in:
Loïc Wisniewski 2026-09-02 17:15:12 +02:00
commit 3975959017
5 changed files with 122 additions and 64 deletions

View file

@ -11,12 +11,12 @@ extends SceneTree
# B. an enemy far off the heading is not - past the cone the shot goes where
# the player pointed, or the gun would be choosing its own targets again,
# which is exactly what #80 took away
# C. the NEAREST candidate inside the cone wins
# D. asteroids are never assisted onto. Snapping to a rock while the player
# is trying to hit the thing shooting at them is the assist working
# against them
# E. the aim guide follows the assist, so the dotted line keeps telling the
# truth about where fire is going
# C. cursor proximity can select a target outside the directional cone
# D. with several candidates, the one nearest the cursor wins rather than
# blindly choosing the one nearest the shooter
# E. asteroids participate through the same aimable-target contract
# F. the guide snaps to the target position and reachable guns bend to the
# same target, so the on-screen reticle tells the truth
var game_state: Node
var main: Node
@ -63,6 +63,12 @@ func _heading(direction: Vector2) -> Vector2:
weapon.fire_range)
func _point(at: Vector2) -> Vector2:
var script: Script = load("res://vessels/VesselWeaponComponent.gd")
return script.assisted_point(self, shooter.global_position, at,
weapon.fire_range)
func _process(delta: float) -> bool:
elapsed += delta
if stage != 0:
@ -95,45 +101,60 @@ func _process(delta: float) -> bool:
return _fail("an enemy 55 degrees off the heading was still picked up (heading %s)" % assisted)
print("an enemy 55 degrees off the aim is ignored - the shot goes where it was aimed")
# --- C. the nearest inside the cone wins ------------------------------
# --- C. a nearby cursor is magnetic too -------------------------------
_clear()
var far_target: Node2D = _enemy_at(Vector2(800.0, 0.0).rotated(deg_to_rad(5.0)))
var raw_cursor: Vector2 = shooter.global_position + Vector2(250.0, 0.0)
var cursor_target: Node2D = _enemy_at(Vector2(150.0, 0.0).rotated(deg_to_rad(45.0)))
var snapped: Vector2 = _point(raw_cursor)
if snapped.distance_to(cursor_target.global_position) > 0.01:
return _fail("a cursor %.0f units from a target outside the cone did not stick to it" \
% raw_cursor.distance_to(cursor_target.global_position))
print("a cursor near a target sticks to its position even outside the directional cone")
# --- D. cursor proximity chooses between cone candidates --------------
_clear()
var far_distance: float = weapon.fire_range - 100.0
var far_target: Node2D = _enemy_at(Vector2(far_distance, 0.0).rotated(deg_to_rad(5.0)))
var close_target: Node2D = _enemy_at(Vector2(300.0, 0.0).rotated(deg_to_rad(20.0)))
assisted = _heading(AIM)
var to_close: Vector2 = (close_target.global_position - shooter.global_position).normalized()
var to_far: Vector2 = (far_target.global_position - shooter.global_position).normalized()
if assisted.distance_to(to_close) > 0.01:
return _fail("the assist chose the enemy at 800 over the one at 300 (heading %s, close %s, far %s)" \
% [assisted, to_close, to_far])
print("with two in the cone, the nearer one wins")
if assisted.distance_to(to_far) > 0.01:
return _fail("the assist ignored the cursor-near enemy at %.0f for the shooter-near one at 300 (heading %s, close %s, far %s)" \
% [far_distance, assisted, to_close, to_far])
print("with two in the cone, the target nearest the cursor wins")
# --- D. never onto an asteroid ----------------------------------------
# --- E. asteroids use the same assist contract -------------------------
_clear()
var rock: Node2D = load("res://world/Asteroid.tscn").instantiate()
rock.position = shooter.global_position + Vector2(400.0, 0.0).rotated(deg_to_rad(15.0))
main.add_child(rock)
assisted = _heading(AIM)
if assisted.distance_to(AIM) > 0.01:
return _fail("the assist snapped onto an asteroid (heading %s)" % assisted)
print("asteroids are never assisted onto")
var to_rock: Vector2 = (rock.global_position - shooter.global_position).normalized()
if assisted.distance_to(to_rock) > 0.01:
return _fail("the assist did not snap onto an asteroid (heading %s, asteroid %s)" \
% [assisted, to_rock])
print("asteroids opt into the same aim-assist target group as enemies")
# --- E. the guide follows the assist ----------------------------------
# --- F. the guide and reachable guns share the snapped target ----------
_clear()
var guide_target: Node2D = _enemy_at(Vector2(500.0, 0.0).rotated(deg_to_rad(20.0)))
controller.aim(AIM)
controller.aim_direction = AIM
var guided: Vector2 = load("res://vessels/VesselWeaponComponent.gd").assisted_heading(
self, fleet.get_centroid(), controller.aim_direction, controller.guide_range())
var from_fleet: Vector2 = (guide_target.global_position - fleet.get_centroid()).normalized()
if guided.distance_to(from_fleet) > 0.02:
return _fail("the aim guide does not follow the assist (guide %s, target %s)" % [guided, from_fleet])
if controller.aim_point.distance_to(guide_target.global_position) > 0.02:
return _fail("the reticle stayed at %s instead of sticking to %s" \
% [controller.aim_point, guide_target.global_position])
var gun_heading: Vector2 = weapon.firing_heading()
var target_heading: Vector2 = (guide_target.global_position - shooter.global_position).normalized()
if gun_heading.distance_to(target_heading) > 0.02:
return _fail("the reachable gun headed %s instead of toward snapped target %s" \
% [gun_heading, target_heading])
# And the guide must be drawn from that same helper, not a copy of the maths.
var source: FileAccess = FileAccess.open("res://controllers/FireStickController.gd", FileAccess.READ)
var text: String = source.get_as_text()
source.close()
if "assisted_heading" not in text:
if "assisted_point" not in text:
return _fail("FireStickController draws its guide without the assist, so the line and the shot would diverge")
print("the aim guide is drawn along the assisted heading, from the same helper the gun uses")
print("the reticle snaps to the target and reachable guns bend toward it")
print("TEST_AIM_ASSIST: PASS")
quit(0)