spaceship/tests/test_desktop_controls.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

188 lines
7.2 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_desktop_controls.gd
# Forgejo #118 and #121: two raw-touch controls that a mouse could not
# reach.
#
# Both read InputEventScreenTouch/ScreenDrag, and the project no longer turns
# clicks into them - the same routing gap #93 found in the research tree.
# The boost was unreachable on a desktop and the formation could not be
# rearranged at all.
#
# Driven through the REAL routing in each case (GameController for the
# fader, BuildController for the editor) rather than by calling the controls
# directly, because the routing is what was missing - a test that called
# handle_mouse_button() itself would have passed against both reports.
#
# Covers:
# A. #118 - a click on the fader takes hold of it, dragging moves the
# boost, and releasing lets go
# B. the fader claims the press, so dragging the boost does not also zoom
# the world behind it
# C. #121 - a click grabs a formation slot, moving drags it, and the
# formation really changes
# D. releasing drops the slot, and moving afterwards does not keep dragging
# E. the wheel zooms the formation panel, which is what replaces the pinch
var game_state: Node
var scene: Node
var controller: Node
var fleet: Node
var fader: Control
var editor: Control
var formation_menu: Control
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_waves()
game_state.reset_formation()
game_state.intro_active = false
game_state.vessel_levels["unlock_distorder"] = 1
# #219: fleet capacity is built now - one berth free, one per Fleet
# Office. This test flies more than one vessel, so it needs the offices
# standing before the fleet spawns into them.
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
fleet = scene.get_node("Fleet")
controller = scene.get_node("BuildController")
fader = scene.get_node("UI/SpeedFader")
editor = scene.get_node("UI/FormationMenu/Editor")
formation_menu = scene.get_node("UI/FormationMenu")
func _fail(message: String) -> bool:
print("TEST_DESKTOP_CONTROLS: FAIL (%s)" % message)
quit(1)
return true
func _click(at: Vector2, pressed: bool, target: Node) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = at
target._unhandled_input(event)
func _move(to: Vector2, target: Node) -> void:
var event: InputEventMouseMotion = InputEventMouseMotion.new()
event.position = to
target._unhandled_input(event)
func _wheel(button: int, at: Vector2) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = button
event.pressed = true
event.position = at
controller._unhandled_input(event)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# The fader only exists where the fleet can actually boost.
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER]))
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
if not fader.visible:
return _fail("the fleet has an Energy Propulser and the fader is still hidden")
if game_state.max_fleet_speed_multiplier <= 1.0:
return _fail("the boost has no range to move through")
# --- A. click, drag, release -----------------------------
var top: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.track_margin + 2.0)
var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.size.y - fader.track_margin - 2.0)
_click(bottom, true, scene)
if not fader.is_active():
return _fail("a click on the fader did not take hold of it")
var at_bottom: float = game_state.fleet_speed_multiplier
_move(top, scene)
var at_top: float = game_state.fleet_speed_multiplier
if at_top <= at_bottom:
return _fail("dragging the fader from bottom to top left the boost at %.2f" % at_top)
_click(top, false, scene)
if fader.is_active():
return _fail("the fader is still held after the button came up")
_move(bottom, scene)
if not is_equal_approx(game_state.fleet_speed_multiplier, at_top):
return _fail("the fader kept following the pointer after release")
print("fader: %.2fx at the bottom, %.2fx at the top, released cleanly" \
% [at_bottom, at_top])
# --- B. it claims the press ------------------------------
var zoom_before: float = scene.get_node("Camera2D").zoom.x
_click(bottom, true, scene)
_click(bottom, false, scene)
if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before):
return _fail("pressing the fader also moved the camera behind it")
print("the press stops at the fader; the world behind it is untouched")
# --- C/D/E. the formation editor -------------------------
formation_menu.visible = true
editor.queue_redraw()
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
var slot_before: Vector2 = game_state.formation[0]
var grab: Vector2 = editor.global_position + editor._slot_screen_pos(0)
_click(grab, true, controller)
if editor.dragging_index != 0:
return _fail("a click on the first formation slot grabbed %d" % editor.dragging_index)
_move(grab + Vector2(70.0, 40.0), controller)
if game_state.formation[0] == slot_before:
return _fail("dragging the slot did not move it")
var dragged: Vector2 = game_state.formation[0]
print("slot 0 moved from %s to %s under a mouse" % [str(slot_before), str(dragged)])
# --- D. release lets go ----------------------------------
_click(grab + Vector2(70.0, 40.0), false, controller)
if editor.dragging_index != -1:
return _fail("the slot is still held after the button came up")
_move(grab + Vector2(200.0, 200.0), controller)
if game_state.formation[0] != dragged:
return _fail("the slot kept following the pointer after release")
print("released, and the slot stopped following")
# --- E. the wheel zooms ----------------------------------
var scale_before: float = editor.scale_factor
_wheel(MOUSE_BUTTON_WHEEL_UP, editor.global_position + editor.size * 0.5)
if editor.scale_factor <= scale_before:
return _fail("the wheel did not zoom the formation panel in")
_wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5)
_wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5)
if editor.scale_factor >= scale_before:
return _fail("the wheel did not zoom back out")
print("the wheel zooms the panel both ways (%.2f -> %.2f)" \
% [scale_before, editor.scale_factor])
print("TEST_DESKTOP_CONTROLS: PASS")
quit(0)
return true
return false