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
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless gameplay test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_skill_tree.gd
|
|
|
|
|
# The mothership's research tree (Forgejo issue #4). It gates which
|
|
|
|
|
# structures may be built at all, and carries permanent upgrades that have
|
|
|
|
|
# no building of their own.
|
|
|
|
|
#
|
|
|
|
|
# Verifies:
|
|
|
|
|
# 1. from a fresh start only the mothership is buildable - the others are
|
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
|
|
|
# not in the menu at all (#21 changed this from greyed-but-listed), and
|
|
|
|
|
# the gate still holds if the build is forced past the menu
|
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
|
|
|
# 2. a locked option cannot be built even if the confirm is forced
|
|
|
|
|
# 3. research costs rocks, needs its prerequisite, and unlocks its building
|
|
|
|
|
# 4. the mothership's action button opens the tree (it used to have no
|
|
|
|
|
# action at all)
|
|
|
|
|
# 5. fleet capacity upgrades raise the cap, and the extra vessel can
|
|
|
|
|
# actually be crafted afterwards
|
|
|
|
|
|
|
|
|
|
var scene: Node
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var controller: Node
|
|
|
|
|
var fleet: Node
|
|
|
|
|
var field: Node
|
|
|
|
|
var popup: Control
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
game_state = root.get_node("GameState")
|
|
|
|
|
game_state.rocks = 500
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
game_state.reset_progression()
|
|
|
|
|
game_state.reset_formation()
|
|
|
|
|
|
|
|
|
|
scene = load("res://Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
controller = scene.get_node("BuildController")
|
|
|
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
field = scene.get_node("BuildingField")
|
|
|
|
|
popup = scene.get_node("UI/BuildMenu/InfoPopup")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
|
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
|
|
|
field.place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
# --- everything but the mothership starts locked --------------
|
|
|
|
|
var mothership_free: bool = game_state.is_building_unlocked(GameState.BUILDING_MOTHERSHIP)
|
|
|
|
|
var crafter_locked: bool = not game_state.is_building_unlocked(GameState.BUILDING_SHIP_CRAFTER)
|
|
|
|
|
print("fresh start: mothership buildable=%s crafter locked=%s" % [
|
|
|
|
|
mothership_free, crafter_locked
|
|
|
|
|
])
|
|
|
|
|
if not (mothership_free and crafter_locked):
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (wrong things buildable at the start)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
controller._on_build_pressed()
|
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
|
|
|
# Since #21 an unresearched structure is not listed at all, rather
|
|
|
|
|
# than listed and greyed. It is therefore unreachable from the
|
|
|
|
|
# menu - but the popup must still refuse if something calls
|
|
|
|
|
# _on_place_crafter() by another route.
|
|
|
|
|
var hidden: bool = not scene.get_node("UI/BuildMenu/CrafterButton").visible
|
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
|
|
|
controller._on_place_crafter()
|
|
|
|
|
var offered: bool = popup.get_node("Panel/BuildButton").visible
|
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
|
|
|
print("locked crafter: hidden_from_menu=%s confirm_offered=%s" % [hidden, offered])
|
|
|
|
|
if not hidden or offered:
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (unresearched option still on offer)")
|
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
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# Forcing the confirm must still build nothing.
|
|
|
|
|
var before: int = game_state.buildings.size()
|
|
|
|
|
controller.pending_build_type = GameState.BUILDING_SHIP_CRAFTER
|
|
|
|
|
controller._confirm_build()
|
|
|
|
|
if game_state.buildings.size() != before:
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (built a locked structure)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
controller._close_info()
|
|
|
|
|
|
|
|
|
|
# --- the mothership opens the tree ----------------------------
|
|
|
|
|
var mothership: Node2D = null
|
|
|
|
|
for b in field.spawned:
|
|
|
|
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_MOTHERSHIP:
|
|
|
|
|
mothership = b
|
|
|
|
|
controller.current_building = mothership
|
|
|
|
|
controller._on_action_pressed()
|
|
|
|
|
var tree_open: bool = scene.get_node("UI/SkillMenu").visible
|
|
|
|
|
print("mothership action opens research: %s" % tree_open)
|
|
|
|
|
if not tree_open:
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (mothership has no research action)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# --- prerequisites --------------------------------------------
|
|
|
|
|
if game_state.can_unlock("wing"):
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (deep node unlockable with no prerequisite)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# --- research costs rocks and unlocks its building ------------
|
|
|
|
|
var rocks_before: int = game_state.rocks
|
|
|
|
|
var cost: int = int(game_state.skill_node("shipyards").cost)
|
|
|
|
|
var ok: bool = game_state.unlock("shipyards")
|
|
|
|
|
var charged: bool = game_state.rocks == rocks_before - cost
|
|
|
|
|
var now_buildable: bool = game_state.is_building_unlocked(GameState.BUILDING_SHIP_CRAFTER)
|
|
|
|
|
print("research shipyards: ok=%s rocks %d->%d charged=%s crafter unlocked=%s" % [
|
|
|
|
|
ok, rocks_before, game_state.rocks, charged, now_buildable
|
|
|
|
|
])
|
|
|
|
|
if not (ok and charged and now_buildable):
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (research did not unlock its building)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# The menu must show it as buildable now, without reopening.
|
|
|
|
|
controller._on_build_pressed()
|
|
|
|
|
if not scene.get_node("UI/BuildMenu/CrafterButton").enabled:
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (build menu still greys an unlocked option)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
controller._close_info()
|
|
|
|
|
scene.get_node("UI/BuildMenu").visible = false
|
|
|
|
|
|
|
|
|
|
# --- fleet capacity -------------------------------------------
|
|
|
|
|
var base_cap: int = game_state.max_fleet_size()
|
|
|
|
|
game_state.unlock("coordination")
|
|
|
|
|
var wing_ok: bool = game_state.unlock("wing")
|
|
|
|
|
var raised_cap: int = game_state.max_fleet_size()
|
|
|
|
|
print("fleet cap %d -> %d after Wing Doctrine (unlocked=%s)" % [
|
|
|
|
|
base_cap, raised_cap, wing_ok
|
|
|
|
|
])
|
|
|
|
|
if not (wing_ok and raised_cap == base_cap + 1):
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (capacity upgrade did not raise the cap)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# And the extra vessel must actually be craftable.
|
|
|
|
|
fleet.spawn_ships(base_cap, Vector2.ZERO)
|
|
|
|
|
var added: bool = fleet.add_ship()
|
|
|
|
|
print("fleet %d ships, add one more -> %s (now %d)" % [
|
|
|
|
|
base_cap, added, fleet.ships.size()
|
|
|
|
|
])
|
|
|
|
|
if not (added and fleet.ships.size() == base_cap + 1):
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (could not crew the extra slot)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
if fleet.add_ship():
|
|
|
|
|
print("TEST_SKILL_TREE: FAIL (cap not enforced beyond the upgrade)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
print("TEST_SKILL_TREE: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|