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-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-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)
|
|
|
|
|
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()
|
|
|
|
|
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.
|
|
|
|
|
# Ships may exceed it individually to reach their slots.
|
|
|
|
|
anchor += steer_direction * cruise_speed * delta
|
|
|
|
|
fleet_target = anchor
|
|
|
|
|
_assign_slot_targets()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _assign_slot_targets() -> void:
|
|
|
|
|
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 * 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:
|
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
|
|
|
# A ship's own `velocity` is NOT how fast it is really travelling
|
|
|
|
|
# while the fleet cruises. Ship snaps to its slot and reports zero
|
|
|
|
|
# the moment it is within arrive_radius of it, even though the
|
|
|
|
|
# anchor is dragging that slot forward at cruise_speed underneath
|
|
|
|
|
# it. Its true motion is its catch-up velocity PLUS the fleet's,
|
|
|
|
|
# which it already carries as `fleet_velocity` (zero for a
|
|
|
|
|
# point-to-point move_to, where `velocity` alone is the whole
|
|
|
|
|
# story).
|
|
|
|
|
#
|
|
|
|
|
# Braking from `velocity` alone therefore stopped the fleet dead
|
|
|
|
|
# on any frame where the ships happened to be seated - and how
|
|
|
|
|
# often that happens depends on the frame rate, because the anchor
|
|
|
|
|
# advances cruise_speed * delta per frame. At ~25fps it steps far
|
|
|
|
|
# enough that ships always lag and always report motion, which is
|
|
|
|
|
# the only reason releasing the stick looked right.
|
|
|
|
|
var motion: Vector2 = ship.velocity + ship.fleet_velocity
|
Fix the stop stutter, and hide unresearched buildings from the menu
Forgejo #13: releasing the stick paused, then moved again before
settling. stop() aims each ship at a braking point ahead of it, but the
ship's own velocity was ~0 - the anchor had been carrying it - so it
started from REST, accelerated into its own stopping distance, and
halted a second time. Measured: 0 -> 45 -> 90 -> 135 -> 180 -> brake.
stop() now hands the fleet's motion to the ship, so the release is one
continuous decelerating coast (222 -> 0) to the identical endpoint.
test_joystick_control now asserts the speed one frame after release is a
real fraction of cruise, and never rises afterwards.
Forgejo #21: unresearched structures are no longer listed greyed out.
They are hidden, and the menu repacks around what is left - a fixed grid
with entries hidden would otherwise leave holes where they used to be.
An entry alone on its row is centred rather than hanging in the left
column. Researched-but-not-usable is still shown greyed: an extractor
with no planet under the fleet is "not now", not "not yet".
This reverses a documented decision (the old comment argued that always
listing everything stops the player wondering whether the game has it at
all), so test_skill_tree's matching assertion was updated from
greyed-but-listed to hidden. The gate itself is unchanged and still
refuses a build forced past the menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:38:17 +02:00
|
|
|
# HAND THE FLEET'S MOTION TO THE SHIP. Until now the anchor was
|
|
|
|
|
# carrying it along and its own `velocity` was ~0, so aiming it at a
|
|
|
|
|
# braking point ahead made it start from REST: the fleet stopped
|
|
|
|
|
# dead, then accelerated into its own stopping distance and halted a
|
|
|
|
|
# second time. That is the "pause, then it moves again" stutter.
|
|
|
|
|
# Giving it the velocity it was actually travelling at makes the
|
|
|
|
|
# stop one continuous decelerating coast.
|
|
|
|
|
ship.velocity = motion
|
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
|
|
|
# Context that is no longer true: the fleet is not cruising any
|
|
|
|
|
# more, so leaving it set would have Ship face a heading it is not
|
|
|
|
|
# travelling on.
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|