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.
This commit is contained in:
Loïc Wisniewski 2026-08-16 17:29:53 +02:00
commit ed46cd77e6
18 changed files with 858 additions and 64 deletions

View file

@ -31,13 +31,13 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
@onready var lab_vessel_buttons: Array = [ @onready var lab_vessel_buttons: Array = [
get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"), get_node("../UI/LabMenu/VesselA"), get_node("../UI/LabMenu/VesselB"),
get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"), get_node("../UI/LabMenu/VesselC"), get_node("../UI/LabMenu/VesselD"),
get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselE"), get_node("../UI/LabMenu/VesselF"),
] ]
@onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel") @onready var lab_vessel_label: Label = get_node("../UI/LabMenu/VesselLabel")
@onready var lab_vessel_labels: Array = [ @onready var lab_vessel_labels: Array = [
get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"), get_node("../UI/LabMenu/VesselALabel"), get_node("../UI/LabMenu/VesselBLabel"),
get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"), get_node("../UI/LabMenu/VesselCLabel"), get_node("../UI/LabMenu/VesselDLabel"),
get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselELabel"), get_node("../UI/LabMenu/VesselFLabel"),
] ]
@onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel") @onready var lab_detail: Label = get_node("../UI/LabMenu/DetailLabel")
@onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel") @onready var lab_status: Label = get_node("../UI/LabMenu/StatusLabel")
@ -66,24 +66,28 @@ const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
get_node("../UI/HangarMenu/AvailableShield"), get_node("../UI/HangarMenu/AvailableShield"),
get_node("../UI/HangarMenu/AvailableGasBomber"), get_node("../UI/HangarMenu/AvailableGasBomber"),
get_node("../UI/HangarMenu/AvailableCarrier"), get_node("../UI/HangarMenu/AvailableCarrier"),
get_node("../UI/HangarMenu/AvailableLaser"),
] ]
@onready var hangar_available_labels: Array = [ @onready var hangar_available_labels: Array = [
get_node("../UI/HangarMenu/AvailableInterceptorLabel"), get_node("../UI/HangarMenu/AvailableInterceptorLabel"),
get_node("../UI/HangarMenu/AvailableShieldLabel"), get_node("../UI/HangarMenu/AvailableShieldLabel"),
get_node("../UI/HangarMenu/AvailableGasBomberLabel"), get_node("../UI/HangarMenu/AvailableGasBomberLabel"),
get_node("../UI/HangarMenu/AvailableCarrierLabel"), get_node("../UI/HangarMenu/AvailableCarrierLabel"),
get_node("../UI/HangarMenu/AvailableLaserLabel"),
] ]
@onready var hangar_fleet_slots: Array = [ @onready var hangar_fleet_slots: Array = [
get_node("../UI/HangarMenu/FleetInterceptor"), get_node("../UI/HangarMenu/FleetInterceptor"),
get_node("../UI/HangarMenu/FleetShield"), get_node("../UI/HangarMenu/FleetShield"),
get_node("../UI/HangarMenu/FleetGasBomber"), get_node("../UI/HangarMenu/FleetGasBomber"),
get_node("../UI/HangarMenu/FleetCarrier"), get_node("../UI/HangarMenu/FleetCarrier"),
get_node("../UI/HangarMenu/FleetLaser"),
] ]
@onready var hangar_fleet_labels: Array = [ @onready var hangar_fleet_labels: Array = [
get_node("../UI/HangarMenu/FleetInterceptorLabel"), get_node("../UI/HangarMenu/FleetInterceptorLabel"),
get_node("../UI/HangarMenu/FleetShieldLabel"), get_node("../UI/HangarMenu/FleetShieldLabel"),
get_node("../UI/HangarMenu/FleetGasBomberLabel"), get_node("../UI/HangarMenu/FleetGasBomberLabel"),
get_node("../UI/HangarMenu/FleetCarrierLabel"), get_node("../UI/HangarMenu/FleetCarrierLabel"),
get_node("../UI/HangarMenu/FleetLaserLabel"),
] ]
var build_menu_entries: Array = [] var build_menu_entries: Array = []
@ -111,11 +115,15 @@ var lab_building_id: int = 0
@onready var craft_gas_bomber_label: Label = get_node("../UI/CraftMenu/GasBomberLabel") @onready var craft_gas_bomber_label: Label = get_node("../UI/CraftMenu/GasBomberLabel")
@onready var craft_carrier_button: IconButton = get_node("../UI/CraftMenu/CarrierButton") @onready var craft_carrier_button: IconButton = get_node("../UI/CraftMenu/CarrierButton")
@onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel") @onready var craft_carrier_label: Label = get_node("../UI/CraftMenu/CarrierLabel")
@onready var craft_laser_button: IconButton = get_node("../UI/CraftMenu/LaserButton")
@onready var craft_laser_label: Label = get_node("../UI/CraftMenu/LaserLabel")
@onready var craft_vessel_buttons: Array = [ @onready var craft_vessel_buttons: Array = [
craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button, craft_interceptor_button, craft_shield_button, craft_gas_bomber_button, craft_carrier_button,
craft_laser_button,
] ]
@onready var craft_vessel_labels: Array = [ @onready var craft_vessel_labels: Array = [
craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label, craft_interceptor_label, craft_shield_label, craft_gas_bomber_label, craft_carrier_label,
craft_laser_label,
] ]
# --- the transfer panel (#24) --------------------------------------------- # --- the transfer panel (#24) ---------------------------------------------

View file

@ -22,6 +22,12 @@ extends Node2D
@onready var pivot_cw_button: IconButton = get_node_or_null("UI/PivotCWButton") @onready var pivot_cw_button: IconButton = get_node_or_null("UI/PivotCWButton")
@onready var pivot_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton") @onready var pivot_ccw_button: IconButton = get_node_or_null("UI/PivotCCWButton")
@onready var gas_bomb_controller: GasBombController = get_node_or_null("GasBombController") @onready var gas_bomb_controller: GasBombController = get_node_or_null("GasBombController")
# Untyped on purpose: LaserController resolves fine as a static type when a
# script IS the -s entry point, but the same class_name reference 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 - for a class_name this new. Node + duck-typed calls sidesteps it.
@onready var laser_controller: Node = get_node_or_null("LaserController")
var touches: Dictionary = {} var touches: Dictionary = {}
var pinch_active: bool = false var pinch_active: bool = false
@ -255,6 +261,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
# else on screen. # else on screen.
if gas_bomb_controller != null and gas_bomb_controller.handle_touch(event): if gas_bomb_controller != null and gas_bomb_controller.handle_touch(event):
return return
if laser_controller != null and laser_controller.handle_touch(event):
return
if _handle_pivot_touch(event): if _handle_pivot_touch(event):
return return
if joystick != null and joystick.visible and joystick.handle_touch(event): if joystick != null and joystick.visible and joystick.handle_touch(event):
@ -277,6 +285,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
func _handle_drag(event: InputEventScreenDrag) -> void: func _handle_drag(event: InputEventScreenDrag) -> void:
if gas_bomb_controller != null and gas_bomb_controller.handle_drag(event): if gas_bomb_controller != null and gas_bomb_controller.handle_drag(event):
return return
if laser_controller != null and laser_controller.handle_drag(event):
return
if joystick != null and joystick.visible and joystick.handle_drag(event): if joystick != null and joystick.visible and joystick.handle_drag(event):
return return
if not touches.has(event.index): if not touches.has(event.index):

View file

@ -464,6 +464,7 @@ const VESSEL_SHIELD: String = "shield"
const VESSEL_GAS_BOMBER: String = "gas_bomber" const VESSEL_GAS_BOMBER: String = "gas_bomber"
const VESSEL_TRANSPORTER: String = "transporter" const VESSEL_TRANSPORTER: String = "transporter"
const VESSEL_CARRIER: String = "carrier" const VESSEL_CARRIER: String = "carrier"
const VESSEL_LASER: String = "laser"
# Compatibility constants for callers and old tests. Runtime metadata is # Compatibility constants for callers and old tests. Runtime metadata is
# resolved from VesselDefinition below. # resolved from VesselDefinition below.

View file

@ -38,6 +38,9 @@ enum Icon {
# Same append-only rule (#57): a wide hauler hull, distinct from every # Same append-only rule (#57): a wide hauler hull, distinct from every
# vessel glyph above. # vessel glyph above.
CARRIER_VESSEL, CARRIER_VESSEL,
# #58: LASER_VESSEL is the ship glyph, LASER_SHOT the action button -
# same two-icon split as GAS_BOMBER/GAS_BOMB above.
LASER_VESSEL, LASER_SHOT,
} }
# Size the glyphs were originally drawn against. Every glyph dimension is # Size the glyphs were originally drawn against. Every glyph dimension is
@ -232,6 +235,10 @@ func _draw() -> void:
_draw_gas_burner_icon(center, pulse, s) _draw_gas_burner_icon(center, pulse, s)
Icon.CARRIER_VESSEL: Icon.CARRIER_VESSEL:
_draw_carrier(center, pulse, s) _draw_carrier(center, pulse, s)
Icon.LASER_VESSEL:
_draw_laser_vessel(center, pulse, s)
Icon.LASER_SHOT:
_draw_laser_shot(center, pulse, s)
func _draw_close(center: Vector2, pulse: float, s: float) -> void: func _draw_close(center: Vector2, pulse: float, s: float) -> void:
@ -270,6 +277,26 @@ func _draw_carrier(center: Vector2, pulse: float, s: float) -> void:
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color) NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
func _draw_laser_vessel(center: Vector2, pulse: float, s: float) -> void:
# A slim dart rather than the interceptor's plain triangle - the notch at
# the nose reads as a fixed forward-facing emitter, not a hull point.
var hull := PackedVector2Array([
center + Vector2(0, -13) * s,
center + Vector2(-8, 9) * s,
center + Vector2(-3, 6) * s,
center + Vector2(3, 6) * s,
center + Vector2(8, 9) * s,
])
NeonDraw.draw_glowing_polygon(self, hull, _active_color(), pulse, core_color)
func _draw_laser_shot(center: Vector2, pulse: float, s: float) -> void:
var from: Vector2 = center + Vector2(-9, 9) * s
var to: Vector2 = center + Vector2(9, -9) * s
NeonDraw.draw_glowing_line(self, from, to, _active_color(), pulse, 1.1, core_color)
NeonDraw.draw_glowing_circle(self, to, 3.0 * s, _active_color(), pulse, core_color)
func _draw_gas_bomber(center: Vector2, pulse: float, s: float) -> void: func _draw_gas_bomber(center: Vector2, pulse: float, s: float) -> void:
var hull := PackedVector2Array([ var hull := PackedVector2Array([
center + Vector2(0, -12) * s, center + Vector2(0, -12) * s,

197
LaserController.gd Normal file
View file

@ -0,0 +1,197 @@
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
@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")
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
queue_redraw()
func _process(delta: float) -> void:
var had_visuals := aiming or not beams.is_empty()
button.visible = fleet.has_capability("laser")
button.enabled = _next_ready_laser_ship() != null
if not button.visible and aiming:
_cancel_aim()
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:
return button.visible and button.enabled and not button._blocked_by_modal() \
and not aiming
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
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
var component: Node = shooter.get_component("laser")
if not component.consume_shot():
_cancel_aim()
return
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()
button.enabled = _next_ready_laser_ship() != null
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)

11
LaserCooldownUpgrade.tres Normal file
View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
upgrade_id = "lsr_cooldown"
display_name = "Capacitor Bank"
description = "Recharges 0.6s sooner per level."
base_cost = 30
max_level = 3
icon = 20
additive_modifiers = {"beam_cooldown": -0.6}

11
LaserDamageUpgrade.tres Normal file
View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
upgrade_id = "lsr_damage"
display_name = "Focusing Lens"
description = "+1 damage per shot."
base_cost = 30
max_level = 3
icon = 15
additive_modifiers = {"damage": 1.0}

11
LaserRangeUpgrade.tres Normal file
View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://VesselUpgradeDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
upgrade_id = "lsr_range"
display_name = "Beam Collimator"
description = "+150 beam range per level."
base_cost = 25
max_level = 3
icon = 17
additive_modifiers = {"beam_range": 150.0}

View file

@ -0,0 +1,29 @@
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=6 format=3]
[ext_resource type="Script" path="res://VesselDefinition.gd" id="1"]
[ext_resource type="PackedScene" path="res://VesselLaserComponent.tscn" id="2"]
[ext_resource type="Resource" path="res://LaserDamageUpgrade.tres" id="3"]
[ext_resource type="Resource" path="res://LaserRangeUpgrade.tres" id="4"]
[ext_resource type="Resource" path="res://LaserCooldownUpgrade.tres" id="5"]
[resource]
script = ExtResource("1")
definition_id = "laser"
display_name = "Laser Ship"
description = "Manually aimed beam vessel. No ammo, but the emitter needs to cool down between shots."
build_cost = 12
build_resource_costs = {"ore": 12, "energy": 6}
build_time = 14.0
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
hull_points = PackedVector2Array(0, -26, -9, 14, -4, 10, 4, 10, 9, 14)
fleet_eligible = true
capabilities = PackedStringArray("laser")
component_scenes = Array[PackedScene]([ExtResource("2")])
base_stats = {"max_speed": 440.0, "acceleration": 420.0, "delivery_speed": 180.0, "damage": 2.0, "beam_range": 950.0, "beam_cooldown": 4.0}
minimum_stats = {"beam_cooldown": 1.5}
upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5")])
unlock_id = "unlock_laser"
unlock_name = "Unlock Laser Ship"
unlock_description = "Research a directed-energy hull for the ship crafter - manually aimed, no ammo, cools down between shots."
unlock_cost = 0
unlock_resource_costs = {"energy": 40}
unlock_icon = -1

232
Main.tscn
View file

@ -1,4 +1,4 @@
[gd_scene load_steps=27 format=3] [gd_scene load_steps=28 format=3]
[ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Starfield.gd" id="1"]
[ext_resource type="Script" path="res://Fleet.gd" id="2"] [ext_resource type="Script" path="res://Fleet.gd" id="2"]
@ -25,6 +25,7 @@
[ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"] [ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"]
[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] [ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"]
[ext_resource type="Script" path="res://GasBombController.gd" id="29"] [ext_resource type="Script" path="res://GasBombController.gd" id="29"]
[ext_resource type="Script" path="res://LaserController.gd" id="30"]
[sub_resource type="ShaderMaterial" id="1"] [sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("5") shader = ExtResource("5")
@ -56,6 +57,9 @@ script = ExtResource("2")
[node name="GasBombController" type="Node2D" parent="."] [node name="GasBombController" type="Node2D" parent="."]
script = ExtResource("29") script = ExtResource("29")
[node name="LaserController" type="Node2D" parent="."]
script = ExtResource("30")
[node name="Camera2D" type="Camera2D" parent="."] [node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(0, 0) position = Vector2(0, 0)
zoom = Vector2(1, 1) zoom = Vector2(1, 1)
@ -217,6 +221,21 @@ offset_top = -424
offset_bottom = -24 offset_bottom = -24
script = ExtResource("13") script = ExtResource("13")
[node name="LaserButton" type="Control" parent="UI"]
visible = false
mouse_filter = 2
anchor_left = 1.0
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -344
offset_right = -196
offset_top = -344
offset_bottom = -196
script = ExtResource("10")
icon = 36
glow_color = Color(0.3, 0.9, 1, 1)
[node name="PivotCCWButton" type="Control" parent="UI"] [node name="PivotCCWButton" type="Control" parent="UI"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0
@ -820,7 +839,7 @@ horizontal_alignment = 1
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -290 offset_left = -290
offset_right = -178 offset_right = -198
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -831,7 +850,7 @@ mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -330 offset_left = -330
offset_right = -138 offset_right = -158
offset_top = 380 offset_top = 380
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -840,8 +859,8 @@ horizontal_alignment = 1
[node name="AvailableShield" type="Control" parent="UI/HangarMenu"] [node name="AvailableShield" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -134 offset_left = -168
offset_right = -22 offset_right = -76
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -852,8 +871,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -174 offset_left = -208
offset_right = 18 offset_right = -36
offset_top = 380 offset_top = 380
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -862,8 +881,8 @@ horizontal_alignment = 1
[node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"] [node name="AvailableGasBomber" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 22 offset_left = -46
offset_right = 134 offset_right = 46
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -874,8 +893,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -18 offset_left = -86
offset_right = 174 offset_right = 86
offset_top = 380 offset_top = 380
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -884,8 +903,8 @@ horizontal_alignment = 1
[node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"] [node name="AvailableCarrier" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 178 offset_left = 76
offset_right = 290 offset_right = 168
offset_top = 255 offset_top = 255
offset_bottom = 375 offset_bottom = 375
script = ExtResource("10") script = ExtResource("10")
@ -896,7 +915,29 @@ glow_color = Color(1, 0.78, 0.3, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 138 offset_left = 36
offset_right = 208
offset_top = 380
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="AvailableLaser" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 198
offset_right = 290
offset_top = 255
offset_bottom = 375
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="AvailableLaserLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 158
offset_right = 330 offset_right = 330
offset_top = 380 offset_top = 380
offset_bottom = 460 offset_bottom = 460
@ -916,7 +957,7 @@ horizontal_alignment = 1
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -290 offset_left = -290
offset_right = -178 offset_right = -198
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -927,7 +968,7 @@ mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -330 offset_left = -330
offset_right = -138 offset_right = -158
offset_top = 690 offset_top = 690
offset_bottom = 770 offset_bottom = 770
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -936,8 +977,8 @@ horizontal_alignment = 1
[node name="FleetShield" type="Control" parent="UI/HangarMenu"] [node name="FleetShield" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -134 offset_left = -168
offset_right = -22 offset_right = -76
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -948,8 +989,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -174 offset_left = -208
offset_right = 18 offset_right = -36
offset_top = 690 offset_top = 690
offset_bottom = 770 offset_bottom = 770
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -958,8 +999,8 @@ horizontal_alignment = 1
[node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"] [node name="FleetGasBomber" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 22 offset_left = -46
offset_right = 134 offset_right = 46
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -970,8 +1011,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -18 offset_left = -86
offset_right = 174 offset_right = 86
offset_top = 690 offset_top = 690
offset_bottom = 770 offset_bottom = 770
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -980,8 +1021,8 @@ horizontal_alignment = 1
[node name="FleetCarrier" type="Control" parent="UI/HangarMenu"] [node name="FleetCarrier" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 178 offset_left = 76
offset_right = 290 offset_right = 168
offset_top = 565 offset_top = 565
offset_bottom = 685 offset_bottom = 685
script = ExtResource("10") script = ExtResource("10")
@ -992,7 +1033,29 @@ glow_color = Color(1, 0.78, 0.3, 1)
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 138 offset_left = 36
offset_right = 208
offset_top = 690
offset_bottom = 770
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
[node name="FleetLaser" type="Control" parent="UI/HangarMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 198
offset_right = 290
offset_top = 565
offset_bottom = 685
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="FleetLaserLabel" type="Label" parent="UI/HangarMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 158
offset_right = 330 offset_right = 330
offset_top = 690 offset_top = 690
offset_bottom = 770 offset_bottom = 770
@ -1044,7 +1107,7 @@ script = ExtResource("24")
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -300 offset_left = -300
offset_right = -212 offset_right = -230
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -1054,8 +1117,8 @@ glow_color = Color(0.25, 0.85, 1, 1)
[node name="VesselB" type="Control" parent="UI/LabMenu"] [node name="VesselB" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -172 offset_left = -194
offset_right = -84 offset_right = -124
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -1065,8 +1128,8 @@ glow_color = Color(0.45, 0.8, 1, 1)
[node name="VesselC" type="Control" parent="UI/LabMenu"] [node name="VesselC" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -44 offset_left = -88
offset_right = 44 offset_right = -18
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -1076,8 +1139,8 @@ glow_color = Color(0.96, 0.32, 0.78, 1)
[node name="VesselD" type="Control" parent="UI/LabMenu"] [node name="VesselD" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 84 offset_left = 18
offset_right = 172 offset_right = 88
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
@ -1087,14 +1150,25 @@ glow_color = Color(0.95, 0.55, 0.85, 1)
[node name="VesselE" type="Control" parent="UI/LabMenu"] [node name="VesselE" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 212 offset_left = 124
offset_right = 300 offset_right = 194
offset_top = 274 offset_top = 274
offset_bottom = 394 offset_bottom = 394
script = ExtResource("10") script = ExtResource("10")
icon = 34 icon = 34
glow_color = Color(1, 0.78, 0.3, 1) glow_color = Color(1, 0.78, 0.3, 1)
[node name="VesselF" type="Control" parent="UI/LabMenu"]
anchor_left = 0.5
anchor_right = 0.5
offset_left = 230
offset_right = 300
offset_top = 274
offset_bottom = 394
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="VesselLabel" type="Label" parent="UI/LabMenu"] [node name="VesselLabel" type="Label" parent="UI/LabMenu"]
visible = false visible = false
mouse_filter = 2 mouse_filter = 2
@ -1112,7 +1186,7 @@ mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -330 offset_left = -330
offset_right = -182 offset_right = -200
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -1123,8 +1197,8 @@ text = "Interceptor"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -202 offset_left = -224
offset_right = -54 offset_right = -94
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -1135,8 +1209,8 @@ text = "Shield"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = -74 offset_left = -118
offset_right = 74 offset_right = 12
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -1147,8 +1221,8 @@ text = "Gas Bomber"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 54 offset_left = 12
offset_right = 202 offset_right = 118
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
@ -1159,14 +1233,26 @@ text = "Transporter"
mouse_filter = 2 mouse_filter = 2
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_left = 182 offset_left = 94
offset_right = 330 offset_right = 224
offset_top = 400 offset_top = 400
offset_bottom = 460 offset_bottom = 460
theme_override_font_sizes/font_size = 22 theme_override_font_sizes/font_size = 22
horizontal_alignment = 1 horizontal_alignment = 1
text = "Carrier" text = "Carrier"
[node name="VesselFLabel" type="Label" parent="UI/LabMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = 200
offset_right = 330
offset_top = 400
offset_bottom = 460
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
text = "Laser Ship"
[node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"] [node name="VesselUpgradeSeparator" type="ColorRect" parent="UI/LabMenu"]
mouse_filter = 2 mouse_filter = 2
anchor_right = 1.0 anchor_right = 1.0
@ -1412,7 +1498,7 @@ anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -310 offset_left = -310
offset_right = -208 offset_right = -232
offset_top = -100 offset_top = -100
offset_bottom = 48 offset_bottom = 48
script = ExtResource("10") script = ExtResource("10")
@ -1425,7 +1511,7 @@ anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -330 offset_left = -330
offset_right = -188 offset_right = -212
offset_top = 56 offset_top = 56
offset_bottom = 136 offset_bottom = 136
theme_override_font_sizes/font_size = 24 theme_override_font_sizes/font_size = 24
@ -1437,8 +1523,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -174 offset_left = -202
offset_right = -72 offset_right = -124
offset_top = -100 offset_top = -100
offset_bottom = 48 offset_bottom = 48
script = ExtResource("10") script = ExtResource("10")
@ -1451,8 +1537,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -194 offset_left = -222
offset_right = -52 offset_right = -104
offset_top = 56 offset_top = 56
offset_bottom = 136 offset_bottom = 136
theme_override_font_sizes/font_size = 24 theme_override_font_sizes/font_size = 24
@ -1465,8 +1551,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -38 offset_left = -94
offset_right = 64 offset_right = -16
offset_top = -100 offset_top = -100
offset_bottom = 28 offset_bottom = 28
script = ExtResource("10") script = ExtResource("10")
@ -1479,8 +1565,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -58 offset_left = -114
offset_right = 84 offset_right = 4
offset_top = 56 offset_top = 56
offset_bottom = 150 offset_bottom = 150
theme_override_font_sizes/font_size = 24 theme_override_font_sizes/font_size = 24
@ -1493,8 +1579,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = 98 offset_left = 14
offset_right = 200 offset_right = 92
offset_top = -100 offset_top = -100
offset_bottom = 48 offset_bottom = 48
script = ExtResource("10") script = ExtResource("10")
@ -1507,8 +1593,8 @@ anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = 78 offset_left = -6
offset_right = 220 offset_right = 112
offset_top = 56 offset_top = 56
offset_bottom = 136 offset_bottom = 136
theme_override_font_sizes/font_size = 24 theme_override_font_sizes/font_size = 24
@ -1516,6 +1602,34 @@ autowrap_mode = 2
text = "Carrier" text = "Carrier"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="LaserButton" type="Control" parent="UI/CraftMenu"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 122
offset_right = 200
offset_top = -100
offset_bottom = 48
script = ExtResource("10")
icon = 35
glow_color = Color(0.3, 0.9, 1, 1)
[node name="LaserLabel" type="Label" parent="UI/CraftMenu"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = 102
offset_right = 220
offset_top = 56
offset_bottom = 136
theme_override_font_sizes/font_size = 24
autowrap_mode = 2
text = "Laser Ship"
horizontal_alignment = 1
[node name="QueueLabel" type="Label" parent="UI/CraftMenu"] [node name="QueueLabel" type="Label" parent="UI/CraftMenu"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=14 format=3] [gd_scene load_steps=15 format=3]
[ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://GameController.gd" id="1"]
[ext_resource type="Script" path="res://LunarBackground.gd" id="2"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"]
@ -9,6 +9,7 @@
[ext_resource type="Script" path="res://TouchJoystick.gd" id="7"] [ext_resource type="Script" path="res://TouchJoystick.gd" id="7"]
[ext_resource type="Script" path="res://ResourceReadout.gd" id="9"] [ext_resource type="Script" path="res://ResourceReadout.gd" id="9"]
[ext_resource type="Script" path="res://GasBombController.gd" id="14"] [ext_resource type="Script" path="res://GasBombController.gd" id="14"]
[ext_resource type="Script" path="res://LaserController.gd" id="16"]
[ext_resource type="Script" path="res://CaptureLabel.gd" id="15"] [ext_resource type="Script" path="res://CaptureLabel.gd" id="15"]
[sub_resource type="ShaderMaterial" id="1"] [sub_resource type="ShaderMaterial" id="1"]
@ -45,6 +46,9 @@ ship_core_color = Color(0.02, 0.09, 0.15, 1)
[node name="GasBombController" type="Node2D" parent="."] [node name="GasBombController" type="Node2D" parent="."]
script = ExtResource("14") script = ExtResource("14")
[node name="LaserController" type="Node2D" parent="."]
script = ExtResource("16")
[node name="Camera2D" type="Camera2D" parent="."] [node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(0, 0) position = Vector2(0, 0)
zoom = Vector2(1, 1) zoom = Vector2(1, 1)
@ -153,6 +157,22 @@ script = ExtResource("7")
glow_color = Color(0.06, 0.32, 0.5, 1) glow_color = Color(0.06, 0.32, 0.5, 1)
core_color = Color(0.02, 0.09, 0.15, 1) core_color = Color(0.02, 0.09, 0.15, 1)
[node name="LaserButton" type="Control" parent="UI"]
visible = false
mouse_filter = 2
anchor_left = 1.0
anchor_right = 1.0
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -344
offset_right = -196
offset_top = -344
offset_bottom = -196
script = ExtResource("6")
icon = 36
glow_color = Color(0.3, 0.9, 1, 1)
core_color = Color(0.02, 0.09, 0.15, 1)
[node name="PivotCCWButton" type="Control" parent="UI"] [node name="PivotCCWButton" type="Control" parent="UI"]
mouse_filter = 2 mouse_filter = 2
anchor_left = 1.0 anchor_left = 1.0

View file

@ -1,13 +1,14 @@
class_name VesselDefinitions class_name VesselDefinitions
extends RefCounted extends RefCounted
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier"] const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser"]
const DEFINITIONS: Dictionary = { const DEFINITIONS: Dictionary = {
"interceptor": preload("res://InterceptorVesselDefinition.tres"), "interceptor": preload("res://InterceptorVesselDefinition.tres"),
"shield": preload("res://ShieldVesselDefinition.tres"), "shield": preload("res://ShieldVesselDefinition.tres"),
"gas_bomber": preload("res://GasBomberVesselDefinition.tres"), "gas_bomber": preload("res://GasBomberVesselDefinition.tres"),
"transporter": preload("res://TransporterVesselDefinition.tres"), "transporter": preload("res://TransporterVesselDefinition.tres"),
"carrier": preload("res://CarrierVesselDefinition.tres"), "carrier": preload("res://CarrierVesselDefinition.tres"),
"laser": preload("res://LaserVesselDefinition.tres"),
} }
static func get_definition(definition_id: String) -> Resource: static func get_definition(definition_id: String) -> Resource:

100
VesselLaserComponent.gd Normal file
View file

@ -0,0 +1,100 @@
class_name VesselLaserComponent
extends Node2D
# Same shape as VesselGasBomberComponent (per-ship cooldown, ready/cooling
# hull fill) - #58's own note: two laser vessels must give two shots, so
# this lives on the SHIP, not the fleet-wide LaserController.
#
# Unlike the gas bomber, damage/beam_range/cooldown are resolved from the
# vessel's own base_stats/upgrades (apply_stats(), the same pattern
# VesselWeaponComponent uses) rather than hardcoded in the controller, so
# ship-lab upgrades actually reach them.
var capability_id := "laser"
var damage: int = 2
var beam_range: float = 950.0
var cooldown_duration: float = 4.0
var cooldown_left: float = 0.0
const READY_COLOR := Color(0.3, 0.9, 1.0)
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
func setup() -> void:
# Keep the ship's normal neon outline above the charge fill.
show_behind_parent = true
queue_redraw()
func apply_stats() -> void:
var ship: Node = get_parent()
var state: Node = ship.get_tree().root.get_node("GameState")
damage = int(state.vessel_stat(ship.vessel_type, "damage"))
beam_range = state.vessel_stat(ship.vessel_type, "beam_range")
cooldown_duration = state.vessel_stat(ship.vessel_type, "beam_cooldown")
queue_redraw()
func is_ready() -> bool:
return cooldown_left <= 0.0
func consume_shot() -> bool:
if not is_ready():
return false
cooldown_left = maxf(cooldown_duration, 0.001)
queue_redraw()
return true
func tick(delta: float) -> void:
if cooldown_left <= 0.0:
return
cooldown_left = maxf(cooldown_left - delta, 0.0)
queue_redraw()
func cooldown_progress() -> float:
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array:
if points.size() < 3 or progress <= 0.001:
return PackedVector2Array()
if progress >= 0.999:
return points.duplicate()
var top := INF
var bottom := -INF
for point in points:
top = minf(top, point.y)
bottom = maxf(bottom, point.y)
var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0))
var clipped := PackedVector2Array()
var previous: Vector2 = points[-1]
var previous_inside := previous.y >= fill_y
for current in points:
var current_inside := current.y >= fill_y
if current_inside != previous_inside:
var amount: float = (fill_y - previous.y) / (current.y - previous.y)
clipped.append(previous.lerp(current, amount))
if current_inside:
clipped.append(current)
previous = current
previous_inside = current_inside
return clipped
func _draw() -> void:
var parent_ship: Node = get_parent()
if parent_ship == null or parent_ship.definition == null:
return
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
var hull: PackedVector2Array = parent_ship.definition.hull_points
if is_ready():
draw_colored_polygon(hull, Color(READY_COLOR, 0.42))
NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75)
return
draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58))
var filled := _filled_hull(hull, cooldown_progress())
if filled.size() >= 3:
draw_colored_polygon(filled, Color(READY_COLOR, 0.68))

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://VesselLaserComponent.gd" id="1"]
[node name="LaserComponent" type="Node2D"]
script = ExtResource("1")

View file

@ -0,0 +1,49 @@
extends Node2D
# Preview, not a test - #58's aim-state screenshot: two laser ships (one
# ready, one rearming), aiming with the beam preview drawn and enemies
# placed along its line.
#
# bash tests/screenshot.sh res://tests/_laser_aim_preview.tscn /tmp/laser.png 20 720x1280
func _ready() -> void:
GameState.buildings.clear()
GameState.extractors.clear()
GameState.transporters.clear()
GameState.vessel_levels["unlock_laser"] = 1
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
main.get_node("BackgroundLayer").visible = false
main.get_node("AsteroidField").visible = false
main.get_node("PlanetField").visible = false
main.get_node("BuildingField").visible = false
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
var fleet: Node = main.get_node("Fleet")
fleet.spawn_ships(4, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_LASER, GameState.VESSEL_LASER]))
var skill: Node = main.get_node("LaserController")
skill._process(0.0)
# Show both per-ship states in one frame: one laser ship rearming, the
# other ready and supplying the shot.
var rearming: Node = fleet.ships[2].get_component("laser")
rearming.consume_shot()
rearming.tick(rearming.cooldown_duration * 0.5)
main.get_node("Camera2D").zoom = Vector2(1.8, 1.8)
var button: Control = main.get_node("UI/LaserButton")
var press := InputEventScreenTouch.new()
press.index = 9
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]:
var enemy: Node = load("res://Enemy.tscn").instantiate()
enemy.position = fleet.get_centroid().lerp(skill.aim_target, t)
main.add_child(enemy)
enemy.speed = 0.0 # after add_child - _ready() sets it from the definition

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_laser_aim_preview.gd" id="1"]
[node name="LaserAimPreview" type="Node2D"]
script = ExtResource("1")

193
tests/test_laser_vessel.gd Normal file
View file

@ -0,0 +1,193 @@
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_laser_vessel.gd
# Forgejo #58: a manually aimed instant beam, modelled closely on the gas
# bomber's press-drag-release skill shot (GasBombController/
# VesselGasBomberComponent) but firing the instant the touch releases -
# there is no projectile travel time to wait out.
#
# Covers:
# A. the control is absent with no laser ship in the fleet, and appears
# the moment one joins
# B. the beam damages every enemy along its line, within beam_half_width,
# and nothing off it or beyond its range
# C. cooldown is per SHIP, not per fleet: two laser ships fire two shots
# before the control disables (the exact bug fixed in eb79efd for
# bombers)
# D. the camera zooms out while aiming and restores afterward
var game_state: Node
var main: Node
var fleet: Node
var skill: LaserController
var on_line_enemies: Array = []
var off_line_enemy: Node
var beyond_range_enemy: Node
var first_shooter: Node2D
var second_shooter: Node2D
var stage: int = 0
var elapsed: float = 0.0
var original_zoom: float = 2.5
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.reset_progression()
# Direct unlock, same shortcut unlock_vessel() itself uses internally -
# this ticket is about the laser's own behaviour, not re-proving the
# ship-lab research flow (already covered generically elsewhere).
game_state.vessel_levels["unlock_laser"] = 1
main = load("res://Main.tscn").instantiate()
root.add_child(main)
fleet = main.get_node("Fleet")
skill = main.get_node("LaserController")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
# --- A. absent, then appears -------------------------------------
if main.get_node("UI/LaserButton").visible:
return _fail_now("laser control appeared without a laser ship")
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_LASER, GameState.VESSEL_LASER]))
main.get_node("Camera2D").zoom = Vector2(original_zoom, original_zoom)
skill._process(0.0)
if not main.get_node("UI/LaserButton").visible:
return _fail_now("a laser ship in the fleet did not reveal the laser control")
print("control hidden with no laser ship, visible once one joins")
# The beam fires FROM the specific ship LaserController picks, not
# the fleet centroid (same discrepancy GasBombController already
# has) - enemies are placed relative to that ship's own position,
# not the centroid, so a non-zero formation offset can't put them
# off the real beam line.
first_shooter = skill._next_ready_laser_ship()
if first_shooter == null:
return _fail_now("no laser ship is ready right after spawning")
var press := InputEventScreenTouch.new()
press.index = 7
var laser_button: Control = main.get_node("UI/LaserButton")
press.position = laser_button.global_position + laser_button.size * 0.5
press.pressed = true
main._handle_touch(press)
if not skill.aiming:
return _fail_now("laser control did not begin aiming")
# --- D. zoom-out while aiming --------------------------------
var drag := InputEventScreenDrag.new()
drag.index = 7
drag.position = press.position + Vector2(300.0, 0.0)
main._handle_drag(drag)
if main.get_node("Camera2D").zoom.x >= original_zoom:
return _fail_now("aiming did not zoom out")
print("aiming zoomed the camera out")
# --- 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)]:
var enemy: Node = load("res://Enemy.tscn").instantiate()
enemy.position = first_shooter.global_position + offset
main.add_child(enemy)
enemy.speed = 0.0
on_line_enemies.append(enemy)
off_line_enemy = load("res://Enemy.tscn").instantiate()
off_line_enemy.position = first_shooter.global_position \
+ Vector2(200.0, LaserController.BEAM_HALF_WIDTH + 60.0)
main.add_child(off_line_enemy)
off_line_enemy.speed = 0.0
beyond_range_enemy = load("res://Enemy.tscn").instantiate()
beyond_range_enemy.position = first_shooter.global_position + Vector2(2000.0, 0.0)
main.add_child(beyond_range_enemy)
beyond_range_enemy.speed = 0.0
var release := InputEventScreenTouch.new()
release.index = 7
release.position = drag.position
release.pressed = false
main._handle_touch(release)
# Instant beam: the hit already landed by the time _handle_touch
# returns, no travel time to wait out like the bomb's projectile.
if skill.last_hit_count != 2:
return _fail_now("beam hit %d enemies, expected 2" % skill.last_hit_count)
for enemy in on_line_enemies:
if not is_instance_valid(enemy) or enemy.hp >= enemy.max_hp:
return _fail_now("an enemy on the beam line was not damaged")
if not is_instance_valid(off_line_enemy) or off_line_enemy.hp != off_line_enemy.max_hp:
return _fail_now("the beam damaged an enemy off its line")
if not is_instance_valid(beyond_range_enemy) or beyond_range_enemy.hp != beyond_range_enemy.max_hp:
return _fail_now("the beam damaged an enemy beyond its range")
print("beam damaged exactly the 2 enemies on its line within range")
# --- C. cooldown is per ship, not per fleet ---------------------
if not main.get_node("UI/LaserButton").enabled:
return _fail_now("firing one laser ship disabled the control with a second ship still ready")
print("one ship fired; control stays enabled with the second ready")
stage = 1
elapsed = 0.0
return false
1:
if elapsed < 0.3:
return false
second_shooter = skill._next_ready_laser_ship()
if second_shooter == null or second_shooter == first_shooter:
return _fail_now("the second shot was not offered to the other ship")
var press2 := InputEventScreenTouch.new()
press2.index = 8
var laser_button: Control = main.get_node("UI/LaserButton")
press2.position = laser_button.global_position + laser_button.size * 0.5
press2.pressed = true
main._handle_touch(press2)
if not skill.aiming:
return _fail_now("laser control did not re-open aiming for the second shot")
var drag2 := InputEventScreenDrag.new()
drag2.index = 8
drag2.position = press2.position + Vector2(0.0, 300.0)
main._handle_drag(drag2)
var release2 := InputEventScreenTouch.new()
release2.index = 8
release2.position = drag2.position
release2.pressed = false
main._handle_touch(release2)
if main.get_node("UI/LaserButton").enabled:
return _fail_now("both laser ships fired but the control is still enabled")
print("second shot fired from the other ship - two ships, two shots, now disabled")
stage = 2
elapsed = 0.0
return false
2:
# --- D continued: zoom restores after aiming ends ---------------
if elapsed < 1.0:
return false
if absf(main.get_node("Camera2D").zoom.x - original_zoom) > 0.03:
return _fail_now("camera did not restore its pre-aim zoom")
print("camera zoom restored")
print("TEST_LASER_VESSEL: PASS")
quit(0)
return true
return false
func _fail_now(message: String) -> bool:
print("TEST_LASER_VESSEL: FAIL (%s)" % message)
quit(1)
return true

View file

@ -23,7 +23,7 @@ func _process(delta: float) -> bool:
return false return false
var definitions: Array = VESSEL_DEFINITIONS.all() var definitions: Array = VESSEL_DEFINITIONS.all()
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet() var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
if fleet_definitions.size() != 4 \ if fleet_definitions.size() != 5 \
or fleet_definitions.any(func(definition): return definition.has_capability("transport")): or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
return _fail("logistics transporters leaked into the combat-fleet catalog") return _fail("logistics transporters leaked into the combat-fleet catalog")
if ships.size() != definitions.size(): if ships.size() != definitions.size():