Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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
|
2026-08-14 23:15:12 +02:00
|
|
|
# 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.
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
# 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://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
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 23:10:07 +02:00
|
|
|
func _ready() -> void:
|
2026-08-15 14:43:29 +02:00
|
|
|
# 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")
|
2026-08-11 23:10:07 +02:00
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 23:15:12 +02:00
|
|
|
func spawn_ships(count: int, center: Vector2 = Vector2.ZERO,
|
|
|
|
|
vessel_types: PackedStringArray = PackedStringArray()) -> void:
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
for ship in ships:
|
|
|
|
|
if is_instance_valid(ship):
|
|
|
|
|
ship.queue_free()
|
|
|
|
|
ships.clear()
|
|
|
|
|
formation_offsets.clear()
|
|
|
|
|
|
2026-08-15 00:09:10 +02:00
|
|
|
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())
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
for i in range(n):
|
|
|
|
|
var offset: Vector2 = GameState.formation[i]
|
|
|
|
|
var ship: Area2D = ship_scene.instantiate()
|
2026-08-15 00:09:10 +02:00
|
|
|
ship.vessel_type = spawn_types[i]
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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
|
2026-08-16 21:38:07 +02:00
|
|
|
_sync_composition_state()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
2026-08-14 23:15:12 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 21:38:07 +02:00
|
|
|
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.
|
|
|
|
|
if not has_capability("distorder"):
|
|
|
|
|
GameState.set_fleet_speed_multiplier(1.0)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 04:45:15 +02:00
|
|
|
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 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
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 23:10:07 +02:00
|
|
|
func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
# Adds one vessel in the next free formation slot (bought from a ship
|
|
|
|
|
# crafter). Returns false when the wedge is already full.
|
2026-08-15 00:09:10 +02:00
|
|
|
if not GameState.vessel_is_fleet_eligible(vessel_type) \
|
|
|
|
|
or ships.size() >= GameState.max_fleet_size():
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
return false
|
|
|
|
|
var offset: Vector2 = GameState.formation[ships.size()]
|
|
|
|
|
var ship: Area2D = ship_scene.instantiate()
|
2026-08-11 23:10:07 +02:00
|
|
|
# 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
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
# 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)
|
2026-08-16 21:38:07 +02:00
|
|
|
_sync_composition_state()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 23:08:43 +02:00
|
|
|
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()
|
2026-08-16 21:38:07 +02:00
|
|
|
_sync_composition_state()
|
2026-08-12 23:08:43 +02:00
|
|
|
return vessel_type
|
|
|
|
|
|
|
|
|
|
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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()):
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
if i >= GameState.max_fleet_size():
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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.
|
2026-08-16 17:59:04 +02:00
|
|
|
# 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
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
fleet_target = anchor
|
|
|
|
|
_assign_slot_targets()
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 17:59:04 +02:00
|
|
|
func _effective_cruise_speed() -> float:
|
|
|
|
|
return cruise_speed * GameState.fleet_speed_multiplier
|
|
|
|
|
|
|
|
|
|
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
func _assign_slot_targets() -> void:
|
2026-08-16 17:59:04 +02:00
|
|
|
var effective_cruise_speed: float = _effective_cruise_speed()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
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.
|
2026-08-16 17:59:04 +02:00
|
|
|
ships[i].fleet_velocity = steer_direction * effective_cruise_speed
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
# 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.
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
#
|
Bound individual vessel speed; stop Fleet.stop() accumulating it (#72)
Rotating the fleet and spamming a direction let vessels fall out of sync
and fly at wildly high speeds. The pivot cooldown added earlier throttled
how often the exploit could be triggered; it never touched the cause.
Fleet.stop() did `ship.velocity = ship.velocity + ship.fleet_velocity`,
compensating for the fact that Ship.velocity read zero during cruise. The
next steer refilled fleet_velocity, so every press-release cycle added a
whole cruise velocity to the ship's own, decaying only at `acceleration` in
between. Tapping faster than that bled it off stacked them without limit,
and ships desynced because each bled off at a rate set by its own distance
to its slot. Measured before this change: spamming a direction while
pivoting flew the fleet 32737 units in 3s against 784 for simply holding
the stick, with a vessel reaching 48x its own top speed.
The compensation existed because `velocity` was not the ship's real motion:
Ship snapped to its slot every tick and zeroed it, so the truth lived in a
second field and anything wanting it had to add the two together. Fixed at
that root rather than by clamping the symptom:
- fleet_velocity is now fed FORWARD into the steering instead of carried
alongside it. The ship aims to travel at its slot's velocity plus a
correction closing the gap, so holding formation costs no error and
`velocity` is always true world motion.
- The snap only applies when the slot is standing still, which is the one
case where converging asymptotically would leave a residue. While the
slot moves the ship steers continuously - a snap there was the teleport
that made `velocity` lie in the first place.
- A hard bound on the INTEGRATED result every tick, not merely on the
request, so no sequence of commands can leave a vessel over its own top
speed.
- Fleet.stop() injects nothing; it reports that the slot has halted.
tests/test_fleet_speed_bounded.gd covers it, and fails against the previous
code with the numbers above: no vessel exceeds its own top speed however
hard the direction is spammed, spamming cannot cover more ground than
holding the stick, both hold while the formation is pivoting, and the fleet
stays together.
test_space_time_distorder.gd now clears the asteroid and enemy groups at
setup, the same clean-slate idiom test_ship_lab.gd uses. Silencing the
spawners was never enough - what Main.tscn had already placed kept moving,
and losing the distorder vessel to a rock resets the multiplier correctly,
which the test then reported three stages later as a failed scene swap. It
also checks it still crews a distorder vessel before relying on that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:34 +02:00
|
|
|
# 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.
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
ship.fleet_velocity = Vector2.ZERO
|
|
|
|
|
var speed: float = motion.length()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
if speed <= 0.001:
|
|
|
|
|
ship.set_move_target(ship.position)
|
|
|
|
|
continue
|
|
|
|
|
var braking_distance: float = (speed * speed) / (2.0 * ship.acceleration)
|
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite
Gameplay and UI
- Research tree at the mothership gates which structures can be built and
grants permanent fleet-capacity upgrades (GameState.SKILL_TREE,
SkillTreeView). Node depth is derived from prerequisites, so the layout
cannot drift out of step with the dependencies.
- Building info popup before committing to a build, with per-building cost
and description.
- Ship crafter redrawn as a space factory with an assembly animation;
mothership redrawn as a radial flagship; command centre as a plotting hub.
- Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build.
- UI and font scaled up for phone use, via a custom Theme resource -
gui/theme/default_font_size does not reach the default theme.
- Asteroids no longer spawn on top of the fleet's start position.
- Fleet formation origin derived from the layout's own offsets rather than
the centroid, fixing the lurch when a lopsided formation started moving.
- Fleet.stop() brakes from the ship's TRUE motion (velocity +
fleet_velocity). Ship zeroes its velocity when seated in its slot even
while the anchor drags that slot forward, so braking from velocity alone
stopped the fleet dead instead of coasting - and whether it did depended
on the frame rate.
Animation architecture (issue #10)
- Buildings, projectiles and explosion VFX no longer animate by redrawing.
Static geometry is rasterised once in _draw(); moving parts are NeonPiece
children animated through node properties, which reuse the cached
geometry. Every NeonDraw call expands into four antialiased polylines, so
redrawing to move something re-tessellated all of it.
Test suite (issue #8): 1075s -> ~55s
- Runs from a copy on Termux's own filesystem. The project lives on
Android's FUSE bridge, read through proot, where load("res://Main.tscn")
alone cost 8.5s per test against ~2s natively - and nearly every test
loads it.
- Runs 6 tests at a time, in a single proot-distro login, with output
buffered per test and printed only on failure.
- --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's
result no longer depends on how busy the machine is. Without it,
parallelism made two tests fail on 2 of 3 runs.
- A test passes only if it both exits 0 and prints ": PASS".
- 38 tests, green on three consecutive runs.
Bundles Carrois Gothic SC (Apache 2.0) with its licence text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
|
|
|
ship.set_move_target(ship.position + motion.normalized() * braking_distance)
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-08-16 21:38:07 +02:00
|
|
|
_sync_composition_state()
|
Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).
All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.
Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.
Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|