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>
169 lines
6 KiB
GDScript
169 lines
6 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
|
|
# greyed, and pressing one explains which research opens it rather than
|
|
# silently failing
|
|
# 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()
|
|
var greyed: bool = not scene.get_node("UI/BuildMenu/CrafterButton").enabled
|
|
controller._on_place_crafter()
|
|
var explains: bool = popup.visible \
|
|
and "research" in popup.get_node("Panel/MessageLabel").text.to_lower()
|
|
var offered: bool = popup.get_node("Panel/BuildButton").visible
|
|
print("locked crafter: greyed=%s explains=%s confirm_offered=%s" % [
|
|
greyed, explains, offered
|
|
])
|
|
if not (greyed and explains) or offered:
|
|
print("TEST_SKILL_TREE: FAIL (locked option did not explain itself)")
|
|
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
|