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 Node
|
|
|
|
|
|
|
|
|
|
# Autoload singleton - the one thing that survives change_scene_to_file()
|
|
|
|
|
# scene swaps between space (Main.tscn) and a planet's surface
|
|
|
|
|
# (PlanetSurface.tscn). Everything else in either scene gets torn down
|
|
|
|
|
# and rebuilt from scratch on each swap, so anything that needs to
|
|
|
|
|
# survive the trip has to live here instead.
|
|
|
|
|
|
|
|
|
|
var has_return_position: bool = false
|
|
|
|
|
var surviving_ship_count: int = 5
|
|
|
|
|
var return_world_position: Vector2 = Vector2.ZERO
|
|
|
|
|
var return_camera_zoom: float = 1.0
|
|
|
|
|
|
|
|
|
|
var planet_name: String = ""
|
|
|
|
|
var planet_type: String = ""
|
|
|
|
|
|
|
|
|
|
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
|
|
|
|
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
|
|
|
|
# every scene swap) so exploration progress isn't lost every time the
|
|
|
|
|
# player lands on and leaves a planet.
|
|
|
|
|
var explored_chunks: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
# --- Resources -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Rocks mined from asteroid-planet surfaces, spent on buildings and ships.
|
|
|
|
|
# Starts stocked for play-testing so buildings can be tried without mining
|
|
|
|
|
# first - drop this back to 0 for a real playthrough.
|
|
|
|
|
var rocks: int = 500
|
|
|
|
|
|
|
|
|
|
const MOTHERSHIP_COST: int = 20
|
|
|
|
|
const SHIP_CRAFTER_COST: int = 12
|
|
|
|
|
const SHIP_COST: int = 6
|
|
|
|
|
|
|
|
|
|
# --- Buildings -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Player-placed structures, as [{"type": String, "position": Vector2}].
|
|
|
|
|
# Buildings exist only in space (Main.tscn) but are stored here so they
|
|
|
|
|
# survive the scene swap to a planet's surface and back.
|
|
|
|
|
const BUILDING_MOTHERSHIP: String = "mothership"
|
|
|
|
|
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
|
|
|
|
const BUILDING_COMMAND_CENTER: String = "command_center"
|
|
|
|
|
# Extractors are not placed in space like the others - they are built ON a
|
|
|
|
|
# planet's surface and tracked separately (see `extractors`).
|
|
|
|
|
const BUILDING_EXTRACTOR: String = "extractor"
|
|
|
|
|
|
|
|
|
|
const COMMAND_CENTER_COST: int = 15
|
|
|
|
|
const EXTRACTOR_COST: int = 25
|
|
|
|
|
|
|
|
|
|
var buildings: Array = []
|
|
|
|
|
|
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
|
|
|
# --- Skill tree ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Researched at the mothership. Two things hang off it: which structures the
|
|
|
|
|
# player is allowed to build at all, and permanent upgrades that have no
|
|
|
|
|
# building of their own. The mothership itself is deliberately NOT in the
|
|
|
|
|
# tree - it is the thing you research AT, so gating it behind research would
|
|
|
|
|
# be a deadlock.
|
|
|
|
|
#
|
|
|
|
|
# `x` is a horizontal position on a 0..1 grid; the vertical position is NOT
|
|
|
|
|
# stored, it is derived from how many prerequisites deep the node is, so the
|
|
|
|
|
# tree always reads root-at-the-bottom and cannot drift out of step with the
|
|
|
|
|
# dependencies when nodes are added. `requires` is a single prerequisite id
|
|
|
|
|
# ("" for a root).
|
|
|
|
|
const SKILL_TREE: Array = [
|
|
|
|
|
{
|
|
|
|
|
"id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "",
|
|
|
|
|
"x": 0.5, "unlocks": BUILDING_SHIP_CRAFTER,
|
|
|
|
|
"desc": "Build ship crafters, and buy extra vessels at them.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "logistics", "name": "Logistics", "cost": 25, "requires": "shipyards",
|
|
|
|
|
"x": 0.18, "unlocks": BUILDING_EXTRACTOR,
|
|
|
|
|
"desc": "Build extractors on planets. They mine ore whether or not you are nearby.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards",
|
|
|
|
|
"x": 0.82, "unlocks": BUILDING_COMMAND_CENTER,
|
|
|
|
|
"desc": "Build command centers, where the fleet formation is arranged.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
|
|
|
|
|
"x": 0.82, "fleet_bonus": 1,
|
|
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing",
|
|
|
|
|
"x": 0.65, "fleet_bonus": 1,
|
|
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
|
|
|
|
|
"x": 0.3, "fleet_bonus": 1,
|
|
|
|
|
"desc": "Permanent: +1 vessel in the fleet.",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const BASE_FLEET_SIZE: int = 5
|
|
|
|
|
|
|
|
|
|
var unlocked: Array = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func skill_node(id: String) -> Dictionary:
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.id == id:
|
|
|
|
|
return node
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func skill_depth(id: String) -> int:
|
|
|
|
|
# How many prerequisites deep this node sits. Drives its height in the
|
|
|
|
|
# tree view: roots at the bottom, everything that depends on something
|
|
|
|
|
# above it.
|
|
|
|
|
var depth: int = 0
|
|
|
|
|
var node: Dictionary = skill_node(id)
|
|
|
|
|
while not node.is_empty() and node.requires != "":
|
|
|
|
|
depth += 1
|
|
|
|
|
node = skill_node(node.requires)
|
|
|
|
|
if depth > SKILL_TREE.size():
|
|
|
|
|
break # a cycle in the data would otherwise hang here
|
|
|
|
|
return depth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func max_skill_depth() -> int:
|
|
|
|
|
var deepest: int = 0
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
deepest = maxi(deepest, skill_depth(node.id))
|
|
|
|
|
return deepest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_unlocked(id: String) -> bool:
|
|
|
|
|
return id in unlocked
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func prerequisite_met(id: String) -> bool:
|
|
|
|
|
var node: Dictionary = skill_node(id)
|
|
|
|
|
if node.is_empty():
|
|
|
|
|
return false
|
|
|
|
|
return node.requires == "" or is_unlocked(node.requires)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_unlock(id: String) -> bool:
|
|
|
|
|
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unlock(id: String) -> bool:
|
|
|
|
|
if not can_unlock(id):
|
|
|
|
|
return false
|
|
|
|
|
if not spend(int(skill_node(id).cost)):
|
|
|
|
|
return false
|
|
|
|
|
unlocked.append(id)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_building_unlocked(type: String) -> bool:
|
|
|
|
|
# The mothership is always buildable: it is where research happens, so
|
|
|
|
|
# locking it would leave a player with no mothership unable to ever get
|
|
|
|
|
# one.
|
|
|
|
|
if type == BUILDING_MOTHERSHIP:
|
|
|
|
|
return true
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.get("unlocks", "") == type:
|
|
|
|
|
return is_unlocked(node.id)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func research_for_building(type: String) -> String:
|
|
|
|
|
for node in SKILL_TREE:
|
|
|
|
|
if node.get("unlocks", "") == type:
|
|
|
|
|
return node.name
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func max_fleet_size() -> int:
|
|
|
|
|
var n: int = BASE_FLEET_SIZE
|
|
|
|
|
for id in unlocked:
|
|
|
|
|
n += int(skill_node(id).get("fleet_bonus", 0))
|
|
|
|
|
return mini(n, DEFAULT_FORMATION.size())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reset_progression() -> void:
|
|
|
|
|
unlocked.clear()
|
|
|
|
|
|
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 add_building(type: String, at_position: Vector2) -> void:
|
|
|
|
|
buildings.append({"type": type, "position": at_position})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func has_mothership() -> bool:
|
|
|
|
|
return get_mothership_position() != null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func remove_buildings_of_type(type: String) -> int:
|
|
|
|
|
var kept: Array = []
|
|
|
|
|
var removed: int = 0
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if b.type == type:
|
|
|
|
|
removed += 1
|
|
|
|
|
else:
|
|
|
|
|
kept.append(b)
|
|
|
|
|
buildings = kept
|
|
|
|
|
return removed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_mothership_position() -> Variant:
|
|
|
|
|
# Where a wiped-out fleet gets rebuilt. Returns null when no mothership
|
|
|
|
|
# has been built yet, so the caller can fall back to the world origin.
|
|
|
|
|
for b in buildings:
|
|
|
|
|
if b.type == BUILDING_MOTHERSHIP:
|
|
|
|
|
return b.position
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Fleet formation -------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Where each vessel sits relative to the fleet centre, editable at a command
|
|
|
|
|
# centre. Lives here (not as a const on Fleet) so a layout the player has
|
|
|
|
|
# arranged survives the trip to a planet's surface and back, and so a
|
|
|
|
|
# rebuilt fleet keeps their arrangement.
|
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
|
|
|
# Slots beyond BASE_FLEET_SIZE only become usable once the matching fleet
|
|
|
|
|
# capacity upgrade is researched, but they are defined up front so the wedge
|
|
|
|
|
# keeps a deliberate shape as it grows rather than having positions invented
|
|
|
|
|
# at unlock time.
|
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
|
|
|
const DEFAULT_FORMATION: Array = [
|
|
|
|
|
Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32),
|
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
|
|
|
Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56),
|
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
|
|
|
]
|
|
|
|
|
|
|
|
|
|
var formation: Array = DEFAULT_FORMATION.duplicate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reset_formation() -> void:
|
|
|
|
|
formation = DEFAULT_FORMATION.duplicate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func rotate_formation(radians: float) -> void:
|
|
|
|
|
# Pivots every slot about the fleet centre, keeping the shape intact
|
|
|
|
|
# and only changing which way it faces.
|
|
|
|
|
for i in range(formation.size()):
|
|
|
|
|
formation[i] = formation[i].rotated(radians)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Extractors and their haulers -----------------------------------------
|
|
|
|
|
|
|
|
|
|
# An extractor sits on a planet and runs a single automated hauler back and
|
|
|
|
|
# forth to the mothership, delivering ore on each arrival.
|
|
|
|
|
#
|
|
|
|
|
# The round trip is SIMULATED HERE, in the autoload, rather than by any
|
|
|
|
|
# node: it advances whether the player is in space, on a planet's surface,
|
|
|
|
|
# or far enough away that nothing about the route is loaded. `progress` is
|
|
|
|
|
# a fraction of one full there-and-back cycle, so a hauler's drawn position
|
|
|
|
|
# is derived from the same number that decides when ore is delivered - the
|
|
|
|
|
# visual arrival and the payout cannot drift apart, because they are the
|
|
|
|
|
# same event.
|
|
|
|
|
#
|
|
|
|
|
# Entries are {"position": Vector2 (the planet's position in SPACE),
|
|
|
|
|
# "planet_name": String, "progress": float}.
|
|
|
|
|
const HAULER_SPEED: float = 260.0
|
|
|
|
|
const HAULER_YIELD: int = 4
|
|
|
|
|
# Progress value at which the hauler reaches the mothership: exactly half
|
|
|
|
|
# way through the cycle, the outbound leg being the first half.
|
|
|
|
|
const HAULER_ARRIVAL: float = 0.5
|
|
|
|
|
|
|
|
|
|
var extractors: Array = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func has_extractor_at(planet_name: String) -> bool:
|
|
|
|
|
for e in extractors:
|
|
|
|
|
if e.planet_name == planet_name:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 15:43:43 +02:00
|
|
|
signal extractor_added(planet_name: String)
|
|
|
|
|
|
|
|
|
|
|
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 add_extractor(planet_name: String, at_position: Vector2) -> void:
|
|
|
|
|
extractors.append({
|
|
|
|
|
"position": at_position,
|
|
|
|
|
"planet_name": planet_name,
|
|
|
|
|
"progress": 0.0,
|
|
|
|
|
})
|
2026-08-11 15:43:43 +02:00
|
|
|
# An extractor is built from SPACE, with the fleet parked on the planet -
|
|
|
|
|
# so the Planet node is already in the tree with its _ready() long since
|
|
|
|
|
# run. Without this it kept showing no derrick until it happened to
|
|
|
|
|
# despawn and respawn, which reads as the build having done nothing.
|
|
|
|
|
extractor_added.emit(planet_name)
|
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_hauler_route_time(extractor: Dictionary) -> float:
|
|
|
|
|
# Full cycle: out to the mothership and back again.
|
|
|
|
|
var mothership: Variant = get_mothership_position()
|
|
|
|
|
if mothership == null:
|
|
|
|
|
return 0.0
|
|
|
|
|
var distance: float = extractor.position.distance_to(mothership)
|
|
|
|
|
if distance < 1.0:
|
|
|
|
|
return 0.0
|
|
|
|
|
return (distance * 2.0) / HAULER_SPEED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_hauler_position(extractor: Dictionary) -> Variant:
|
|
|
|
|
# Where the hauler is right now, derived purely from `progress`. Returns
|
|
|
|
|
# null when there is no route (no mothership built yet).
|
|
|
|
|
var mothership: Variant = get_mothership_position()
|
|
|
|
|
if mothership == null:
|
|
|
|
|
return null
|
|
|
|
|
var t: float = extractor.progress
|
|
|
|
|
if t <= HAULER_ARRIVAL:
|
|
|
|
|
return extractor.position.lerp(mothership, t / HAULER_ARRIVAL)
|
|
|
|
|
return mothership.lerp(extractor.position, (t - HAULER_ARRIVAL) / (1.0 - HAULER_ARRIVAL))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
|
|
|
# Runs in the autoload, so extraction keeps earning while the player is
|
|
|
|
|
# on a planet's surface or nowhere near the route.
|
|
|
|
|
if extractors.is_empty():
|
|
|
|
|
return
|
|
|
|
|
for e in extractors:
|
|
|
|
|
var cycle: float = get_hauler_route_time(e)
|
|
|
|
|
if cycle <= 0.0:
|
|
|
|
|
continue # nothing to deliver to yet
|
|
|
|
|
var before: float = e.progress
|
|
|
|
|
e.progress += delta / cycle
|
|
|
|
|
# Ore lands the instant the hauler reaches the mothership - the same
|
|
|
|
|
# instant get_hauler_position() puts it there.
|
|
|
|
|
if before < HAULER_ARRIVAL and e.progress >= HAULER_ARRIVAL:
|
|
|
|
|
rocks += HAULER_YIELD
|
|
|
|
|
while e.progress >= 1.0:
|
|
|
|
|
e.progress -= 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_afford(cost: int) -> bool:
|
|
|
|
|
return rocks >= cost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func spend(cost: int) -> bool:
|
|
|
|
|
if not can_afford(cost):
|
|
|
|
|
return false
|
|
|
|
|
rocks -= cost
|
|
|
|
|
return true
|