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:
Loïc Wisniewski 2026-09-05 09:32:42 +02:00
commit 2239235993
5 changed files with 123 additions and 28 deletions

View file

@ -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:

View file

@ -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: