#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
baac11ae8b
commit
2f161621c3
495 changed files with 571 additions and 567 deletions
12
vessels/CarrierCapacityUpgrade.tres
Normal file
12
vessels/CarrierCapacityUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "car_capacity"
|
||||
display_name = "Reinforced Hold"
|
||||
description = "+30 cargo capacity per level."
|
||||
base_cost = 40
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 21
|
||||
additive_modifiers = {"cargo_capacity": 30.0}
|
||||
26
vessels/CarrierVesselDefinition.tres
Normal file
26
vessels/CarrierVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=4 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselCargoComponent.tscn" id="2"]
|
||||
[ext_resource type="Resource" path="res://vessels/CarrierCapacityUpgrade.tres" id="3"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "carrier"
|
||||
display_name = "Carrier"
|
||||
description = "Unarmed hauler with a much larger hold than any combat vessel. Cannot fire."
|
||||
build_cost = 24
|
||||
build_time = 20.0
|
||||
icon = 34
|
||||
glow_color = Color(1, 0.78, 0.3, 1)
|
||||
hull_points = PackedVector2Array(-32, -10, -18, -18, 18, -18, 32, -10, 32, 14, -32, 14)
|
||||
fleet_eligible = true
|
||||
capabilities = PackedStringArray("cargo")
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
base_stats = {"max_speed": 380.0, "acceleration": 1080.0, "delivery_speed": 180.0, "cargo_capacity": 120.0}
|
||||
upgrades = Array[Resource]([ExtResource("3")])
|
||||
unlock_id = "unlock_carrier"
|
||||
unlock_name = "Unlock Carrier"
|
||||
unlock_description = "Research a dedicated hauler hull that trades weapons for a much larger cargo hold."
|
||||
unlock_cost = 0
|
||||
unlock_resource_costs = {"ore": 60}
|
||||
research_point_cost = 1
|
||||
unlock_icon = -1
|
||||
25
vessels/DistorderVesselDefinition.tres
Normal file
25
vessels/DistorderVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselDistorderComponent.tscn" id="2"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "distorder"
|
||||
display_name = "Energy Propulser"
|
||||
description = "Unarmed propulsion hull that burns energy to drive the whole fleet faster. While one flies with you, a fader lets you push cruise speed past its default."
|
||||
build_cost = 14
|
||||
build_resource_costs = {"ore": 14, "energy": 8}
|
||||
build_time = 16.0
|
||||
icon = 37
|
||||
glow_color = Color(0.85, 0.95, 0.15, 1)
|
||||
hull_points = PackedVector2Array(0, -26, 9, -8, 13, 6, 18, 20, 0, 12, -18, 20, -13, 6, -9, -8)
|
||||
fleet_eligible = true
|
||||
capabilities = PackedStringArray("distorder")
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
base_stats = {"max_speed": 430.0, "acceleration": 1230.0, "delivery_speed": 180.0}
|
||||
unlock_id = "unlock_distorder"
|
||||
unlock_name = "Unlock Energy Propulser"
|
||||
unlock_description = "Research a propulsion hull that burns energy to drive the whole fleet faster - while one flies with you, a fader lets you push cruise speed past its default."
|
||||
unlock_cost = 0
|
||||
unlock_resource_costs = {"energy": 50}
|
||||
research_point_cost = 1
|
||||
unlock_icon = -1
|
||||
327
vessels/Fleet.gd
Normal file
327
vessels/Fleet.gd
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
extends Node2D
|
||||
|
||||
# Formation slots come from GameState.formation (editable at a command
|
||||
# centre) rather than being fixed here, so a layout the player arranged
|
||||
# survives scene swaps and fleet rebuilds. Ships are always
|
||||
# spawned programmatically (spawn_ships), using however many of these
|
||||
# offsets are needed. The optional vessel-type list preserves the exact
|
||||
# mixed fleet through scene swaps; a fresh start still defaults each slot
|
||||
# to an interceptor.
|
||||
# The fleet's cruise speed. This is the speed of the FLEET as a whole: the
|
||||
# anchor below advances at exactly this rate, and individual ships are
|
||||
# allowed to fly faster than it (Ship.max_speed) so they can close on their
|
||||
# formation slot - which is what makes a pivot while under way look sharp
|
||||
# instead of sluggish.
|
||||
@export var cruise_speed: float = 260.0
|
||||
|
||||
# Per-scene neon palette for the fleet. PlanetSurface.tscn overrides these
|
||||
# with darker values, since the lunar surface is bright grey and the default
|
||||
# near-white core would wash out against it. See NeonDraw.
|
||||
@export var ship_glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||
@export var ship_core_color: Color = Color(1.0, 1.0, 1.0)
|
||||
|
||||
var ship_scene: PackedScene = preload("res://vessels/Ship.tscn")
|
||||
|
||||
var ships: Array = []
|
||||
var formation_offsets: Array = []
|
||||
var fleet_target: Vector2 = Vector2.ZERO
|
||||
# Formation centre while under joystick steering, and the heading it is
|
||||
# travelling along. Zero heading means the fleet is not being steered.
|
||||
var anchor: Vector2 = Vector2.ZERO
|
||||
var steer_direction: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Lets anything resolve "the fleet" without a hardcoded ../Fleet sibling
|
||||
# path - enemies parented under different hosts (e.g. a planet surface's
|
||||
# SurfaceFeatures) need this to find their target.
|
||||
add_to_group("player_fleet")
|
||||
# Researching a vessel upgrade improves the ships already flying, not
|
||||
# just the next one built (#5).
|
||||
GameState.vessel_upgraded.connect(_on_vessel_upgraded)
|
||||
|
||||
|
||||
func _on_vessel_upgraded(_id: String) -> void:
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship):
|
||||
ship.apply_vessel_stats()
|
||||
|
||||
|
||||
func spawn_ships(count: int, center: Vector2 = Vector2.ZERO,
|
||||
vessel_types: PackedStringArray = PackedStringArray()) -> void:
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship):
|
||||
ship.queue_free()
|
||||
ships.clear()
|
||||
formation_offsets.clear()
|
||||
|
||||
var spawn_types: PackedStringArray = PackedStringArray()
|
||||
if vessel_types.is_empty():
|
||||
for i in range(count):
|
||||
spawn_types.append(GameState.VESSEL_INTERCEPTOR)
|
||||
else:
|
||||
for vessel_type in vessel_types:
|
||||
if GameState.vessel_is_fleet_eligible(vessel_type):
|
||||
spawn_types.append(vessel_type)
|
||||
if spawn_types.is_empty():
|
||||
spawn_types.append(GameState.VESSEL_INTERCEPTOR)
|
||||
var n: int = clamp(spawn_types.size(), 1, GameState.max_fleet_size())
|
||||
for i in range(n):
|
||||
var offset: Vector2 = GameState.formation[i]
|
||||
var ship: Area2D = ship_scene.instantiate()
|
||||
ship.vessel_type = spawn_types[i]
|
||||
ship.position = center + offset
|
||||
ship.glow_color = ship_glow_color
|
||||
ship.core_color = ship_core_color
|
||||
add_child(ship)
|
||||
ships.append(ship)
|
||||
formation_offsets.append(offset)
|
||||
if ship.has_signal("destroyed"):
|
||||
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||
|
||||
fleet_target = center
|
||||
_sync_composition_state()
|
||||
|
||||
|
||||
func vessel_types() -> PackedStringArray:
|
||||
var result: PackedStringArray = PackedStringArray()
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship):
|
||||
result.append(String(ship.vessel_type))
|
||||
return result
|
||||
|
||||
|
||||
func _sync_composition_state() -> void:
|
||||
GameState.sync_fleet_cargo_capacity(vessel_types())
|
||||
# Composition changes are authoritative; scene teardown is not. Keeping
|
||||
# this out of SpeedFader prevents an outgoing scene's empty fleet from
|
||||
# resetting a boost while the same vessels are being restored on a surface.
|
||||
# Each distorder vessel past the first widens the speed ceiling, so the
|
||||
# COUNT matters, not merely whether one is present.
|
||||
GameState.set_distorder_vessel_count(count_ships_with_capability("distorder"))
|
||||
if not has_capability("distorder"):
|
||||
GameState.set_fleet_speed_multiplier(1.0)
|
||||
|
||||
|
||||
func has_vessel_type(vessel_type: String) -> bool:
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func first_vessel_of_type(vessel_type: String) -> Node2D:
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship) and String(ship.vessel_type) == vessel_type:
|
||||
return ship
|
||||
return null
|
||||
|
||||
|
||||
func has_capability(capability: String) -> bool:
|
||||
return first_ship_with_capability(capability) != null
|
||||
|
||||
|
||||
func count_ships_with_capability(capability: String) -> int:
|
||||
var total: int = 0
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship) and ship.has_capability(capability):
|
||||
total += 1
|
||||
return total
|
||||
|
||||
|
||||
func first_ship_with_capability(capability: String) -> Node2D:
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship) and ship.has_capability(capability):
|
||||
return ship
|
||||
return null
|
||||
|
||||
|
||||
func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
||||
# Adds one vessel in the next free formation slot (bought from a ship
|
||||
# crafter). Returns false when the wedge is already full.
|
||||
if not GameState.vessel_is_fleet_eligible(vessel_type) \
|
||||
or ships.size() >= GameState.max_fleet_size():
|
||||
return false
|
||||
var offset: Vector2 = GameState.formation[ships.size()]
|
||||
var ship: Area2D = ship_scene.instantiate()
|
||||
# Before add_child, so Ship._ready() builds the right vessel - the shield
|
||||
# ring is created there and only for a shield vessel.
|
||||
ship.vessel_type = vessel_type
|
||||
# From the formation origin, not the centroid: with a lopsided layout the
|
||||
# centroid is already displaced by the average offset, so adding the new
|
||||
# ship's own offset to it would drop it well outside the wedge.
|
||||
ship.position = get_formation_origin() + offset
|
||||
ship.glow_color = ship_glow_color
|
||||
ship.core_color = ship_core_color
|
||||
add_child(ship)
|
||||
ships.append(ship)
|
||||
formation_offsets.append(offset)
|
||||
if ship.has_signal("destroyed"):
|
||||
ship.destroyed.connect(_on_ship_destroyed.bind(ship))
|
||||
ship.set_move_target(fleet_target + offset)
|
||||
_sync_composition_state()
|
||||
return true
|
||||
|
||||
|
||||
func store_ship(index: int) -> String:
|
||||
# A fleet always keeps one ship deployed. Removing the last one would trip
|
||||
# the wiped-out-fleet safety net on the next frame and immediately spawn a
|
||||
# replacement, which is the opposite of storing it at the mothership.
|
||||
if ships.size() <= 1 or index < 0 or index >= ships.size():
|
||||
return ""
|
||||
var ship: Node = ships[index]
|
||||
if not is_instance_valid(ship):
|
||||
return ""
|
||||
var vessel_type: String = String(ship.vessel_type)
|
||||
ships.remove_at(index)
|
||||
formation_offsets.remove_at(index)
|
||||
ship.queue_free()
|
||||
apply_formation()
|
||||
_sync_composition_state()
|
||||
return vessel_type
|
||||
|
||||
|
||||
func apply_formation() -> void:
|
||||
# Re-seats the EXISTING ships into the current GameState.formation.
|
||||
# Without this, editing the layout at a command centre (or pivoting it)
|
||||
# only affected ships built later, which read as the edit doing nothing.
|
||||
for i in range(ships.size()):
|
||||
if i >= GameState.max_fleet_size():
|
||||
break
|
||||
formation_offsets[i] = GameState.formation[i]
|
||||
_assign_slot_targets()
|
||||
|
||||
|
||||
func move_to(world_pos: Vector2) -> void:
|
||||
steer_direction = Vector2.ZERO
|
||||
fleet_target = world_pos
|
||||
anchor = world_pos
|
||||
_assign_slot_targets()
|
||||
|
||||
|
||||
func move_in_direction(direction: Vector2) -> void:
|
||||
# Joystick steering. Sets the heading; _process advances the anchor.
|
||||
#
|
||||
# This used to park a target 2000 units ahead and let the ships chase
|
||||
# it. That made formation changes barely register while flying: with
|
||||
# the aim point that far away, a 60-unit formation offset moved a
|
||||
# ship's heading by under two degrees, so pivoting mid-flight looked
|
||||
# like it did almost nothing. Anchoring the formation and having each
|
||||
# ship hold its own slot relative to it fixes that.
|
||||
if direction == Vector2.ZERO:
|
||||
stop()
|
||||
return
|
||||
if steer_direction == Vector2.ZERO:
|
||||
anchor = get_formation_origin()
|
||||
steer_direction = direction.normalized()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if steer_direction == Vector2.ZERO:
|
||||
return
|
||||
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
||||
# Ships may exceed it individually to reach their slots. #59: multiplied
|
||||
# at the point of use, not baked into cruise_speed itself, so the base
|
||||
# value stays stable regardless of the current boost.
|
||||
anchor += steer_direction * _effective_cruise_speed() * delta
|
||||
fleet_target = anchor
|
||||
_assign_slot_targets()
|
||||
|
||||
|
||||
func _effective_cruise_speed() -> float:
|
||||
return cruise_speed * GameState.fleet_speed_multiplier
|
||||
|
||||
|
||||
func _assign_slot_targets() -> void:
|
||||
var effective_cruise_speed: float = _effective_cruise_speed()
|
||||
for i in range(ships.size()):
|
||||
ships[i].set_move_target(fleet_target + formation_offsets[i])
|
||||
# Context, not a command: the ship adds this to its own velocity to
|
||||
# work out how it is really moving. See Ship.fleet_velocity.
|
||||
ships[i].fleet_velocity = steer_direction * effective_cruise_speed
|
||||
|
||||
|
||||
func stop() -> void:
|
||||
steer_direction = Vector2.ZERO
|
||||
# Aim each ship at the point where its own braking curve brings it to
|
||||
# rest, so releasing the stick coasts to a halt instead of snapping to
|
||||
# a dead stop (which is what targeting the current position would do,
|
||||
# since that lands inside arrive_radius immediately).
|
||||
fleet_target = get_formation_origin()
|
||||
for ship in ships:
|
||||
# A ship's `velocity` is already its true motion, including the
|
||||
# fleet's travel, because Ship feeds the slot velocity forward into
|
||||
# its own steering rather than carrying it in a separate field. So
|
||||
# stopping is only a matter of telling it the slot has halted and
|
||||
# aiming it at the point its own braking curve reaches.
|
||||
#
|
||||
# This used to do `ship.velocity += ship.fleet_velocity` here, to
|
||||
# compensate for `velocity` reading zero while the ship was in fact
|
||||
# cruising (Ship snapped to its slot every tick and zeroed it). That
|
||||
# compensation was an unbounded ACCUMULATOR: every release added a
|
||||
# whole cruise velocity, the next steer refilled fleet_velocity, and
|
||||
# tapping a direction repeatedly stacked them up faster than
|
||||
# `acceleration` could bleed them off - so a vessel could be flung
|
||||
# along at many times its own top speed, and ships fell out of sync
|
||||
# because each bled off at a rate set by its own distance to its
|
||||
# slot. Ship now bounds its total speed every tick as well, so
|
||||
# neither half of that can come back.
|
||||
var motion: Vector2 = ship.velocity
|
||||
# The fleet is not cruising any more, so the slot is standing still.
|
||||
ship.fleet_velocity = Vector2.ZERO
|
||||
var speed: float = motion.length()
|
||||
if speed <= 0.001:
|
||||
ship.set_move_target(ship.position)
|
||||
continue
|
||||
var braking_distance: float = (speed * speed) / (2.0 * ship.acceleration)
|
||||
ship.set_move_target(ship.position + motion.normalized() * braking_distance)
|
||||
|
||||
|
||||
func get_move_targets() -> Array:
|
||||
var result: Array = []
|
||||
for ship in ships:
|
||||
result.append(ship.move_target)
|
||||
return result
|
||||
|
||||
|
||||
func restore(previous_fleet_target: Vector2, previous_move_targets: Array) -> void:
|
||||
fleet_target = previous_fleet_target
|
||||
for i in range(ships.size()):
|
||||
ships[i].move_target = previous_move_targets[i]
|
||||
|
||||
|
||||
func _on_ship_destroyed(ship: Node) -> void:
|
||||
var idx: int = ships.find(ship)
|
||||
if idx != -1:
|
||||
ships.remove_at(idx)
|
||||
formation_offsets.remove_at(idx)
|
||||
_sync_composition_state()
|
||||
|
||||
|
||||
func get_centroid() -> Vector2:
|
||||
if ships.is_empty():
|
||||
return fleet_target
|
||||
var sum: Vector2 = Vector2.ZERO
|
||||
for ship in ships:
|
||||
sum += ship.position
|
||||
return sum / ships.size()
|
||||
|
||||
|
||||
func get_formation_origin() -> Vector2:
|
||||
# Where the anchor must be for the ships to be standing in their slots
|
||||
# right now. This is NOT the centroid: ships sit at anchor + offset, so
|
||||
# the centroid is anchor + the AVERAGE offset, and the two only coincide
|
||||
# when the layout happens to be balanced around its origin.
|
||||
#
|
||||
# The default wedge is balanced, which is why using the centroid as the
|
||||
# anchor looked fine - until a layout dragged off to one corner at a
|
||||
# command centre made the average offset large. Seeding the anchor from
|
||||
# the centroid then displaced every slot by that average the instant
|
||||
# steering began, so the fleet lurched towards the formation's own
|
||||
# lopsided side before the anchor caught up and it flew the right way.
|
||||
if ships.is_empty():
|
||||
return fleet_target
|
||||
var sum: Vector2 = Vector2.ZERO
|
||||
for offset in formation_offsets:
|
||||
sum += offset
|
||||
return get_centroid() - sum / formation_offsets.size()
|
||||
1
vessels/Fleet.gd.uid
Normal file
1
vessels/Fleet.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ckf3enlke7dol
|
||||
27
vessels/GasBomberVesselDefinition.tres
Normal file
27
vessels/GasBomberVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselGasBomberComponent.tscn" id="2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "gas_bomber"
|
||||
display_name = "Gas Bomber"
|
||||
description = "Area-control vessel that launches volatile gas bombs at a manually aimed target."
|
||||
build_cost = 10
|
||||
build_resource_costs = {"gas": 8, "ore": 10}
|
||||
build_time = 18.0
|
||||
icon = 31
|
||||
glow_color = Color(0.96, 0.32, 0.78, 1)
|
||||
hull_points = PackedVector2Array(0, -24, -24, 14, -10, 10, 0, 22, 10, 10, 24, 14)
|
||||
fleet_eligible = true
|
||||
capabilities = PackedStringArray("gas_bomber")
|
||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||
base_stats = {"max_speed": 420.0, "acceleration": 1200.0, "delivery_speed": 180.0}
|
||||
unlock_id = "unlock_gas_bomber"
|
||||
unlock_name = "Unlock Gas Bomber"
|
||||
unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters."
|
||||
unlock_cost = 0
|
||||
unlock_resource_costs = {"gas": 30}
|
||||
research_point_cost = 1
|
||||
unlock_icon = 31
|
||||
12
vessels/InterceptorDamageUpgrade.tres
Normal file
12
vessels/InterceptorDamageUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "int_damage"
|
||||
display_name = "Heavy Slugs"
|
||||
description = "+1 damage per shot."
|
||||
base_cost = 30
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 15
|
||||
additive_modifiers = {"damage": 1.0}
|
||||
12
vessels/InterceptorRangeUpgrade.tres
Normal file
12
vessels/InterceptorRangeUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "int_range"
|
||||
display_name = "Targeting Array"
|
||||
description = "+60 firing range per level."
|
||||
base_cost = 20
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 17
|
||||
additive_modifiers = {"fire_range": 60.0}
|
||||
12
vessels/InterceptorRateUpgrade.tres
Normal file
12
vessels/InterceptorRateUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "int_rate"
|
||||
display_name = "Autoloader"
|
||||
description = "Fires 15% faster per level."
|
||||
base_cost = 25
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 16
|
||||
multiplicative_modifiers = {"fire_cooldown": 0.85}
|
||||
21
vessels/InterceptorVesselDefinition.tres
Normal file
21
vessels/InterceptorVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=7 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselWeaponComponent.tscn" id="2"]
|
||||
[ext_resource type="Resource" path="res://vessels/InterceptorDamageUpgrade.tres" id="3"]
|
||||
[ext_resource type="Resource" path="res://vessels/InterceptorRateUpgrade.tres" id="4"]
|
||||
[ext_resource type="Resource" path="res://vessels/InterceptorRangeUpgrade.tres" id="5"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselCargoComponent.tscn" id="7"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "interceptor"
|
||||
display_name = "Interceptor"
|
||||
description = "Fast combat vessel with a rapid-firing cannon."
|
||||
build_cost = 6
|
||||
build_time = 10.0
|
||||
icon = 2
|
||||
glow_color = Color(0.25, 0.85, 1, 1)
|
||||
fleet_eligible = true
|
||||
capabilities = PackedStringArray("weapon", "cargo")
|
||||
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("7")])
|
||||
base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 1350.0, "delivery_speed": 180.0, "cargo_capacity": 20.0}
|
||||
upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5")])
|
||||
12
vessels/LaserCooldownUpgrade.tres
Normal file
12
vessels/LaserCooldownUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/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
|
||||
research_point_cost = 1
|
||||
icon = 20
|
||||
additive_modifiers = {"beam_cooldown": -0.6}
|
||||
12
vessels/LaserDamageUpgrade.tres
Normal file
12
vessels/LaserDamageUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/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
|
||||
research_point_cost = 1
|
||||
icon = 15
|
||||
additive_modifiers = {"damage": 1.0}
|
||||
12
vessels/LaserRangeUpgrade.tres
Normal file
12
vessels/LaserRangeUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/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
|
||||
research_point_cost = 1
|
||||
icon = 17
|
||||
additive_modifiers = {"beam_range": 150.0}
|
||||
30
vessels/LaserVesselDefinition.tres
Normal file
30
vessels/LaserVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=6 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselLaserComponent.tscn" id="2"]
|
||||
[ext_resource type="Resource" path="res://vessels/LaserDamageUpgrade.tres" id="3"]
|
||||
[ext_resource type="Resource" path="res://vessels/LaserRangeUpgrade.tres" id="4"]
|
||||
[ext_resource type="Resource" path="res://vessels/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": 1260.0, "delivery_speed": 180.0, "damage": 2.0, "beam_range": 1450.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}
|
||||
research_point_cost = 1
|
||||
unlock_icon = -1
|
||||
48
vessels/Projectile.gd
Normal file
48
vessels/Projectile.gd
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
extends Area2D
|
||||
|
||||
@export var speed: float = 500.0
|
||||
@export var lifetime: float = 1.5
|
||||
@export var damage: int = 1
|
||||
@export var bolt_length: float = 20.0
|
||||
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
var age: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player_projectiles")
|
||||
area_entered.connect(_on_area_entered)
|
||||
# Draw once. The earlier invisible-bolt bug came from _draw() deriving
|
||||
# the bolt's look from `age` - drawing only at age==0 froze it at
|
||||
# whatever the age==0 case rendered as. _draw() below is now a fixed
|
||||
# shape with no age dependency, so one draw is safe and matches the
|
||||
# Ship.gd/Asteroid.gd pattern: position/rotation are transform
|
||||
# properties Godot applies at render time without re-tessellating.
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func launch(direction: Vector2) -> void:
|
||||
velocity = direction.normalized() * speed
|
||||
rotation = direction.angle() + PI / 2.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
position += velocity * delta
|
||||
age += delta
|
||||
if age >= lifetime:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
# Asteroids answer take_damage() exactly as enemies do, so both are hit by
|
||||
# the same branch rather than by a second special case.
|
||||
if area.is_in_group("enemies") or area.is_in_group("asteroids"):
|
||||
area.take_damage(damage)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# Fixed-length, constant-brightness bolt. Drawn once and left alone.
|
||||
var tip: Vector2 = Vector2(0, -bolt_length * 0.6)
|
||||
var tail: Vector2 = Vector2(0, bolt_length * 0.4)
|
||||
NeonDraw.draw_glowing_line(self, tail, tip, Color(1.0, 0.85, 0.3), 1.0, 0.5)
|
||||
1
vessels/Projectile.gd.uid
Normal file
1
vessels/Projectile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bv5epiw5tu65j
|
||||
12
vessels/Projectile.tscn
Normal file
12
vessels/Projectile.tscn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/Projectile.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="1"]
|
||||
radius = 4.0
|
||||
|
||||
[node name="Projectile" type="Area2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("1")
|
||||
12
vessels/ShieldHitsUpgrade.tres
Normal file
12
vessels/ShieldHitsUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "shd_hits"
|
||||
display_name = "Layered Emitters"
|
||||
description = "+1 hit absorbed before the shield drops."
|
||||
base_cost = 60
|
||||
max_level = 2
|
||||
research_point_cost = 1
|
||||
icon = 19
|
||||
additive_modifiers = {"shield_charges": 1.0}
|
||||
12
vessels/ShieldRechargeUpgrade.tres
Normal file
12
vessels/ShieldRechargeUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "shd_recharge"
|
||||
display_name = "Fast Cycling"
|
||||
description = "Shield returns 1.2s sooner per level."
|
||||
base_cost = 35
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 20
|
||||
additive_modifiers = {"shield_recharge": -1.2}
|
||||
27
vessels/ShieldVesselDefinition.tres
Normal file
27
vessels/ShieldVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=5 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselShieldComponent.tscn" id="3"]
|
||||
[ext_resource type="Resource" path="res://vessels/ShieldHitsUpgrade.tres" id="4"]
|
||||
[ext_resource type="Resource" path="res://vessels/ShieldRechargeUpgrade.tres" id="5"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "shield"
|
||||
display_name = "Shield Vessel"
|
||||
description = "Combat vessel whose rechargeable personal shield absorbs lethal impacts."
|
||||
build_cost = 14
|
||||
build_time = 16.0
|
||||
icon = 30
|
||||
glow_color = Color(0.45, 0.8, 1, 1)
|
||||
fleet_eligible = true
|
||||
capabilities = PackedStringArray("shield")
|
||||
component_scenes = Array[PackedScene]([ExtResource("3")])
|
||||
hull_points = PackedVector2Array(0, -24, 20.7846, -12, 20.7846, 12, 0, 24, -20.7846, 12, -20.7846, -12)
|
||||
base_stats = {"damage": 1.0, "fire_cooldown": 0.5, "fire_range": 320.0, "max_speed": 460.0, "acceleration": 1350.0, "shield_charges": 1.0, "shield_recharge": 6.0, "delivery_speed": 180.0}
|
||||
minimum_stats = {"shield_recharge": 2.0}
|
||||
upgrades = Array[Resource]([ExtResource("4"), ExtResource("5")])
|
||||
unlock_id = "unlock_shield"
|
||||
unlock_name = "Unlock Shield Vessel"
|
||||
unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up."
|
||||
unlock_cost = 45
|
||||
research_point_cost = 1
|
||||
unlock_icon = 23
|
||||
197
vessels/Ship.gd
Normal file
197
vessels/Ship.gd
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
extends Area2D
|
||||
|
||||
signal destroyed
|
||||
|
||||
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
|
||||
|
||||
@export var max_speed: float = 460.0
|
||||
@export var acceleration: float = 450.0
|
||||
@export var rotation_speed: float = 8.0
|
||||
@export var arrive_radius: float = 6.0
|
||||
@export var vessel_type: String = "interceptor"
|
||||
|
||||
var definition: Resource
|
||||
var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||
var core_color: Color = Color.WHITE
|
||||
# The ship's TRUE world velocity - what `position` is actually integrated
|
||||
# from, and the single source of truth for how fast this vessel is moving.
|
||||
# Nothing else may contribute motion; see the note in _physics_process.
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
# The velocity of this ship's formation SLOT (the fleet anchor's own travel).
|
||||
# Fed forward into the steering below rather than added on top of it: matching
|
||||
# the slot's speed is most of the job of holding formation, so supplying it
|
||||
# directly leaves `velocity` free to be a small correction and lets the ship
|
||||
# sit in its slot with no steady-state lag and no teleporting.
|
||||
var fleet_velocity: Vector2 = Vector2.ZERO
|
||||
var move_target: Vector2
|
||||
|
||||
# Compatibility accessors keep old saves/tests readable while shield state
|
||||
# itself belongs exclusively to the optional ShieldComponent.
|
||||
var shield_charges: int:
|
||||
get:
|
||||
var component: Node = get_component("shield")
|
||||
return component.charges if component != null else 0
|
||||
set(value):
|
||||
var component: Node = get_component("shield")
|
||||
if component != null:
|
||||
component.set_charges(value)
|
||||
|
||||
var shield_recharge_left: float:
|
||||
get:
|
||||
var component: Node = get_component("shield")
|
||||
return component.recharge_left if component != null else 0.0
|
||||
set(value):
|
||||
var component: Node = get_component("shield")
|
||||
if component != null:
|
||||
component.recharge_left = value
|
||||
|
||||
var projectile_damage: int:
|
||||
get:
|
||||
var component: Node = get_component("weapon")
|
||||
return component.projectile_damage if component != null else 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
move_target = position
|
||||
add_to_group("player_ships")
|
||||
area_entered.connect(_on_area_entered)
|
||||
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||
if definition == null:
|
||||
return
|
||||
glow_color = definition.glow_color
|
||||
for component_scene in definition.component_scenes:
|
||||
var component: Node = component_scene.instantiate()
|
||||
add_child(component)
|
||||
if component.has_method("setup"):
|
||||
component.setup()
|
||||
apply_vessel_stats()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_component(capability: String) -> Node:
|
||||
for child in get_children():
|
||||
if child.get("capability_id") == capability:
|
||||
return child
|
||||
return null
|
||||
|
||||
|
||||
func has_capability(capability: String) -> bool:
|
||||
return get_component(capability) != null
|
||||
|
||||
|
||||
func set_move_target(world_pos: Vector2) -> void:
|
||||
move_target = world_pos
|
||||
|
||||
|
||||
func apply_vessel_stats() -> void:
|
||||
if definition == null:
|
||||
definition = VESSEL_DEFINITIONS.get_definition(vessel_type)
|
||||
if definition == null:
|
||||
return
|
||||
max_speed = GameState.vessel_stat(vessel_type, "max_speed")
|
||||
acceleration = GameState.vessel_stat(vessel_type, "acceleration")
|
||||
for child in get_children():
|
||||
if child.has_method("apply_stats"):
|
||||
child.apply_stats()
|
||||
|
||||
|
||||
func has_shield() -> bool:
|
||||
var component: Node = get_component("shield")
|
||||
return component != null and component.is_active()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
for child in get_children():
|
||||
if child.has_method("tick"):
|
||||
child.tick(delta)
|
||||
|
||||
# #59: multiplied here, at the point of use, rather than baked into the
|
||||
# cached max_speed/acceleration fields apply_vessel_stats() sets - those
|
||||
# stay the true base values, so a stat upgrade researched mid-boost can't
|
||||
# corrupt them, and the fader takes effect the instant it moves with no
|
||||
# re-apply needed. arrive_radius is scaled too: at N physics ticks/sec a
|
||||
# ship's per-tick step is `speed * delta`, and once that step outgrows
|
||||
# the (otherwise fixed) arrive_radius it can overshoot the snap-to-target
|
||||
# zone every tick while chasing a target that is ALSO now receding N
|
||||
# times faster - a small, boost-proportional radius keeps the same
|
||||
# "snaps onto its slot" formation tightness at any boost level that
|
||||
# 1.0x already has.
|
||||
var boost: float = GameState.fleet_speed_multiplier
|
||||
var effective_acceleration: float = acceleration * boost
|
||||
var to_target: Vector2 = move_target - position
|
||||
var distance: float = to_target.length()
|
||||
|
||||
# Snap only when the slot is STANDING STILL. A stationary target is the
|
||||
# one case where converging asymptotically would leave a permanent
|
||||
# sub-pixel residue, and settling exactly is what "the fleet has arrived"
|
||||
# means everywhere else in the game. While the slot is moving the ship
|
||||
# steers continuously instead - a snap there would be a teleport, which
|
||||
# is what used to make `velocity` read zero for a ship that was in fact
|
||||
# travelling at cruise speed, and that lie is what the whole
|
||||
# stop()/fleet_velocity bookkeeping tangle existed to paper over.
|
||||
if fleet_velocity == Vector2.ZERO and distance <= arrive_radius * boost:
|
||||
position = move_target
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
# Feed-forward plus correction. The ship aims to travel at its slot's
|
||||
# velocity, plus whatever extra closes the gap to the slot - so
|
||||
# holding formation costs no error, and `velocity` stays the ship's
|
||||
# real motion rather than a residual that has to be added to
|
||||
# something else to mean anything.
|
||||
var correction: Vector2 = Vector2.ZERO
|
||||
if distance > 0.001:
|
||||
var braking_speed: float = sqrt(2.0 * effective_acceleration * distance)
|
||||
correction = to_target.normalized() * min(max_speed * boost, braking_speed)
|
||||
var desired_velocity: Vector2 = fleet_velocity + correction
|
||||
# HARD BOUND on how fast any single vessel may travel, applied to the
|
||||
# total and re-applied every tick. Feed-forward plus correction can
|
||||
# ask for more than a vessel can actually fly, and without this the
|
||||
# excess was free speed: a ship that had been handed a large velocity
|
||||
# kept it, decaying only at `acceleration`. Clamping the REQUEST is
|
||||
# not enough - the clamp has to sit on the integrated result, so no
|
||||
# sequence of commands can leave a vessel travelling faster than its
|
||||
# own top speed.
|
||||
desired_velocity = desired_velocity.limit_length(max_speed * boost)
|
||||
velocity = velocity.move_toward(desired_velocity, effective_acceleration * delta)
|
||||
velocity = velocity.limit_length(max_speed * boost)
|
||||
position += velocity * delta
|
||||
|
||||
if velocity.length() > 8.0:
|
||||
var target_angle: float = velocity.angle() + PI / 2.0
|
||||
rotation = lerp_angle(rotation, target_angle, rotation_speed * delta)
|
||||
|
||||
|
||||
func take_damage(_amount: int) -> bool:
|
||||
# Ships have no hit points - any unabsorbed hit is lethal. A shield
|
||||
# charge absorbs a hit entirely regardless of its amount, matching
|
||||
# VesselShieldComponent's existing all-or-nothing semantics; `_amount`
|
||||
# is accepted (not `int`-clamped away) so callers like EnemyProjectile
|
||||
# have a normal damage parameter to pass even though it isn't graded here.
|
||||
for child in get_children():
|
||||
if child.has_method("absorb_hit") and child.absorb_hit():
|
||||
return true
|
||||
ShipExplosionVFX.spawn(get_tree().root, position)
|
||||
destroyed.emit()
|
||||
queue_free()
|
||||
return false
|
||||
|
||||
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
|
||||
return
|
||||
# The enemy is told about the impact BEFORE this ship resolves its own
|
||||
# damage, because resolving it can free this node - and a ship that dies
|
||||
# to a ramming chaser must still take the chaser with it. Whether the
|
||||
# impact actually destroys the enemy is the enemy's decision (#82).
|
||||
if area.is_in_group("enemies") and area.has_method("collide_with_player_vessel"):
|
||||
area.collide_with_player_vessel()
|
||||
var absorbed: bool = take_damage(1)
|
||||
if absorbed and area.is_in_group("asteroids") and is_instance_valid(area):
|
||||
area.queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var points: PackedVector2Array = definition.hull_points if definition != null else PackedVector2Array([
|
||||
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||
])
|
||||
NeonDraw.draw_glowing_polygon(self, points, glow_color, 1.0, core_color)
|
||||
1
vessels/Ship.gd.uid
Normal file
1
vessels/Ship.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dd78hwrpks1d5
|
||||
12
vessels/Ship.tscn
Normal file
12
vessels/Ship.tscn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/Ship.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="1"]
|
||||
radius = 16.0
|
||||
|
||||
[node name="Ship" type="Area2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("1")
|
||||
12
vessels/TransportCapacityUpgrade.tres
Normal file
12
vessels/TransportCapacityUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "trn_capacity"
|
||||
display_name = "Expanded Hold"
|
||||
description = "Carries 8 more resources per supply run."
|
||||
base_cost = 30
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 21
|
||||
additive_modifiers = {"cargo_capacity": 8.0}
|
||||
12
vessels/TransportSpeedUpgrade.tres
Normal file
12
vessels/TransportSpeedUpgrade.tres
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_resource type="Resource" script_class="VesselUpgradeDefinition" load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselUpgradeDefinition.gd" id="1"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
upgrade_id = "trn_speed"
|
||||
display_name = "Freight Engines"
|
||||
description = "+70 speed on supply runs."
|
||||
base_cost = 30
|
||||
max_level = 3
|
||||
research_point_cost = 1
|
||||
icon = 22
|
||||
additive_modifiers = {"transport_speed": 70.0}
|
||||
19
vessels/TransporterVesselDefinition.tres
Normal file
19
vessels/TransporterVesselDefinition.tres
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[gd_resource type="Resource" script_class="VesselDefinition" load_steps=6 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselDefinition.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselCargoComponent.tscn" id="2"]
|
||||
[ext_resource type="PackedScene" path="res://vessels/VesselTransportComponent.tscn" id="3"]
|
||||
[ext_resource type="Resource" path="res://vessels/TransportCapacityUpgrade.tres" id="4"]
|
||||
[ext_resource type="Resource" path="res://vessels/TransportSpeedUpgrade.tres" id="5"]
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
definition_id = "transporter"
|
||||
display_name = "Transporter"
|
||||
description = "Hub-owned logistics vessel that moves resources automatically."
|
||||
build_cost = 0
|
||||
build_time = 0.0
|
||||
icon = 12
|
||||
glow_color = Color(0.95, 0.55, 0.85, 1)
|
||||
capabilities = PackedStringArray("cargo", "transport")
|
||||
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3")])
|
||||
base_stats = {"max_speed": 460.0, "acceleration": 1350.0, "cargo_capacity": 20.0, "transport_speed": 150.0, "delivery_speed": 180.0}
|
||||
upgrades = Array[Resource]([ExtResource("4"), ExtResource("5")])
|
||||
9
vessels/VesselCargoComponent.gd
Normal file
9
vessels/VesselCargoComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class_name VesselCargoComponent
|
||||
extends Node
|
||||
|
||||
var capability_id: String = "cargo"
|
||||
var capacity: int
|
||||
|
||||
func apply_stats() -> void:
|
||||
var state: Node = get_tree().root.get_node("GameState")
|
||||
capacity = int(state.vessel_stat(get_parent().vessel_type, "cargo_capacity"))
|
||||
1
vessels/VesselCargoComponent.gd.uid
Normal file
1
vessels/VesselCargoComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bo16x4wq6hg5k
|
||||
4
vessels/VesselCargoComponent.tscn
Normal file
4
vessels/VesselCargoComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselCargoComponent.gd" id="1"]
|
||||
[node name="CargoComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
84
vessels/VesselCooldownFillComponent.gd
Normal file
84
vessels/VesselCooldownFillComponent.gd
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
class_name VesselCooldownFillComponent
|
||||
extends Node2D
|
||||
|
||||
# Shared per-vessel cooldown state and hull-fill feedback for abilities that
|
||||
# grant one independently recharging shot per ship.
|
||||
|
||||
var cooldown_duration: float = 1.0
|
||||
var cooldown_left: float = 0.0
|
||||
|
||||
|
||||
func setup() -> void:
|
||||
show_behind_parent = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_ready() -> bool:
|
||||
return cooldown_left <= 0.0
|
||||
|
||||
|
||||
func _begin_cooldown(duration: float) -> bool:
|
||||
if not is_ready():
|
||||
return false
|
||||
cooldown_duration = maxf(duration, 0.001)
|
||||
cooldown_left = cooldown_duration
|
||||
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: float = INF
|
||||
var bottom: float = -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: bool = previous.y >= fill_y
|
||||
for current in points:
|
||||
var current_inside: bool = 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 ship: Node = get_parent()
|
||||
if ship == null or ship.definition == null:
|
||||
return
|
||||
var hull: PackedVector2Array = ship.definition.hull_points
|
||||
var ready_color: Color = _ready_color()
|
||||
if is_ready():
|
||||
draw_colored_polygon(hull, Color(ready_color, 0.42))
|
||||
NeonDraw.draw_glowing_polygon(self, hull, ready_color, 0.75,
|
||||
ship.core_color, 0.75)
|
||||
return
|
||||
draw_colored_polygon(hull, Color(_cooling_color(), 0.58))
|
||||
var filled: PackedVector2Array = _filled_hull(hull, cooldown_progress())
|
||||
if filled.size() >= 3:
|
||||
draw_colored_polygon(filled, Color(ready_color, 0.68))
|
||||
|
||||
|
||||
func _ready_color() -> Color: return Color.WHITE
|
||||
func _cooling_color() -> Color: return Color(0.2, 0.2, 0.2)
|
||||
1
vessels/VesselCooldownFillComponent.gd.uid
Normal file
1
vessels/VesselCooldownFillComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://drsagaj2wr0ni
|
||||
45
vessels/VesselDefinition.gd
Normal file
45
vessels/VesselDefinition.gd
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
class_name VesselDefinition
|
||||
extends Resource
|
||||
|
||||
# The stable internal key: capability strings, unlock ids in vessel_levels,
|
||||
# component scene names and every test refer to a vessel by this, NOT by
|
||||
# display_name. The two are deliberately allowed to diverge - #77 renamed the
|
||||
# "distorder" to the Energy Propulser purely in what the player sees, and
|
||||
# nothing underneath had to be migrated for it.
|
||||
#
|
||||
# Note for editors of the .tres files themselves: Godot's resource format does
|
||||
# NOT accept `#` comments inside a [resource] block. One added there parses as
|
||||
# part of the preceding value and silently blanks the property after it, which
|
||||
# presents as "<blank> did not resolve its definition" a long way from the
|
||||
# edit. Comments about the data go here, in the script.
|
||||
@export var definition_id: String
|
||||
@export var display_name: String
|
||||
@export_multiline var description: String
|
||||
@export var build_cost: int
|
||||
@export var build_resource_costs: Dictionary = {}
|
||||
@export var build_time: float = 10.0
|
||||
@export var icon: int = 2
|
||||
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
||||
@export var hull_points: PackedVector2Array = PackedVector2Array([
|
||||
Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16),
|
||||
])
|
||||
@export var fleet_eligible: bool = false
|
||||
@export var capabilities: PackedStringArray = []
|
||||
@export var component_scenes: Array[PackedScene] = []
|
||||
@export var base_stats: Dictionary = {}
|
||||
@export var minimum_stats: Dictionary = {}
|
||||
@export var upgrades: Array[Resource] = []
|
||||
@export var unlock_id: String
|
||||
@export var unlock_name: String
|
||||
@export_multiline var unlock_description: String
|
||||
@export var unlock_cost: int
|
||||
@export var unlock_resource_costs: Dictionary = {}
|
||||
# #63: spent from GameState.research_points, alongside unlock_resource_costs
|
||||
# rather than instead of it. 0 (the default) means this unlock needs no
|
||||
# research points at all.
|
||||
@export var research_point_cost: int = 0
|
||||
@export var unlock_icon: int = -1
|
||||
|
||||
|
||||
func has_capability(capability: String) -> bool:
|
||||
return capability in capabilities
|
||||
1
vessels/VesselDefinition.gd.uid
Normal file
1
vessels/VesselDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c6txy3v61blm8
|
||||
36
vessels/VesselDefinitions.gd
Normal file
36
vessels/VesselDefinitions.gd
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
class_name VesselDefinitions
|
||||
extends RefCounted
|
||||
|
||||
const ORDER: PackedStringArray = ["interceptor", "shield", "gas_bomber", "transporter", "carrier", "laser", "distorder"]
|
||||
const DEFINITIONS: Dictionary = {
|
||||
"interceptor": preload("res://vessels/InterceptorVesselDefinition.tres"),
|
||||
"shield": preload("res://vessels/ShieldVesselDefinition.tres"),
|
||||
"gas_bomber": preload("res://vessels/GasBomberVesselDefinition.tres"),
|
||||
"transporter": preload("res://vessels/TransporterVesselDefinition.tres"),
|
||||
"carrier": preload("res://vessels/CarrierVesselDefinition.tres"),
|
||||
"laser": preload("res://vessels/LaserVesselDefinition.tres"),
|
||||
"distorder": preload("res://vessels/DistorderVesselDefinition.tres"),
|
||||
}
|
||||
|
||||
static func get_definition(definition_id: String) -> Resource:
|
||||
return DEFINITIONS.get(definition_id)
|
||||
|
||||
static func all() -> Array:
|
||||
var result: Array = []
|
||||
for definition_id in ORDER:
|
||||
result.append(DEFINITIONS[definition_id])
|
||||
return result
|
||||
|
||||
|
||||
static func fleet() -> Array:
|
||||
var result: Array = []
|
||||
for definition in all():
|
||||
if definition.fleet_eligible:
|
||||
result.append(definition)
|
||||
return result
|
||||
|
||||
static func with_capability(capability: String) -> Resource:
|
||||
for definition in all():
|
||||
if definition.has_capability(capability):
|
||||
return definition
|
||||
return null
|
||||
1
vessels/VesselDefinitions.gd.uid
Normal file
1
vessels/VesselDefinitions.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cgwj1q4nf3ar7
|
||||
9
vessels/VesselDistorderComponent.gd
Normal file
9
vessels/VesselDistorderComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class_name VesselDistorderComponent
|
||||
extends Node
|
||||
|
||||
# Pure presence marker (#59) - no per-shot behaviour, no tick(). Its only
|
||||
# job is to make Ship.has_capability("distorder") true, which is what
|
||||
# SpeedFader.gd/Fleet.gd key off to reveal and apply the speed boost.
|
||||
# Energy drain while boosted is a later ticket (#62).
|
||||
|
||||
var capability_id := "distorder"
|
||||
1
vessels/VesselDistorderComponent.gd.uid
Normal file
1
vessels/VesselDistorderComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c2una2fh0f06b
|
||||
6
vessels/VesselDistorderComponent.tscn
Normal file
6
vessels/VesselDistorderComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/VesselDistorderComponent.gd" id="1"]
|
||||
|
||||
[node name="DistorderComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
19
vessels/VesselGasBomberComponent.gd
Normal file
19
vessels/VesselGasBomberComponent.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class_name VesselGasBomberComponent
|
||||
extends "res://vessels/VesselCooldownFillComponent.gd"
|
||||
|
||||
var capability_id := "gas_bomber"
|
||||
|
||||
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
||||
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
cooldown_duration = 5.0
|
||||
|
||||
|
||||
func consume_shot(duration: float) -> bool:
|
||||
return _begin_cooldown(duration)
|
||||
|
||||
|
||||
func _ready_color() -> Color: return READY_COLOR
|
||||
func _cooling_color() -> Color: return COOLING_COLOR
|
||||
1
vessels/VesselGasBomberComponent.gd.uid
Normal file
1
vessels/VesselGasBomberComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cim0c4n73qd2j
|
||||
6
vessels/VesselGasBomberComponent.tscn
Normal file
6
vessels/VesselGasBomberComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/VesselGasBomberComponent.gd" id="1"]
|
||||
|
||||
[node name="GasBomberComponent" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
30
vessels/VesselLaserComponent.gd
Normal file
30
vessels/VesselLaserComponent.gd
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
class_name VesselLaserComponent
|
||||
extends "res://vessels/VesselCooldownFillComponent.gd"
|
||||
|
||||
var capability_id := "laser"
|
||||
var damage: int = 2
|
||||
var beam_range: float = 950.0
|
||||
|
||||
const READY_COLOR := Color(0.3, 0.9, 1.0)
|
||||
const COOLING_COLOR := Color(0.08, 0.28, 0.34)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
cooldown_duration = 4.0
|
||||
|
||||
|
||||
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 consume_shot() -> bool:
|
||||
return _begin_cooldown(cooldown_duration)
|
||||
|
||||
|
||||
func _ready_color() -> Color: return READY_COLOR
|
||||
func _cooling_color() -> Color: return COOLING_COLOR
|
||||
1
vessels/VesselLaserComponent.gd.uid
Normal file
1
vessels/VesselLaserComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bds5gccvd8dj8
|
||||
6
vessels/VesselLaserComponent.tscn
Normal file
6
vessels/VesselLaserComponent.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://vessels/VesselLaserComponent.gd" id="1"]
|
||||
|
||||
[node name="LaserComponent" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
69
vessels/VesselShieldComponent.gd
Normal file
69
vessels/VesselShieldComponent.gd
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
class_name VesselShieldComponent
|
||||
extends Node
|
||||
|
||||
const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0)
|
||||
const SHIELD_RADIUS: float = 30.0
|
||||
|
||||
var capability_id: String = "shield"
|
||||
var charges: int = 0
|
||||
var recharge_left: float = 0.0
|
||||
var ring: Node2D
|
||||
|
||||
|
||||
func setup() -> void:
|
||||
var ship: Node2D = get_parent()
|
||||
var state: Node = ship.get_tree().root.get_node("GameState")
|
||||
ring = NeonPiece.create(ship, Vector2.ZERO, SHIELD_COLOR)
|
||||
var points := PackedVector2Array()
|
||||
for i in range(14):
|
||||
var angle: float = float(i) / 14.0 * TAU
|
||||
points.append(Vector2(cos(angle), sin(angle)) * SHIELD_RADIUS)
|
||||
ring.add_polygon(points, 0.7)
|
||||
ring.queue_redraw()
|
||||
charges = int(state.vessel_stat(ship.vessel_type, "shield_charges"))
|
||||
_update_visual()
|
||||
|
||||
|
||||
func apply_stats() -> void:
|
||||
var was_up: bool = charges > 0
|
||||
if was_up:
|
||||
charges = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges"))
|
||||
_update_visual()
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
var maximum: int = int(_state().vessel_stat(get_parent().vessel_type, "shield_charges"))
|
||||
if charges >= maximum:
|
||||
return
|
||||
recharge_left -= delta
|
||||
if recharge_left <= 0.0:
|
||||
charges += 1
|
||||
recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge")
|
||||
_update_visual()
|
||||
|
||||
|
||||
func is_active() -> bool:
|
||||
return charges > 0
|
||||
|
||||
|
||||
func absorb_hit() -> bool:
|
||||
if not is_active():
|
||||
return false
|
||||
charges -= 1
|
||||
recharge_left = _state().vessel_stat(get_parent().vessel_type, "shield_recharge")
|
||||
_update_visual()
|
||||
return true
|
||||
|
||||
|
||||
func set_charges(value: int) -> void:
|
||||
charges = value
|
||||
_update_visual()
|
||||
|
||||
|
||||
func _state() -> Node:
|
||||
return get_tree().root.get_node("GameState")
|
||||
|
||||
|
||||
func _update_visual() -> void:
|
||||
if ring != null:
|
||||
ring.visible = is_active()
|
||||
1
vessels/VesselShieldComponent.gd.uid
Normal file
1
vessels/VesselShieldComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ghacatobxl11
|
||||
4
vessels/VesselShieldComponent.tscn
Normal file
4
vessels/VesselShieldComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselShieldComponent.gd" id="1"]
|
||||
[node name="ShieldComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
9
vessels/VesselTransportComponent.gd
Normal file
9
vessels/VesselTransportComponent.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class_name VesselTransportComponent
|
||||
extends Node
|
||||
|
||||
var capability_id: String = "transport"
|
||||
var travel_speed: float
|
||||
|
||||
func apply_stats() -> void:
|
||||
var state: Node = get_tree().root.get_node("GameState")
|
||||
travel_speed = state.vessel_stat(get_parent().vessel_type, "transport_speed")
|
||||
1
vessels/VesselTransportComponent.gd.uid
Normal file
1
vessels/VesselTransportComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://tsapg8ld1vl8
|
||||
4
vessels/VesselTransportComponent.tscn
Normal file
4
vessels/VesselTransportComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselTransportComponent.gd" id="1"]
|
||||
[node name="TransportComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
15
vessels/VesselUpgradeDefinition.gd
Normal file
15
vessels/VesselUpgradeDefinition.gd
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class_name VesselUpgradeDefinition
|
||||
extends Resource
|
||||
|
||||
@export var upgrade_id: String
|
||||
@export var display_name: String
|
||||
@export_multiline var description: String
|
||||
@export var base_cost: int
|
||||
@export var max_level: int = 1
|
||||
@export var icon: int
|
||||
@export var additive_modifiers: Dictionary = {}
|
||||
@export var multiplicative_modifiers: Dictionary = {}
|
||||
# #63: spent from GameState.research_points per level bought, alongside
|
||||
# base_cost (ore) rather than instead of it. 0 (the default) means this
|
||||
# level needs no research points at all.
|
||||
@export var research_point_cost: int = 0
|
||||
1
vessels/VesselUpgradeDefinition.gd.uid
Normal file
1
vessels/VesselUpgradeDefinition.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dw41jux1cn3sw
|
||||
58
vessels/VesselWeaponComponent.gd
Normal file
58
vessels/VesselWeaponComponent.gd
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
class_name VesselWeaponComponent
|
||||
extends Node
|
||||
|
||||
# The fleet's gun. It does NOT pick its own targets: the player aims it with
|
||||
# the fire stick, and it shoots along whatever heading that stick reports for
|
||||
# as long as it is held.
|
||||
#
|
||||
# It used to seek the nearest enemy in range and fire on its own, which meant
|
||||
# combat happened whether or not the player did anything - there was nothing
|
||||
# to aim, and nothing to decide. Firing is now an action like any other, and
|
||||
# the right-hand stick is what performs it.
|
||||
#
|
||||
# `fire_direction` is set every frame by FireStickController from the stick's
|
||||
# own reading, and cleared the moment it is released. Zero means "not firing",
|
||||
# which is the resting state.
|
||||
|
||||
var capability_id: String = "weapon"
|
||||
var fire_range: float
|
||||
var fire_cooldown: float
|
||||
var projectile_damage: int
|
||||
var fire_timer: float = 0.0
|
||||
var fire_direction: Vector2 = Vector2.ZERO
|
||||
var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn")
|
||||
|
||||
|
||||
func apply_stats() -> void:
|
||||
var ship: Node = get_parent()
|
||||
var state: Node = ship.get_tree().root.get_node("GameState")
|
||||
fire_range = state.vessel_stat(ship.vessel_type, "fire_range")
|
||||
fire_cooldown = state.vessel_stat(ship.vessel_type, "fire_cooldown")
|
||||
projectile_damage = int(state.vessel_stat(ship.vessel_type, "damage"))
|
||||
|
||||
|
||||
# Called by FireStickController. A zero vector stops the gun.
|
||||
func set_fire_direction(direction: Vector2) -> void:
|
||||
fire_direction = direction if direction.length() > 0.001 else Vector2.ZERO
|
||||
|
||||
|
||||
func is_firing() -> bool:
|
||||
return fire_direction != Vector2.ZERO
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
# The cooldown runs down whether or not the trigger is held, so tapping
|
||||
# the stick in a new direction is not punished by a timer that only
|
||||
# started when the stick moved.
|
||||
fire_timer -= delta
|
||||
if fire_direction == Vector2.ZERO or fire_timer > 0.0:
|
||||
return
|
||||
var ship: Node2D = get_parent()
|
||||
var projectile: Node2D = projectile_scene.instantiate()
|
||||
projectile.position = ship.position
|
||||
projectile.damage = projectile_damage
|
||||
ship.get_tree().root.add_child(projectile)
|
||||
# Aimed by DIRECTION, not at a target: launch() takes a displacement, so
|
||||
# the gun's own range decides how far the shot is sent.
|
||||
projectile.launch(fire_direction.normalized() * fire_range)
|
||||
fire_timer = fire_cooldown
|
||||
1
vessels/VesselWeaponComponent.gd.uid
Normal file
1
vessels/VesselWeaponComponent.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwo0nkmr4e1sl
|
||||
4
vessels/VesselWeaponComponent.tscn
Normal file
4
vessels/VesselWeaponComponent.tscn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://vessels/VesselWeaponComponent.gd" id="1"]
|
||||
[node name="WeaponComponent" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Loading…
Reference in a new issue