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>
This commit is contained in:
commit
85814ce88c
192 changed files with 9602 additions and 0 deletions
145
tests/test_keyboard_input.gd
Normal file
145
tests/test_keyboard_input.gd
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
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
|
||||
Loading…
Reference in a new issue