spaceship/tests/test_skill_tree.gd
Loïc Wisniewski 0e071877e2 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

169 lines
6.1 KiB
GDScript

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
# 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
# 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()
# 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
controller._on_place_crafter()
var offered: bool = popup.get_node("Panel/BuildButton").visible
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)")
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