Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
164 lines
7.3 KiB
GDScript
164 lines
7.3 KiB
GDScript
class_name LaserController
|
|
extends "res://controllers/AimedFleetAbilityController.gd"
|
|
|
|
# The player's end of the laser: the button, the aim, the fleet's magazine.
|
|
# The SHOT itself is a LaserBeamEffect in the world (see that file) - it was
|
|
# lifted out of here so a garrisoned laser vessel could fire the same beam
|
|
# from a different trigger without this controller being involved at all.
|
|
#
|
|
# What is left here is everything that is genuinely about the player firing:
|
|
# where they aimed, what it costs the fleet hold, and the guide line drawn
|
|
# while they aim it.
|
|
|
|
const BEAM_EFFECT = preload("res://vfx/LaserBeamEffect.gd")
|
|
# #201: the beam borrows the fire stick's aim helper, so it is loaded here
|
|
# for its static assist rather than for anything a laser hull carries.
|
|
const WEAPON_COMPONENT = preload("res://vessels/VesselWeaponComponent.gd")
|
|
const BEAM_COLOR := BEAM_EFFECT.BEAM_COLOR
|
|
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
|
|
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
|
|
|
|
# The beams this controller has fired and that are still alive. Kept so the
|
|
# fleet's own shots can be reasoned about - "is anything still on screen" for
|
|
# the redraw, and the tests' view of what a shot did - without this becoming
|
|
# the owner of them again.
|
|
var beams: Array = []
|
|
var last_hit_count: int = 0
|
|
|
|
|
|
func _button_path() -> NodePath: return "../UI/LaserButton"
|
|
func _status_path() -> NodePath: return "../UI/LaserStatus"
|
|
func _component_id() -> String: return "laser"
|
|
func _ammo_resource() -> String: return GameState.RESOURCE_ENERGY
|
|
func _ammo_cost() -> int: return ENERGY_PER_SHOT
|
|
func _aim_padding() -> float: return BEAM_HALF_WIDTH
|
|
func _has_effect_visuals() -> bool: return not beams.is_empty()
|
|
|
|
|
|
func _aim_max_range() -> float:
|
|
# Falls back to the VESSEL DEFINITION's own beam_range, not to a copy of
|
|
# the number. This used to hardcode 950.0 in both branches, which silently
|
|
# became a lie the moment the definition's range changed - the aim line
|
|
# would have been drawn to a different distance than the beam actually
|
|
# reaches, and only in the cases where the fallback fires.
|
|
var ship: Node2D = _next_ready_ship()
|
|
if ship != null:
|
|
var component: Node = ship.get_component(_component_id())
|
|
if component != null:
|
|
return component.beam_range
|
|
return GameState.vessel_stat(GameState.VESSEL_LASER, "beam_range")
|
|
|
|
|
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
|
# A beam always reaches full range; dragging chooses its direction only.
|
|
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
|
return _assisted_target(origin, origin + direction * _aim_max_range())
|
|
|
|
|
|
# The mouse path (#80) names a world point directly instead of dragging one
|
|
# into existence. It has a real cursor, so it is assisted from that rather
|
|
# than from a virtual one at the end of the beam - otherwise the crosshair
|
|
# would be the only aim in the game the helper does not touch.
|
|
func fire_at(world_target: Vector2) -> bool:
|
|
return super(_assisted_target(_aim_origin(), world_target))
|
|
|
|
|
|
# #201: ordinary fire's aim helper, applied to the beam. It answers with a
|
|
# PLACE, not a heading - the same thing the fire stick resolves and every
|
|
# hull then converges on - so a drag that ends near an enemy, or merely
|
|
# inside the assist cone, passes through that enemy instead of a few degrees
|
|
# past it. The beam still runs on to its full range from there.
|
|
func _assisted_target(origin: Vector2, point: Vector2) -> Vector2:
|
|
return WEAPON_COMPONENT.assisted_point(get_tree(), origin, point,
|
|
_aim_max_range())
|
|
|
|
|
|
func _consume_shot(component: Node) -> bool:
|
|
return component.consume_shot()
|
|
|
|
|
|
func _perform_shot(ship: Node2D, component: Node) -> void:
|
|
# The cooldown was already spent by _consume_shot() above, so this fires
|
|
# the effect directly rather than going through fire_ability() and
|
|
# spending it twice.
|
|
# Aimed from the HULL, not from the fleet centre. aim_target is a place
|
|
# now that the assist can move it onto an enemy (#201), and ordinary fire
|
|
# converges on such a place by letting each hull find its own heading to
|
|
# it - a shared heading measured at the centroid would walk this beam off
|
|
# the very target the assist just picked, by the ship's formation offset.
|
|
var direction: Vector2 = aim_target - ship.global_position
|
|
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
|
var effect: Node2D = BEAM_EFFECT.fire(get_tree().root, ship.global_position,
|
|
direction, component.beam_range, int(component.damage))
|
|
beams.append(effect)
|
|
last_hit_count = int(effect.hit_count)
|
|
|
|
|
|
func _next_ready_laser_ship() -> Node2D:
|
|
return _next_ready_ship()
|
|
|
|
|
|
func _advance_effects(_delta: float) -> void:
|
|
# The effects animate and free themselves; this only forgets the ones that
|
|
# have gone.
|
|
beams = beams.filter(func(beam): return is_instance_valid(beam))
|
|
|
|
|
|
func beam_extension(beam: Node2D) -> float:
|
|
return beam.extension()
|
|
|
|
|
|
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.
|
|
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
|
super(origin, origin + _aim_heading(origin, target) * _aim_max_range())
|
|
|
|
|
|
func _aim_heading(origin: Vector2, target: Vector2) -> Vector2:
|
|
var direction: Vector2 = target - origin
|
|
return direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
|
|
|
|
|
func _draw() -> void:
|
|
# Only the aim guide. Each beam draws itself, in its own node.
|
|
if not aiming:
|
|
return
|
|
var origin: Vector2 = _aim_origin()
|
|
var reach: float = _aim_max_range()
|
|
var far_end: Vector2 = origin + _aim_heading(origin, aim_target) * reach
|
|
# Drawn to the far end rather than to aim_target, for the same reason the
|
|
# camera frames it: this is the line the beam will occupy (#203).
|
|
NeonDraw.draw_glowing_line(self, origin, far_end, TARGET_COLOR, 0.7, 0.6)
|
|
# A cross-tick where it stops, so the reach is a mark on the screen rather
|
|
# than a line that merely runs out. Sized in SCREEN pixels: framing 2900
|
|
# units puts the camera far enough out that a mark measured in world units
|
|
# would be a few pixels of nothing at the moment it matters most.
|
|
var across: Vector2 = far_end.direction_to(origin).orthogonal() \
|
|
* _screen_units(END_TICK_PIXELS)
|
|
NeonDraw.draw_glowing_line(self, far_end - across, far_end + across,
|
|
TARGET_COLOR, 0.7, 0.5)
|
|
# What the assist picked, when that is nearer than the far end. The line
|
|
# still runs past it - everything behind it on the same line is hit too.
|
|
if origin.distance_to(aim_target) < reach - 1.0:
|
|
NeonDraw.draw_glowing_circle(self, aim_target,
|
|
maxf(BEAM_HALF_WIDTH, _screen_units(TARGET_RING_PIXELS)),
|
|
TARGET_COLOR, 0.7)
|
|
|
|
|
|
# World units that cover a given number of screen pixels at the camera's
|
|
# current zoom.
|
|
func _screen_units(pixels: float) -> float:
|
|
return pixels / maxf(camera.zoom.x, 0.001)
|