Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
class_name LaserController
|
|
|
|
|
extends Node2D
|
|
|
|
|
|
|
|
|
|
# Same press-drag-release aiming plumbing as GasBombController, adapted for
|
|
|
|
|
# an instant beam rather than a lobbed, timed projectile: there is no travel
|
|
|
|
|
# time or explosion delay - the beam strikes the instant the shot fires, and
|
|
|
|
|
# only a brief flash lingers for feedback. Damage/range/cooldown are read
|
|
|
|
|
# straight off the firing ship's own VesselLaserComponent (already resolved
|
|
|
|
|
# from vessel_stat/upgrades) rather than hardcoded here, unlike the bomber's
|
|
|
|
|
# AOE_RADIUS/DAMAGE/COOLDOWN.
|
|
|
|
|
#
|
|
|
|
|
# Reused as-is by both Main.tscn and PlanetSurface.tscn (same relative
|
|
|
|
|
# ../Fleet, ../Camera2D, ../UI/LaserButton paths as GasBombController), so
|
|
|
|
|
# it works identically in space and on a landed surface with no branching.
|
|
|
|
|
|
|
|
|
|
const BEAM_COLOR := Color(0.3, 0.9, 1.0)
|
|
|
|
|
const TARGET_COLOR := Color(0.6, 0.95, 1.0)
|
|
|
|
|
const BEAM_HALF_WIDTH := 22.0
|
|
|
|
|
const DRAG_DEADZONE := 22.0
|
|
|
|
|
const FLASH_DURATION := 0.22
|
2026-08-16 19:11:31 +02:00
|
|
|
# #61: ammunition from the fleet hold, mirroring #60's gas bomber exactly.
|
|
|
|
|
const ENERGY_PER_SHOT: int = 8
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
|
|
|
|
|
@export var minimum_zoom: float = 0.4
|
|
|
|
|
@onready var fleet: Node2D = get_node("../Fleet")
|
|
|
|
|
@onready var camera: Camera2D = get_node("../Camera2D")
|
|
|
|
|
@onready var button: IconButton = get_node("../UI/LaserButton")
|
2026-08-16 19:11:31 +02:00
|
|
|
@onready var status: Label = get_node("../UI/LaserStatus")
|
|
|
|
|
|
|
|
|
|
var status_message_until: int = 0
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
|
|
|
|
|
var aiming := false
|
|
|
|
|
var aim_touch := -1
|
|
|
|
|
var aim_start_screen := Vector2.ZERO
|
|
|
|
|
var aim_screen := Vector2.ZERO
|
|
|
|
|
var aim_target := Vector2.ZERO
|
|
|
|
|
var saved_zoom := 1.0
|
|
|
|
|
var restoring_zoom := false
|
|
|
|
|
var beams: Array = []
|
|
|
|
|
var last_hit_count := 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
button.visible = false
|
2026-08-16 19:11:31 +02:00
|
|
|
status.text = ""
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 19:11:31 +02:00
|
|
|
func _can_afford_shot() -> bool:
|
|
|
|
|
return GameState.fleet_resource(GameState.RESOURCE_ENERGY) >= ENERGY_PER_SHOT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _say(message: String) -> void:
|
|
|
|
|
status.text = message
|
|
|
|
|
status_message_until = Time.get_ticks_msec() + 3000
|
|
|
|
|
|
|
|
|
|
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
func _process(delta: float) -> void:
|
|
|
|
|
var had_visuals := aiming or not beams.is_empty()
|
|
|
|
|
button.visible = fleet.has_capability("laser")
|
2026-08-16 19:11:31 +02:00
|
|
|
button.enabled = _next_ready_laser_ship() != null and _can_afford_shot()
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
if not button.visible and aiming:
|
|
|
|
|
_cancel_aim()
|
2026-08-16 19:11:31 +02:00
|
|
|
if status.text != "" and Time.get_ticks_msec() >= status_message_until:
|
|
|
|
|
status.text = ""
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
|
|
|
|
|
if restoring_zoom and not aiming:
|
|
|
|
|
var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0)
|
|
|
|
|
camera.zoom = Vector2(z, z)
|
|
|
|
|
if is_equal_approx(z, saved_zoom):
|
|
|
|
|
restoring_zoom = false
|
|
|
|
|
|
|
|
|
|
for i in range(beams.size() - 1, -1, -1):
|
|
|
|
|
var beam: Dictionary = beams[i]
|
|
|
|
|
beam.time = maxf(float(beam.time) - delta, 0.0)
|
|
|
|
|
if float(beam.time) <= 0.0:
|
|
|
|
|
beams.remove_at(i)
|
|
|
|
|
if aiming or not beams.is_empty() or had_visuals:
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_begin_aim() -> bool:
|
2026-08-16 19:11:31 +02:00
|
|
|
# Deliberately NOT button.enabled: an unaffordable shot still needs the
|
|
|
|
|
# press to reach here so it can explain itself (#60's identical
|
|
|
|
|
# reasoning on the gas bomber). A wholly unready fleet (no laser ship at
|
|
|
|
|
# all) has nothing more useful to say than "greyed out".
|
|
|
|
|
return button.visible and not button._blocked_by_modal() \
|
|
|
|
|
and not aiming and _next_ready_laser_ship() != null
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
|
|
|
if event.pressed:
|
|
|
|
|
if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position):
|
|
|
|
|
return false
|
2026-08-16 19:11:31 +02:00
|
|
|
if not _can_afford_shot():
|
|
|
|
|
_say("Need %d energy in the fleet hold to fire (have %d)" \
|
|
|
|
|
% [ENERGY_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_ENERGY)])
|
|
|
|
|
return true
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
aiming = true
|
|
|
|
|
aim_touch = event.index
|
|
|
|
|
aim_start_screen = event.position
|
|
|
|
|
aim_screen = event.position
|
|
|
|
|
saved_zoom = camera.zoom.x
|
|
|
|
|
restoring_zoom = false
|
|
|
|
|
button.pressed_down = true
|
|
|
|
|
button.queue_redraw()
|
|
|
|
|
_update_aim()
|
|
|
|
|
return true
|
|
|
|
|
if event.index != aim_touch:
|
|
|
|
|
return false
|
|
|
|
|
var should_fire := aim_start_screen.distance_to(aim_screen) >= DRAG_DEADZONE
|
|
|
|
|
if should_fire:
|
|
|
|
|
_fire()
|
|
|
|
|
else:
|
|
|
|
|
_cancel_aim()
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
|
|
|
|
if not aiming or event.index != aim_touch:
|
|
|
|
|
return false
|
|
|
|
|
aim_screen = event.position
|
|
|
|
|
_update_aim()
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _aim_origin() -> Vector2:
|
|
|
|
|
return fleet.get_centroid()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _aim_max_range() -> float:
|
|
|
|
|
var ship: Node2D = _next_ready_laser_ship()
|
|
|
|
|
if ship == null:
|
|
|
|
|
return 950.0
|
|
|
|
|
var component: Node = ship.get_component("laser")
|
|
|
|
|
return component.beam_range if component != null else 950.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_aim() -> void:
|
|
|
|
|
# A beam always reaches its full range - only the DIRECTION is aimed,
|
|
|
|
|
# unlike the bomb's drag-controlled distance.
|
|
|
|
|
var drag := aim_screen - aim_start_screen
|
|
|
|
|
var origin: Vector2 = _aim_origin()
|
|
|
|
|
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
|
|
|
|
aim_target = origin + direction * _aim_max_range()
|
|
|
|
|
_update_aim_zoom(origin, aim_target)
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
|
|
|
|
var displacement := target - origin
|
|
|
|
|
var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
|
|
|
|
|
var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + BEAM_HALF_WIDTH)
|
|
|
|
|
var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + BEAM_HALF_WIDTH)
|
|
|
|
|
var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom)
|
|
|
|
|
camera.zoom = Vector2(desired, desired)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish_aim() -> void:
|
|
|
|
|
aiming = false
|
|
|
|
|
aim_touch = -1
|
|
|
|
|
button.pressed_down = false
|
|
|
|
|
button.queue_redraw()
|
|
|
|
|
restoring_zoom = true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _cancel_aim() -> void:
|
|
|
|
|
_finish_aim()
|
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _fire() -> void:
|
|
|
|
|
var shooter: Node2D = _next_ready_laser_ship()
|
|
|
|
|
if shooter == null:
|
|
|
|
|
_cancel_aim()
|
|
|
|
|
return
|
2026-08-16 19:11:31 +02:00
|
|
|
if not _can_afford_shot():
|
|
|
|
|
_say("Need %d energy in the fleet hold to fire (have %d)" \
|
|
|
|
|
% [ENERGY_PER_SHOT, GameState.fleet_resource(GameState.RESOURCE_ENERGY)])
|
|
|
|
|
_cancel_aim()
|
|
|
|
|
return
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
var component: Node = shooter.get_component("laser")
|
|
|
|
|
if not component.consume_shot():
|
|
|
|
|
_cancel_aim()
|
|
|
|
|
return
|
2026-08-16 19:11:31 +02:00
|
|
|
GameState.withdraw_fleet_resource(GameState.RESOURCE_ENERGY, ENERGY_PER_SHOT)
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
var direction: Vector2 = (aim_target - _aim_origin())
|
|
|
|
|
direction = direction.normalized() if direction.length() > 0.001 else Vector2.UP
|
|
|
|
|
var beam_to: Vector2 = shooter.global_position + direction * component.beam_range
|
|
|
|
|
_strike(shooter.global_position, beam_to, direction, int(component.damage))
|
|
|
|
|
_finish_aim()
|
2026-08-16 19:11:31 +02:00
|
|
|
button.enabled = _next_ready_laser_ship() != null and _can_afford_shot()
|
Add laser ship vessel and its skill shot (#58)
A manually aimed instant beam, modelled on the gas bomber's press-drag-
release aiming (GasBombController/VesselGasBomberComponent) but firing
the moment the touch releases - no projectile travel time. Damage,
beam_range and cooldown are resolved per ship from base_stats/upgrades
(VesselLaserComponent.apply_stats(), the same pattern
VesselWeaponComponent uses) rather than hardcoded in the controller, so
the new damage/range/cooldown ship-lab upgrades actually reach them.
Cooldown lives on the component (per ship), not the controller (per
fleet) - two laser ships give two shots, the exact bug already fixed
for bombers in eb79efd.
LaserController.gd is reused unmodified by both Main.tscn and
PlanetSurface.tscn (relative ../Fleet, ../Camera2D, ../UI/LaserButton
paths, matching GasBombController), so it works identically in space
and on a landed surface. Wired into GameController's touch/drag
first-refusal chain alongside the bomb controller.
The crafter, hangar and ship-lab menus were already generic over
VesselDefinitions.fleet()/.all() - only the per-vessel Main.tscn nodes
are hand-placed, not the logic - so this repacks those rows to
5/5/6 columns and extends BuildController's onready arrays. No new
signal handlers were needed. Also fixes test_vessel_composition.gd's
hardcoded fleet-catalog size again (4 -> 5) now that laser is a fifth
fleet-eligible vessel.
Note for later tickets: a brand-new class_name script (LaserController)
resolves fine as a static type when its own file is the godot -s entry
point, but the identical type hint fails to parse ("Could not find
type") when the script holding it is only reached indirectly - e.g.
Main.tscn loaded from inside a _screenshot_tool.gd preview. Untyped
Node + duck-typed calls sidesteps it; GameController.gd's
laser_controller field documents this in place.
2026-08-16 17:29:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _strike(from: Vector2, to: Vector2, direction: Vector2, damage: int) -> void:
|
|
|
|
|
beams.append({"from": from, "to": to, "time": FLASH_DURATION})
|
|
|
|
|
last_hit_count = 0
|
|
|
|
|
var range_along: float = from.distance_to(to)
|
|
|
|
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
|
|
|
|
if not is_instance_valid(enemy) or not (enemy is Node2D):
|
|
|
|
|
continue
|
|
|
|
|
var to_enemy: Vector2 = enemy.global_position - from
|
|
|
|
|
var along: float = to_enemy.dot(direction)
|
|
|
|
|
if along < 0.0 or along > range_along:
|
|
|
|
|
continue
|
|
|
|
|
var perpendicular: float = (to_enemy - direction * along).length()
|
|
|
|
|
if perpendicular <= BEAM_HALF_WIDTH:
|
|
|
|
|
enemy.take_damage(damage)
|
|
|
|
|
last_hit_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _next_ready_laser_ship() -> Node2D:
|
|
|
|
|
for ship in fleet.ships:
|
|
|
|
|
if not is_instance_valid(ship):
|
|
|
|
|
continue
|
|
|
|
|
var component: Node = ship.get_component("laser")
|
|
|
|
|
if component != null and component.is_ready():
|
|
|
|
|
return ship
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw() -> void:
|
|
|
|
|
if aiming:
|
|
|
|
|
NeonDraw.draw_glowing_line(self, _aim_origin(), aim_target, TARGET_COLOR, 0.7, 0.6)
|
|
|
|
|
for beam in beams:
|
|
|
|
|
var progress: float = 1.0 - float(beam.time) / FLASH_DURATION
|
|
|
|
|
var alpha: float = 1.0 - progress
|
|
|
|
|
NeonDraw.draw_glowing_line(self, beam.from, beam.to, BEAM_COLOR, alpha, 1.2 - progress * 0.6)
|