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:
parent
bbb1d4b0c5
commit
ed46cd77e6
18 changed files with 858 additions and 64 deletions
|
|
@ -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) ---------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
197
LaserController.gd
Normal 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
11
LaserCooldownUpgrade.tres
Normal 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
11
LaserDamageUpgrade.tres
Normal 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
11
LaserRangeUpgrade.tres
Normal 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}
|
||||||
29
LaserVesselDefinition.tres
Normal file
29
LaserVesselDefinition.tres
Normal 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
232
Main.tscn
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
100
VesselLaserComponent.gd
Normal 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))
|
||||||
6
VesselLaserComponent.tscn
Normal file
6
VesselLaserComponent.tscn
Normal 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")
|
||||||
49
tests/_laser_aim_preview.gd
Normal file
49
tests/_laser_aim_preview.gd
Normal 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
|
||||||
6
tests/_laser_aim_preview.tscn
Normal file
6
tests/_laser_aim_preview.tscn
Normal 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
193
tests/test_laser_vessel.gd
Normal 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
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue