Aim the laser by depth, and ease the aim view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
a902fddba0
commit
2239235993
5 changed files with 123 additions and 28 deletions
|
|
@ -8,6 +8,17 @@ const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|||
# shot effect, and effect visuals.
|
||||
|
||||
const AIM_DEADZONE: float = 22.0
|
||||
# The aim view eases to its framing instead of cutting to it, in both
|
||||
# directions. A hard jump loses where everything just went - the more so now
|
||||
# that aiming can travel a long way - and the eye cannot follow a cut it did
|
||||
# not make. Fast, though: this happens under a thumb that is already aiming,
|
||||
# so it settles in about a quarter of a second rather than gliding.
|
||||
const ZOOM_EASE_SPEED: float = 12.0
|
||||
# A floor under that rate, as a multiple of the zoom being approached. A purely
|
||||
# proportional ease slows as it closes and never actually arrives, which would
|
||||
# leave "has the camera restored" false forever and let the next aim record a
|
||||
# resting zoom that is still moving.
|
||||
const ZOOM_MIN_RATE: float = 4.0
|
||||
|
||||
@export var minimum_zoom: float = 0.4
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
|
|
@ -22,6 +33,9 @@ var aim_start_screen: Vector2 = Vector2.ZERO
|
|||
var aim_screen: Vector2 = Vector2.ZERO
|
||||
var aim_target: Vector2 = Vector2.ZERO
|
||||
var saved_zoom: float = 1.0
|
||||
# What the aim wants to be framed at. Written by _update_aim_zoom(); the
|
||||
# camera walks toward it in _process() rather than being assigned it.
|
||||
var desired_zoom: float = 1.0
|
||||
var restoring_zoom: bool = false
|
||||
|
||||
|
||||
|
|
@ -45,11 +59,11 @@ func _process(delta: float) -> void:
|
|||
if status.text != "" and Time.get_ticks_msec() >= status_message_until:
|
||||
status.text = ""
|
||||
|
||||
if restoring_zoom and not aiming:
|
||||
var zoom: float = move_toward(camera.zoom.x, saved_zoom,
|
||||
maxf(saved_zoom, 0.1) * delta * 5.0)
|
||||
camera.zoom = Vector2(zoom, zoom)
|
||||
if is_equal_approx(zoom, saved_zoom):
|
||||
if aiming:
|
||||
_ease_zoom(desired_zoom, delta)
|
||||
elif restoring_zoom:
|
||||
_ease_zoom(saved_zoom, delta)
|
||||
if is_equal_approx(camera.zoom.x, saved_zoom):
|
||||
restoring_zoom = false
|
||||
|
||||
_advance_effects(delta)
|
||||
|
|
@ -77,6 +91,7 @@ func fire_at(world_target: Vector2) -> bool:
|
|||
# Recorded before firing because _fire() ends by restoring it, and a stale
|
||||
# value from an earlier aim would snap the camera somewhere unrelated.
|
||||
saved_zoom = camera.zoom.x
|
||||
desired_zoom = saved_zoom
|
||||
restoring_zoom = false
|
||||
aim_target = world_target
|
||||
_fire()
|
||||
|
|
@ -96,6 +111,7 @@ func handle_touch(event: InputEventScreenTouch) -> bool:
|
|||
aim_start_screen = event.position
|
||||
aim_screen = event.position
|
||||
saved_zoom = camera.zoom.x
|
||||
desired_zoom = saved_zoom
|
||||
restoring_zoom = false
|
||||
button.pressed_down = true
|
||||
button.queue_redraw()
|
||||
|
|
@ -165,9 +181,18 @@ func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
|||
else half_screen.x / (absf(displacement.x) + padding)
|
||||
var fit_y: float = saved_zoom if absf(displacement.y) < 1.0 \
|
||||
else half_screen.y / (absf(displacement.y) + padding)
|
||||
var desired: float = clampf(minf(saved_zoom, minf(fit_x, fit_y)),
|
||||
desired_zoom = clampf(minf(saved_zoom, minf(fit_x, fit_y)),
|
||||
minimum_zoom, saved_zoom)
|
||||
camera.zoom = Vector2(desired, desired)
|
||||
|
||||
|
||||
# Proportional rather than a fixed step per second, so the same easing reads
|
||||
# the same whether the camera is crossing from 2.5 to 0.15 or barely moving,
|
||||
# with the floor above closing the last of it exactly.
|
||||
func _ease_zoom(target: float, delta: float) -> void:
|
||||
var step: float = maxf(absf(target - camera.zoom.x) * ZOOM_EASE_SPEED,
|
||||
maxf(target, 0.1) * ZOOM_MIN_RATE) * delta
|
||||
var zoom: float = move_toward(camera.zoom.x, target, step)
|
||||
camera.zoom = Vector2(zoom, zoom)
|
||||
|
||||
|
||||
func _finish_aim() -> void:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
|||
# #203's range mark and the assist's target ring, in screen pixels.
|
||||
const END_TICK_PIXELS: float = 17.0
|
||||
const TARGET_RING_PIXELS: float = 15.0
|
||||
# How far the thumb travels to walk the aim point out to the beam's full
|
||||
# reach. The same idiom the gas bomb uses for its throw distance.
|
||||
const DRAG_RADIUS: float = 260.0
|
||||
# The aim view frames twice the distance to the chosen target, so there is as
|
||||
# much space beyond it as in front of it. Framing the target itself would put
|
||||
# it on the edge of the screen, which is the half that matters when the point
|
||||
# of dragging further is to reach past what is already close.
|
||||
const FRAMING_FACTOR: float = 2.0
|
||||
const BEAM_HALF_WIDTH := BEAM_EFFECT.BEAM_HALF_WIDTH
|
||||
const FLASH_DURATION := BEAM_EFFECT.FLASH_DURATION
|
||||
const ENERGY_PER_SHOT: int = VesselLaserComponent.ENERGY_PER_SHOT
|
||||
|
|
@ -55,9 +63,15 @@ func _aim_max_range() -> float:
|
|||
|
||||
|
||||
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||
# A beam always reaches full range; dragging chooses its direction only.
|
||||
# The shot always reaches full range; the drag chooses a direction AND a
|
||||
# depth along it. Depth is what lets the helper reach a target standing
|
||||
# behind a nearer one: the assist looks for something near the point the
|
||||
# thumb actually named, and only falls back to the nearest thing in the
|
||||
# cone when the player has named no place in particular.
|
||||
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
||||
return _assisted_target(origin, origin + direction * _aim_max_range())
|
||||
var reach: float = _aim_max_range()
|
||||
var depth: float = minf(drag.length() / DRAG_RADIUS, 1.0) * reach
|
||||
return _assisted_target(origin, origin + direction * maxf(depth, 1.0))
|
||||
|
||||
|
||||
# The mouse path (#80) names a world point directly instead of dragging one
|
||||
|
|
@ -117,14 +131,13 @@ func beam_tip(beam: Node2D) -> Vector2:
|
|||
return beam.tip()
|
||||
|
||||
|
||||
# #203: the aim view frames the beam's WHOLE reach, not the point the aim
|
||||
# happens to sit on. The assist can settle that point on an enemy a few
|
||||
# hundred units away (#201), and fitting the camera to it would say the shot
|
||||
# stops there. Fitting the far end instead is also what makes the range
|
||||
# visible at all: the beam is 2900 units long and the inherited floor on the
|
||||
# zoom - sized for the gas bomb's 950 - left three quarters of it off screen.
|
||||
# #203: framed around the chosen target rather than around the fleet. The
|
||||
# inherited floor on the zoom was sized for the gas bomb's 950 units and left
|
||||
# most of a 2900-unit aim off screen; the laser's own floor lets this fit.
|
||||
# Twice the target's distance, so what lies beyond it is on screen too - that
|
||||
# is the half the player is reaching into when they drag further out.
|
||||
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
||||
super(origin, origin + _aim_heading(origin, target) * _aim_max_range())
|
||||
super(origin, origin + (target - origin) * FRAMING_FACTOR)
|
||||
|
||||
|
||||
func _aim_heading(origin: Vector2, target: Vector2) -> Vector2:
|
||||
|
|
|
|||
|
|
@ -46,12 +46,26 @@ func _ready() -> void:
|
|||
press.position = button.global_position + button.size * 0.5
|
||||
press.pressed = true
|
||||
skill.handle_touch(press)
|
||||
var drag := InputEventScreenDrag.new()
|
||||
drag.index = 9
|
||||
drag.position = press.position + Vector2(-245.0, -235.0)
|
||||
skill.handle_drag(drag)
|
||||
for t in [0.35, 0.6]:
|
||||
# Enemies BEFORE the drag, so the aim helper actually sees them. A nearer
|
||||
# one on the heading and a further one just off it: the state this preview
|
||||
# exists to show is the drag reaching past the first to name the second.
|
||||
var heading: Vector2 = Vector2(-245.0, -235.0).normalized()
|
||||
var reach: float = skill._aim_max_range()
|
||||
var origin: Vector2 = fleet.get_centroid()
|
||||
var far_offset: Vector2 = heading.rotated(deg_to_rad(8.0)) * 1900.0
|
||||
for offset in [heading * 900.0, far_offset]:
|
||||
var enemy: Node = load("res://enemies/Enemy.tscn").instantiate()
|
||||
enemy.position = fleet.get_centroid().lerp(skill.aim_target, t)
|
||||
enemy.position = origin + offset
|
||||
main.add_child(enemy)
|
||||
enemy.speed = 0.0 # after add_child - _ready() sets it from the definition
|
||||
|
||||
var drag := InputEventScreenDrag.new()
|
||||
drag.index = 9
|
||||
# Dragged out to the further enemy's depth rather than to the beam's end.
|
||||
drag.position = press.position + far_offset.normalized() \
|
||||
* (1900.0 / reach * LaserController.DRAG_RADIUS)
|
||||
skill.handle_drag(drag)
|
||||
# The zoom eases now, so a preview that captures the first frame would
|
||||
# catch it mid-move. Settle it before the screenshot is taken.
|
||||
for _step in range(40):
|
||||
skill._process(0.05)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ func _process(delta: float) -> bool:
|
|||
drag.index = 7
|
||||
drag.position = Vector2(350, 700)
|
||||
main._handle_drag(drag)
|
||||
# The aim view eases to its framing now instead of cutting to it,
|
||||
# so the drag alone has not moved the camera yet.
|
||||
if not is_equal_approx(main.get_node("Camera2D").zoom.x, original_zoom):
|
||||
return _fail_now("the aim zoom jumped instead of easing")
|
||||
for _step in range(40):
|
||||
skill._process(0.05)
|
||||
if main.get_node("Camera2D").zoom.x >= original_zoom:
|
||||
return _fail_now("long aim did not zoom out")
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ var skill: Node
|
|||
# in a test script has been observed to corrupt method resolution for the
|
||||
# WHOLE script. See tests/test_gas_bomber_cooldowns.gd's identical fix (#60).
|
||||
const BEAM_HALF_WIDTH: float = 38.0
|
||||
# Mirrored for the same reason, and it bites exactly as documented: a bare
|
||||
# DRAG_RADIUS here made every method call in this script
|
||||
# resolve against a plain Node2D.
|
||||
const DRAG_RADIUS: float = 260.0
|
||||
var on_line_enemies: Array = []
|
||||
var off_line_enemy: Node
|
||||
var beyond_range_enemy: Node
|
||||
|
|
@ -120,20 +124,28 @@ func _process(delta: float) -> bool:
|
|||
drag.index = 7
|
||||
drag.position = press.position + Vector2(300.0, 0.0)
|
||||
main._handle_drag(drag)
|
||||
# The aim view eases to its framing rather than cutting to it, so
|
||||
# the drag alone has not moved the camera yet.
|
||||
if not is_equal_approx(main.get_node("Camera2D").zoom.x, original_zoom):
|
||||
return _fail_now("the aim zoom jumped instead of easing")
|
||||
for _step in range(40):
|
||||
skill._process(0.05)
|
||||
if main.get_node("Camera2D").zoom.x >= original_zoom:
|
||||
return _fail_now("aiming did not zoom out")
|
||||
print("aiming zoomed the camera out")
|
||||
print("aiming eased the camera out instead of cutting to it")
|
||||
|
||||
# #203: far enough out to show the WHOLE beam. The drag above is
|
||||
# horizontal, so the screen's narrow half is the binding one.
|
||||
# #203: framed to twice the distance of the chosen target, so what
|
||||
# stands behind it is on screen too. The drag above is horizontal,
|
||||
# so the screen's narrow half is the binding one.
|
||||
var half_screen: Vector2 = skill.get_viewport_rect().size * 0.5 \
|
||||
- Vector2(54.0, 54.0)
|
||||
var framed: float = (2900.0 + BEAM_HALF_WIDTH) \
|
||||
var aimed_distance: float = skill._aim_origin().distance_to(skill.aim_target)
|
||||
var framed: float = (aimed_distance * 2.0 + BEAM_HALF_WIDTH) \
|
||||
* main.get_node("Camera2D").zoom.x
|
||||
if framed > half_screen.x + 1.0:
|
||||
return _fail_now("aiming framed %.0fpx of beam on %.0fpx of screen" \
|
||||
return _fail_now("framed %.0fpx of aim on %.0fpx of screen" \
|
||||
% [framed, half_screen.x])
|
||||
print("aiming framed the beam's full range")
|
||||
print("aiming framed twice the distance to the target")
|
||||
|
||||
# --- B. targets: two on the line, one off it, one past range ---
|
||||
for offset in [Vector2(200.0, 0.0), Vector2(500.0, 0.0)]:
|
||||
|
|
@ -251,6 +263,31 @@ func _process(delta: float) -> bool:
|
|||
return _fail_now("the aim bent toward an enemy outside the assist cone")
|
||||
print("an enemy outside the cone left the aim where it was pointed")
|
||||
|
||||
# --- F. the drag names a DEPTH, so the aim can reach past a
|
||||
# nearer target instead of always settling on the closest one.
|
||||
var near_enemy: Node = load("res://enemies/Enemy.tscn").instantiate()
|
||||
near_enemy.position = origin + Vector2(700.0, 0.0)
|
||||
main.add_child(near_enemy)
|
||||
near_enemy.speed = 0.0
|
||||
# Further out and off to one side, but still inside the assist
|
||||
# cone: without depth this one is unreachable, because the nearer
|
||||
# enemy wins the cone every time.
|
||||
var far_enemy: Node = load("res://enemies/Enemy.tscn").instantiate()
|
||||
far_enemy.position = origin + Vector2(2000.0, 0.0).rotated(deg_to_rad(10.0))
|
||||
main.add_child(far_enemy)
|
||||
far_enemy.speed = 0.0
|
||||
|
||||
var near_drag := Vector2(700.0 / 2900.0 * DRAG_RADIUS, 0.0)
|
||||
var near_pick: Vector2 = skill._calculate_aim_target(near_drag, origin)
|
||||
if near_pick.distance_to(near_enemy.global_position) > 1.0:
|
||||
return _fail_now("a short drag did not settle on the nearer enemy")
|
||||
var far_drag := Vector2(2000.0 / 2900.0 * DRAG_RADIUS, 0.0) \
|
||||
.rotated(deg_to_rad(10.0))
|
||||
var far_pick: Vector2 = skill._calculate_aim_target(far_drag, origin)
|
||||
if far_pick.distance_to(far_enemy.global_position) > 1.0:
|
||||
return _fail_now("dragging further did not reach past the nearer enemy")
|
||||
print("drag depth selects between a near and a far target in the same cone")
|
||||
|
||||
print("TEST_LASER_VESSEL: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
Loading…
Reference in a new issue