spaceship/tests/test_skill_menu.gd
Loïc Wisniewski 1f5fecd371 Buy fleet berths with Fleet Offices
#219: fleet capacity is a built, supplied thing rather than a researched
one. The eight fleet-size doctrines are gone; capacity is one berth free
plus one per Fleet Office standing. An office charges every 25s for the
vessel in its berth, priced off that vessel's own build cost so an expensive
hull is expensive to keep, and drawn from its own store the way a gas burner
draws gas. An office that cannot pay goes dark, its vessel is scuttled, and
the player is told through the same queue quest lines use - which reaches
them on a planet surface too.

An empty berth is idle rather than starved: an office is only failing while
there is something in it to fail, and holding it disabled would leave a
berth that could never be crewed and so could never be paid for again.

Ships have no hit points and a shield absorbs a hit wholesale, so the lost
vessel is scuttled rather than damaged - otherwise a shielded hull would
survive its own eviction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 17:09:27 +02:00

197 lines
7.7 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_skill_menu.gd
# Forgejo #93, both halves of it.
#
# A mouse could not drive the research menu at all. Input reaches the tree
# through BuildController._unhandled_input as raw events (a Control that
# eats them through the GUI system would swallow the drag - see
# SkillTreeView's header), and the tree only ever read touch events. That
# was invisible for as long as the project set emulate_touch_from_mouse,
# which handed it a synthetic touch per click; with that setting gone, the
# menu is inert under a mouse and there is no other way to research
# anything on a desktop.
#
# The panel also still had a hole in it. The buy button below the readout
# went away in #83, and nothing moved into the space, so ~180px of empty
# panel sat under the text while the tree canvas above it stayed cramped.
#
# Covers:
# A. a left click selects a node, through the real routing in
# BuildController rather than by calling the view directly
# B. a second click on the SAME node researches it, which is #83's
# confirm gesture reached with a mouse
# C. holding the left button and moving scrolls the canvas, and moving
# with no button held does NOT - a hovering pointer must not drag the
# tree out from under the reader
# D. the wheel scrolls, in both directions
# E. the readout sits at the bottom of the panel and the tree canvas fills
# everything above it, so neither the dead band nor the cramped tree
# comes back
const GAP_TOLERANCE: float = 48.0
var game_state: Node
var scene: Node
var controller: Node
var skill_menu: Control
var tree_view: Control
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_formation()
game_state.rocks = 500
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
skill_menu = scene.get_node("UI/SkillMenu")
tree_view = scene.get_node("UI/SkillMenu/Tree")
func _click(at: Vector2, pressed: bool) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = at
# Through the controller, not straight into the view: the missing piece
# was the ROUTING, so a test that called tree_view.handle_mouse_button()
# itself would have passed against the reported bug.
controller._unhandled_input(event)
func _wheel(button: int) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = button
event.pressed = true
event.position = tree_view.global_position + tree_view.size * 0.5
controller._unhandled_input(event)
func _move(relative: Vector2) -> void:
var event: InputEventMouseMotion = InputEventMouseMotion.new()
event.relative = relative
event.position = tree_view.global_position + tree_view.size * 0.5
controller._unhandled_input(event)
func _node_at(id: String) -> Vector2:
return tree_view.global_position + tree_view.node_screen_pos(game_state.skill_node(id))
func _process(delta: float) -> bool:
elapsed += delta
if stage == 0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
scene.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
elapsed = 0.0
stage = 1
return false
if stage != 1 or elapsed < 0.2:
return false
stage = 2
var mothership: Node2D = null
for building in scene.get_node("BuildingField").spawned:
if is_instance_valid(building) and building.building_type == GameState.BUILDING_MOTHERSHIP:
mothership = building
controller.current_building = mothership
controller._on_action_pressed()
if not skill_menu.visible:
return _fail("the mothership did not open the research menu")
# --- A. a click selects --------------------------------------------
var shipyards: Vector2 = _node_at("shipyards")
_click(shipyards, true)
if tree_view.selected_id != "shipyards":
return _fail("a left click on a node selected \"%s\" instead of it" % tree_view.selected_id)
_click(shipyards, false)
print("a left click selects the node under it")
# --- B. a second click researches ----------------------------------
if game_state.is_unlocked("shipyards"):
return _fail("shipyards was already researched, so the confirm proves nothing")
_click(shipyards, true)
_click(shipyards, false)
if not game_state.is_unlocked("shipyards"):
return _fail("a second click on the selected node did not research it")
print("a second click on the same node researches it")
# --- C. held drag scrolls, a bare hover does not --------------------
# #219 removed the eight-deep doctrine lane and the tree now fits its own
# view, so there is nothing left to scroll. What is under test here is the
# INPUT rule - drag only while the button is held, wheel both ways - not
# how tall the tree happens to be, so the view is shortened to give the
# gesture something to move. Scrolled to the bottom first, the way the
# menu opens when the content does overflow.
var authored_height: float = tree_view.size.y
tree_view.size.y = authored_height * 0.4
tree_view.scroll_y = tree_view._max_scroll()
if tree_view.scroll_y <= 0.0:
return _fail("the tree still has nothing to scroll, so this proves nothing")
var before: float = tree_view.scroll_y
_move(Vector2(0.0, 120.0))
if not is_equal_approx(tree_view.scroll_y, before):
return _fail("the tree scrolled under a hovering pointer with no button held")
_click(_node_at("shipyards"), true)
_move(Vector2(0.0, 120.0))
var dragged: float = tree_view.scroll_y
if dragged >= before:
return _fail("holding the button and moving did not scroll the tree (%f -> %f)" \
% [before, dragged])
_click(_node_at("shipyards"), false)
_move(Vector2(0.0, 120.0))
if not is_equal_approx(tree_view.scroll_y, dragged):
return _fail("the tree kept scrolling after the button was released")
print("drag scrolls only while the button is held: %.0f -> %.0f" % [before, dragged])
# --- D. the wheel scrolls ------------------------------------------
_wheel(MOUSE_BUTTON_WHEEL_UP)
var wheeled: float = tree_view.scroll_y
if wheeled >= dragged:
return _fail("the wheel did not scroll the tree upward")
_wheel(MOUSE_BUTTON_WHEEL_DOWN)
if tree_view.scroll_y <= wheeled:
return _fail("the wheel did not scroll back down")
print("the wheel scrolls both ways")
# --- E. the panel has no dead band at the bottom --------------------
# Back to the authored height: section C shortened the view to have
# something to scroll, and the layout below is about the real one.
tree_view.size.y = authored_height
tree_view.scroll_y = 0.0
var detail: Control = scene.get_node("UI/SkillMenu/DetailLabel")
var status: Control = scene.get_node("UI/SkillMenu/StatusLabel")
var tree_bottom: float = tree_view.position.y + tree_view.size.y
var readout_gap: float = detail.position.y - tree_bottom
var floor_gap: float = skill_menu.size.y - (status.position.y + status.size.y)
print("tree %.0fpx tall, %.0fpx to the readout, %.0fpx of panel below it" \
% [tree_view.size.y, readout_gap, floor_gap])
if tree_view.size.y <= 0.0:
return _fail("the tree canvas has no height")
if readout_gap < 0.0:
return _fail("the tree canvas overlaps the readout below it")
if readout_gap > GAP_TOLERANCE:
return _fail("%.0fpx of unused panel between the tree and the readout" % readout_gap)
if floor_gap > GAP_TOLERANCE:
return _fail("%.0fpx of dead panel below the readout - the removed buy button's hole" \
% floor_gap)
print("TEST_SKILL_MENU: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_SKILL_MENU: FAIL (%s)" % message)
quit(1)
return true