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
|
|
|
|
|
# offsets are needed - both for a fresh start (5 ships) and for restoring
|
|
|
|
|
# a possibly-smaller surviving fleet after returning from a planet's
|
|
|
|
|
# surface, so both cases share one code path instead of two that could
|
|
|
|
|
# drift out of sync.
|
|
|
|
|
# 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:
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
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 spawn_ships(count: int, center: Vector2 = Vector2.ZERO) -> void:
|
|
|
|
|
for ship in ships:
|
|
|
|
|
if is_instance_valid(ship):
|
|
|
|
|
ship.queue_free()
|
|
|
|
|
ships.clear()
|
|
|
|
|
formation_offsets.clear()
|
|
|
|
|
|
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
|
|
|
var n: int = clamp(count, 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()
|
|
|
|
|
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-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.
|
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 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
# Before the steering early-out: a ship the crafter has already built and
|
|
|
|
|
# paid for is owed whether or not the fleet happens to be flying (#16).
|
|
|
|
|
_claim_owed_ships()
|
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 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()
|
|
|
|
|
|
|
|
|
|
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
func _claim_owed_ships() -> void:
|
|
|
|
|
# Production completes in the GameState autoload, which may well happen
|
|
|
|
|
# while the player is on a planet's surface with no Fleet node in
|
2026-08-11 23:10:07 +02:00
|
|
|
# existence to hand a vessel to. Those vessels wait in `vessels_owed`
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
# fleet with room can take them.
|
2026-08-11 23:10:07 +02:00
|
|
|
while not GameState.vessels_owed.is_empty() and ships.size() < GameState.max_fleet_size():
|
|
|
|
|
if not add_ship(String(GameState.vessels_owed[0])):
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
return
|
2026-08-11 23:10:07 +02:00
|
|
|
GameState.vessels_owed.pop_front()
|
Buildings produce from their own stock, and queue orders until supplied
Forgejo #16, part 3 of #12. BuildController deducted the cost from the
bank at the moment of purchase and refused outright with "Not enough
rocks", so nothing ever had to be delivered anywhere. Each building now
carries its own stock, and an order it cannot pay for yet is QUEUED and
completes by itself once ore arrives.
Records get an `id` because a building has to be referable across the
space <-> planet-surface scene swap, when every Building NODE has been
destroyed and rebuilt. Position would nearly work and then fail on two
buildings placed on the same spot.
Production advances in the GameState autoload, the same way extractor
haulers already do, so an order can complete while the player is down a
gravity well. A finished vessel becomes `ships_owed` rather than being
handed straight to a Fleet that may not exist, and the fleet claims what
it is owed when it next has room.
The craft menu remembers which crafter it was opened for: _process()
clears current_building while any menu is open, so by the next frame the
menu would no longer know. It shows the ore delivered HERE rather than
the bank, since showing the bank would promise something the crafter
cannot actually build.
Ship crafting cannot be supplied until #17 lands - nothing yet moves ore
into a building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:18:44 +02:00
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
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()
|