Improve aim assist target stickiness
This commit is contained in:
parent
c2ebbfaa2d
commit
3975959017
5 changed files with 122 additions and 64 deletions
|
|
@ -104,9 +104,16 @@ func aim(direction: Vector2) -> void:
|
||||||
func aim_at(point: Vector2) -> void:
|
func aim_at(point: Vector2) -> void:
|
||||||
if fleet == null:
|
if fleet == null:
|
||||||
return
|
return
|
||||||
aim_point = point
|
var origin: Vector2 = fleet.get_centroid()
|
||||||
|
var reach: float = guide_range()
|
||||||
|
# The reticle resolves once from the fleet centre, so the player's cursor
|
||||||
|
# visibly sticks. Hulls keep the raw point and resolve it from their own
|
||||||
|
# muzzle/range below; a short-range ship must not bend toward something it
|
||||||
|
# cannot actually reach (#172).
|
||||||
|
aim_point = WEAPON_COMPONENT.assisted_point(get_tree(), origin, point, reach) \
|
||||||
|
if reach > 0.0 else point
|
||||||
has_aim = true
|
has_aim = true
|
||||||
aim_direction = point - fleet.get_centroid()
|
aim_direction = aim_point - origin
|
||||||
for ship in fleet.ships:
|
for ship in fleet.ships:
|
||||||
if not is_instance_valid(ship):
|
if not is_instance_valid(ship):
|
||||||
continue
|
continue
|
||||||
|
|
@ -148,10 +155,9 @@ func _draw() -> void:
|
||||||
if reach <= 0.0:
|
if reach <= 0.0:
|
||||||
return
|
return
|
||||||
var origin: Vector2 = fleet.get_centroid()
|
var origin: Vector2 = fleet.get_centroid()
|
||||||
# Drawn along the heading a shot will ACTUALLY take, assist included, so
|
# aim_at() has already resolved the shared assisted point, so the line and
|
||||||
# the line keeps telling the truth about where fire is going (#89).
|
# reticle show the position that every in-range hull resolves toward (#181).
|
||||||
var heading: Vector2 = WEAPON_COMPONENT.assisted_heading(get_tree(), origin,
|
var heading: Vector2 = aim_direction.normalized()
|
||||||
aim_direction, reach)
|
|
||||||
# #172: the line stops at the aimed PLACE when that is inside the guns'
|
# #172: the line stops at the aimed PLACE when that is inside the guns'
|
||||||
# reach. Running it on to full range past a point the fleet is converging
|
# reach. Running it on to full range past a point the fleet is converging
|
||||||
# on would be drawing a direction again, which is what the ticket took
|
# on would be drawing a direction again, which is what the ticket took
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ var max_hp: int
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("enemies")
|
add_to_group("enemies")
|
||||||
|
# Weapons discover anything the player may deliberately aim at through one
|
||||||
|
# compositional contract. Asteroids implement the same contract without the
|
||||||
|
# aim-assist code having to know either concrete type (#181).
|
||||||
|
add_to_group("player_aim_targets")
|
||||||
definition = ENEMY_DEFINITIONS.get_definition(enemy_id)
|
definition = ENEMY_DEFINITIONS.get_definition(enemy_id)
|
||||||
speed = definition.speed
|
speed = definition.speed
|
||||||
acceleration = definition.acceleration
|
acceleration = definition.acceleration
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ extends SceneTree
|
||||||
# B. an enemy far off the heading is not - past the cone the shot goes where
|
# 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,
|
# the player pointed, or the gun would be choosing its own targets again,
|
||||||
# which is exactly what #80 took away
|
# which is exactly what #80 took away
|
||||||
# C. the NEAREST candidate inside the cone wins
|
# C. cursor proximity can select a target outside the directional cone
|
||||||
# D. asteroids are never assisted onto. Snapping to a rock while the player
|
# D. with several candidates, the one nearest the cursor wins rather than
|
||||||
# is trying to hit the thing shooting at them is the assist working
|
# blindly choosing the one nearest the shooter
|
||||||
# against them
|
# E. asteroids participate through the same aimable-target contract
|
||||||
# E. the aim guide follows the assist, so the dotted line keeps telling the
|
# F. the guide snaps to the target position and reachable guns bend to the
|
||||||
# truth about where fire is going
|
# same target, so the on-screen reticle tells the truth
|
||||||
|
|
||||||
var game_state: Node
|
var game_state: Node
|
||||||
var main: Node
|
var main: Node
|
||||||
|
|
@ -63,6 +63,12 @@ func _heading(direction: Vector2) -> Vector2:
|
||||||
weapon.fire_range)
|
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:
|
func _process(delta: float) -> bool:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
if stage != 0:
|
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)
|
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")
|
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()
|
_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)))
|
var close_target: Node2D = _enemy_at(Vector2(300.0, 0.0).rotated(deg_to_rad(20.0)))
|
||||||
assisted = _heading(AIM)
|
assisted = _heading(AIM)
|
||||||
var to_close: Vector2 = (close_target.global_position - shooter.global_position).normalized()
|
var to_close: Vector2 = (close_target.global_position - shooter.global_position).normalized()
|
||||||
var to_far: Vector2 = (far_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:
|
if assisted.distance_to(to_far) > 0.01:
|
||||||
return _fail("the assist chose the enemy at 800 over the one at 300 (heading %s, close %s, far %s)" \
|
return _fail("the assist ignored the cursor-near enemy at %.0f for the shooter-near one at 300 (heading %s, close %s, far %s)" \
|
||||||
% [assisted, to_close, to_far])
|
% [far_distance, assisted, to_close, to_far])
|
||||||
print("with two in the cone, the nearer one wins")
|
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()
|
_clear()
|
||||||
var rock: Node2D = load("res://world/Asteroid.tscn").instantiate()
|
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))
|
rock.position = shooter.global_position + Vector2(400.0, 0.0).rotated(deg_to_rad(15.0))
|
||||||
main.add_child(rock)
|
main.add_child(rock)
|
||||||
assisted = _heading(AIM)
|
assisted = _heading(AIM)
|
||||||
if assisted.distance_to(AIM) > 0.01:
|
var to_rock: Vector2 = (rock.global_position - shooter.global_position).normalized()
|
||||||
return _fail("the assist snapped onto an asteroid (heading %s)" % assisted)
|
if assisted.distance_to(to_rock) > 0.01:
|
||||||
print("asteroids are never assisted onto")
|
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()
|
_clear()
|
||||||
var guide_target: Node2D = _enemy_at(Vector2(500.0, 0.0).rotated(deg_to_rad(20.0)))
|
var guide_target: Node2D = _enemy_at(Vector2(500.0, 0.0).rotated(deg_to_rad(20.0)))
|
||||||
controller.aim(AIM)
|
controller.aim(AIM)
|
||||||
controller.aim_direction = AIM
|
if controller.aim_point.distance_to(guide_target.global_position) > 0.02:
|
||||||
var guided: Vector2 = load("res://vessels/VesselWeaponComponent.gd").assisted_heading(
|
return _fail("the reticle stayed at %s instead of sticking to %s" \
|
||||||
self, fleet.get_centroid(), controller.aim_direction, controller.guide_range())
|
% [controller.aim_point, guide_target.global_position])
|
||||||
var from_fleet: Vector2 = (guide_target.global_position - fleet.get_centroid()).normalized()
|
var gun_heading: Vector2 = weapon.firing_heading()
|
||||||
if guided.distance_to(from_fleet) > 0.02:
|
var target_heading: Vector2 = (guide_target.global_position - shooter.global_position).normalized()
|
||||||
return _fail("the aim guide does not follow the assist (guide %s, target %s)" % [guided, from_fleet])
|
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.
|
# 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 source: FileAccess = FileAccess.open("res://controllers/FireStickController.gd", FileAccess.READ)
|
||||||
var text: String = source.get_as_text()
|
var text: String = source.get_as_text()
|
||||||
source.close()
|
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")
|
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")
|
print("TEST_AIM_ASSIST: PASS")
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
|
||||||
|
|
@ -54,42 +54,66 @@ func apply_stats() -> void:
|
||||||
# past about 45 the gun starts choosing targets on its own, which is the
|
# past about 45 the gun starts choosing targets on its own, which is the
|
||||||
# behaviour #80 deliberately took away.
|
# behaviour #80 deliberately took away.
|
||||||
const ASSIST_CONE_DEGREES: float = 28.0
|
const ASSIST_CONE_DEGREES: float = 28.0
|
||||||
|
# #181: a nearby cursor is a second way to express intent. This is deliberately
|
||||||
|
# smaller than the directional cone at ordinary weapon range: it catches a
|
||||||
|
# pointer placed beside something without making the whole screen magnetic.
|
||||||
|
const ASSIST_CURSOR_RADIUS: float = 180.0
|
||||||
|
const ASSIST_TARGET_GROUP: StringName = &"player_aim_targets"
|
||||||
|
|
||||||
|
|
||||||
# The heading a shot fired from `origin` along `direction` should actually
|
# The concrete point an aim should stick to. A target can qualify in two ways:
|
||||||
# take: straight, unless an enemy sits inside the assist cone and within
|
# it is close to the cursor, or it lies inside the established directional
|
||||||
# reach, in which case it points at the nearest one.
|
# cone. Cursor proximity wins, then distance from the shooter breaks the old
|
||||||
|
# directional case. This preserves approximate stick aiming while letting a
|
||||||
|
# deliberately placed cursor choose a farther target in the same direction.
|
||||||
#
|
#
|
||||||
# Static, and used by BOTH the gun and the aim guide (see
|
# Targets opt in through one group rather than this utility branching on
|
||||||
# FireStickController._draw). If the guide drew the raw stick heading while
|
# enemies versus asteroids. That keeps future aimable compositions out of the
|
||||||
# the shot took an assisted one, the line would be telling the player
|
# weapon code: joining the contract is all they need to do.
|
||||||
# something the gun does not do - the same class of mismatch #86 removed
|
static func assisted_point(tree: SceneTree, origin: Vector2, point: Vector2,
|
||||||
# between the guide's length and a bolt's actual reach.
|
reach: float) -> Vector2:
|
||||||
#
|
var to_point: Vector2 = point - origin
|
||||||
# Enemies only, never asteroids. Snapping onto a rock while the player is
|
var heading: Vector2 = to_point.normalized()
|
||||||
# trying to hit the thing shooting at them would be the assist working
|
if heading == Vector2.ZERO or tree == null or reach <= 0.0:
|
||||||
# against them; rocks are large, stationary enough, and easy to hit on
|
return point
|
||||||
# purpose.
|
var cone: float = cos(deg_to_rad(ASSIST_CONE_DEGREES))
|
||||||
|
var cursor_pick: Node2D = null
|
||||||
|
var cursor_distance: float = INF
|
||||||
|
var direction_pick: Node2D = null
|
||||||
|
var direction_distance: float = INF
|
||||||
|
for target_node in tree.get_nodes_in_group(ASSIST_TARGET_GROUP):
|
||||||
|
if not is_instance_valid(target_node) or not (target_node is Node2D) \
|
||||||
|
or target_node.is_queued_for_deletion():
|
||||||
|
continue
|
||||||
|
var target: Node2D = target_node as Node2D
|
||||||
|
var to_target: Vector2 = target.global_position - origin
|
||||||
|
var distance: float = to_target.length()
|
||||||
|
if distance > reach or distance < 0.001:
|
||||||
|
continue
|
||||||
|
var from_cursor: float = target.global_position.distance_to(point)
|
||||||
|
if from_cursor <= ASSIST_CURSOR_RADIUS and from_cursor < cursor_distance:
|
||||||
|
cursor_distance = from_cursor
|
||||||
|
cursor_pick = target
|
||||||
|
if to_target.normalized().dot(heading) >= cone and distance < direction_distance:
|
||||||
|
direction_distance = distance
|
||||||
|
direction_pick = target
|
||||||
|
if cursor_pick != null:
|
||||||
|
return cursor_pick.global_position
|
||||||
|
if direction_pick != null:
|
||||||
|
return direction_pick.global_position
|
||||||
|
return point
|
||||||
|
|
||||||
|
|
||||||
|
# Compatibility entry point for systems that genuinely have only a heading.
|
||||||
|
# Their virtual cursor sits at maximum range; the returned vector still points
|
||||||
|
# at the same target selected by assisted_point().
|
||||||
static func assisted_heading(tree: SceneTree, origin: Vector2, direction: Vector2,
|
static func assisted_heading(tree: SceneTree, origin: Vector2, direction: Vector2,
|
||||||
reach: float) -> Vector2:
|
reach: float) -> Vector2:
|
||||||
var heading: Vector2 = direction.normalized()
|
var heading: Vector2 = direction.normalized()
|
||||||
if heading == Vector2.ZERO or tree == null:
|
if heading == Vector2.ZERO:
|
||||||
return heading
|
return heading
|
||||||
var cone: float = cos(deg_to_rad(ASSIST_CONE_DEGREES))
|
var point: Vector2 = assisted_point(tree, origin, origin + heading * reach, reach)
|
||||||
var nearest: Vector2 = Vector2.ZERO
|
return (point - origin).normalized()
|
||||||
var nearest_distance: float = INF
|
|
||||||
for enemy in tree.get_nodes_in_group("enemies"):
|
|
||||||
if not is_instance_valid(enemy) or not (enemy is Node2D):
|
|
||||||
continue
|
|
||||||
var to_enemy: Vector2 = (enemy as Node2D).global_position - origin
|
|
||||||
var distance: float = to_enemy.length()
|
|
||||||
if distance > reach or distance < 0.001 or distance >= nearest_distance:
|
|
||||||
continue
|
|
||||||
if to_enemy.normalized().dot(heading) < cone:
|
|
||||||
continue
|
|
||||||
nearest_distance = distance
|
|
||||||
nearest = to_enemy
|
|
||||||
return heading if nearest == Vector2.ZERO else nearest.normalized()
|
|
||||||
|
|
||||||
|
|
||||||
# Where this gun is firing FROM. The hull it is bolted to.
|
# Where this gun is firing FROM. The hull it is bolted to.
|
||||||
|
|
@ -130,10 +154,12 @@ func is_firing() -> bool:
|
||||||
func firing_heading() -> Vector2:
|
func firing_heading() -> Vector2:
|
||||||
if not has_fire_target:
|
if not has_fire_target:
|
||||||
return Vector2.ZERO
|
return Vector2.ZERO
|
||||||
var to_point: Vector2 = fire_target - muzzle()
|
var origin: Vector2 = muzzle()
|
||||||
|
var to_point: Vector2 = fire_target - origin
|
||||||
if to_point.length() <= 0.001:
|
if to_point.length() <= 0.001:
|
||||||
return Vector2.ZERO
|
return Vector2.ZERO
|
||||||
return assisted_heading(get_tree(), muzzle(), to_point, fire_range)
|
var point: Vector2 = assisted_point(get_tree(), origin, fire_target, fire_range)
|
||||||
|
return (point - origin).normalized()
|
||||||
|
|
||||||
|
|
||||||
func tick(delta: float) -> void:
|
func tick(delta: float) -> void:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ var shape_points: PackedVector2Array = PackedVector2Array()
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
randomize()
|
randomize()
|
||||||
add_to_group("asteroids")
|
add_to_group("asteroids")
|
||||||
|
add_to_group("player_aim_targets")
|
||||||
area_entered.connect(_on_area_entered)
|
area_entered.connect(_on_area_entered)
|
||||||
|
|
||||||
var point_count: int = 7
|
var point_count: int = 7
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue