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") # --- #70: no two nodes drawn on top of each other ------------------- # SkillTreeView.node_screen_pos() places a node purely from its own # (depth, x) - two nodes sharing both values land on the exact same # pixel with no visual way to tell them apart. depth is derived from # `requires` chains, not authored directly, so this has to check the # same way the view does rather than eyeballing the data. var seen: Dictionary = {} for node in GameState.SKILL_TREE: var key: String = "%d:%s" % [game_state.skill_depth(node.id), node.x] if seen.has(key): print("TEST_SKILL_TREE: FAIL (%s and %s are both at depth/x %s - drawn on top of each other)" \ % [seen[key], node.id, key]) quit(1) return seen[key] = node.id print("no two skill-tree nodes share the same (depth, x)") game_state.rocks = 500 game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() game_state.reset_formation() scene = load("res://world/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 # the generic definition entry point by another route. var hidden: bool = not scene.get_node("UI/BuildMenu/CrafterButton").visible controller._show_building_info(GameState.BUILDING_SHIP_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 # The tree is a tall, vertically scrolling canvas. A branch keeps a # stable lane as it rises, and dragging exposes higher depths. var freight: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight")) var yards: Vector2 = controller.skill_menu.tree.node_screen_pos(game_state.skill_node("freight_yards")) if absf(freight.x - yards.x) > 1.0 or yards.y >= freight.y: print("TEST_SKILL_TREE: FAIL (freight branch does not rise vertically)") quit(1) return true var tree_view: Control = controller.skill_menu.tree var before_scroll: float = tree_view.scroll_y var drag: InputEventScreenDrag = InputEventScreenDrag.new() # Start at the root at the bottom; pull the canvas down to reveal # the branches rising above it. drag.relative = Vector2(0.0, 180.0) tree_view.handle_drag(drag) # #219 removed the eight-deep doctrine lane, which was what made # the canvas taller than the view. A tree that FITS has nothing to # scroll, so what is checked depends on which it is - and the # clamp is worth pinning either way, since a canvas that drifts # off its own top edge is the bug this gesture used to expose. if before_scroll > 0.0: if tree_view.scroll_y >= before_scroll: print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)") quit(1) return true print("tree scrolls: %.0f -> %.0f" % [before_scroll, tree_view.scroll_y]) else: if tree_view.scroll_y != 0.0: print("TEST_SKILL_TREE: FAIL (a tree that fits scrolled to %.1f)" \ % tree_view.scroll_y) quit(1) return true print("tree fits the view; the pan clamps at the top") # --- prerequisites -------------------------------------------- if game_state.can_unlock("power_systems"): 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 ------------------------------------------- # #219 took capacity out of the tree entirely: a berth is a Fleet # Office standing somewhere. What this checks now is that the # tree does NOT move the cap, and that the structure does. var base_cap: int = game_state.max_fleet_size() game_state.research_points = 2 game_state.unlock("coordination") if game_state.max_fleet_size() != base_cap: print("TEST_SKILL_TREE: FAIL (research still moves the fleet cap)") quit(1) return true game_state.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2(2400.0, 0.0)) var raised_cap: int = game_state.max_fleet_size() print("fleet cap %d -> %d after a Fleet Office" % [base_cap, raised_cap]) if raised_cap != base_cap + 1: print("TEST_SKILL_TREE: FAIL (a Fleet Office 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