spaceship/tests/test_keyboard_input.gd
Loïc Wisniewski 85814ce88c Initial commit: SpaceshipTest, a top-down space RTS in Godot 4.7.1
A fleet of neon-outlined ships explores a procedurally generated, seeded
infinite world: tap/joystick steering, pinch zoom, formation pivoting,
planet landings, an ore economy with extractors and haulers, and player
built structures (mothership, ship crafter, command centre).

All art is hand-drawn with draw_* calls rather than textures, and the
backgrounds are camera-relative shaders so they stay precise at large
world coordinates.

Includes a 35-test headless suite (tests/run_tests.sh) plus screenshot
tooling that renders real frames through Xvfb and Mesa softpipe, since
--headless has no GPU.

Excluded from version control: .godot/ (import cache, and it holds
export credentials), builds/, android/build/ and packaged APKs - all
regenerable, and ~325 MB combined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:58 +02:00

145 lines
4.9 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_keyboard_input.gd
# Desktop/web controls. The phone build is driven entirely by touch, but the
# web build is played with a keyboard and mouse, so WASD (and the arrows)
# must steer the fleet, Q/E must pivot the formation, and releasing the keys
# must stop - the stick has a centred-again signal to hook, the keyboard has
# nothing equivalent, so a missed release would leave the fleet flying off
# forever.
#
# Verifies:
# 1. W/A/S/D and the arrow keys each produce the right heading, and
# diagonals combine and normalise
# 2. holding a key actually moves the fleet that way
# 3. releasing every key stops it
# 4. Q and E turn the formation, in opposite directions
# 5. the on-screen stick is not taken away on a touch device
var scene: Node
var controller: Node
var fleet: Node
var joystick: Control
var stage: int = 0
var elapsed: float = 0.0
var start_centroid: Vector2 = Vector2.ZERO
var moving_centroid: Vector2 = Vector2.ZERO
func _initialize() -> void:
root.get_node("GameState").reset_formation()
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
controller = scene
fleet = scene.get_node("Fleet")
joystick = scene.get_node("UI/Joystick")
func _key(code: int, pressed: bool) -> void:
var event: InputEventKey = InputEventKey.new()
event.keycode = code
event.pressed = pressed
controller._handle_key(event)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# --- headings -------------------------------------------------
var cases: Array = [
{"keys": [KEY_W], "expect": Vector2.UP},
{"keys": [KEY_S], "expect": Vector2.DOWN},
{"keys": [KEY_A], "expect": Vector2.LEFT},
{"keys": [KEY_D], "expect": Vector2.RIGHT},
{"keys": [KEY_UP], "expect": Vector2.UP},
{"keys": [KEY_RIGHT], "expect": Vector2.RIGHT},
{"keys": [KEY_W, KEY_D], "expect": Vector2(1, -1).normalized()},
{"keys": [KEY_W, KEY_S], "expect": Vector2.ZERO},
]
for case in cases:
for k in case.keys:
_key(k, true)
var got: Vector2 = controller.keyboard_direction()
var ok: bool = got.distance_to(case.expect) < 0.001
if not ok:
print("keys %s -> %s, expected %s" % [case.keys, got, case.expect])
print("TEST_KEYBOARD_INPUT: FAIL (wrong heading)")
quit(1)
return true
for k in case.keys:
_key(k, false)
print("all %d key combinations produced the right heading" % cases.size())
# The stick must survive on a touch device.
var stick_kept: bool = joystick.visible
print("stick still visible after keyboard use: %s (touchscreen=%s)" % [
stick_kept, DisplayServer.is_touchscreen_available()
])
if DisplayServer.is_touchscreen_available() and not stick_kept:
print("TEST_KEYBOARD_INPUT: FAIL (took the stick away on a touch device)")
quit(1)
return true
# --- holding a key flies the fleet ----------------------------
start_centroid = fleet.get_centroid()
_key(KEY_D, true)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.6:
return false
moving_centroid = fleet.get_centroid()
var travelled: float = moving_centroid.x - start_centroid.x
var sideways: float = absf(moving_centroid.y - start_centroid.y)
print("held D for 0.6s: moved x=%.1f y-drift=%.1f" % [travelled, sideways])
if travelled < 50.0 or sideways > 30.0:
print("TEST_KEYBOARD_INPUT: FAIL (holding a key did not fly the fleet that way)")
quit(1)
return true
# --- releasing stops ------------------------------------------
_key(KEY_D, false)
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.9:
return false
var coasted: float = fleet.get_centroid().x - moving_centroid.x
var still_steering: bool = fleet.steer_direction != Vector2.ZERO
print("0.9s after release: drifted a further %.1f, still steering=%s" % [
coasted, still_steering
])
if still_steering:
print("TEST_KEYBOARD_INPUT: FAIL (fleet kept flying after the key was released)")
quit(1)
return true
# --- Q/E pivot ------------------------------------------------
var game_state: Node = root.get_node("GameState")
var before: float = game_state.formation[1].angle()
_key(KEY_E, true)
_key(KEY_E, false)
var after_cw: float = game_state.formation[1].angle()
_key(KEY_Q, true)
_key(KEY_Q, false)
var after_ccw: float = game_state.formation[1].angle()
var cw_turn: float = angle_difference(before, after_cw)
var ccw_turn: float = angle_difference(after_cw, after_ccw)
print("E turned %.3f rad, Q turned %.3f rad" % [cw_turn, ccw_turn])
if absf(cw_turn) < 0.01 or absf(ccw_turn) < 0.01 or signf(cw_turn) == signf(ccw_turn):
print("TEST_KEYBOARD_INPUT: FAIL (Q/E did not pivot in opposite directions)")
quit(1)
return true
print("TEST_KEYBOARD_INPUT: PASS")
quit(0)
return true
return false