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:
Loïc Wisniewski 2026-08-10 18:41:58 +02:00
commit 85814ce88c
192 changed files with 9602 additions and 0 deletions

View file

@ -0,0 +1,153 @@
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_pivot_controls.gd
# Covers the fleet-pivot buttons:
# 1. HOLDING one turns the formation continuously, not once per press
# 2. a brief tap still produces a clear minimum step
# 3. pivoting works while the fleet is under way, i.e. the new layout
# survives the joystick's per-frame steering updates instead of being
# overwritten by the old offsets
var scene: Node
var fleet: Node
var controller: Node
var cw: IconButton
var stage: int = 0
var elapsed: float = 0.0
var angle_at_hold_start: float = 0.0
var angle_after_hold: float = 0.0
# GameState (an autoload) is not available as a bare identifier from a
# script that extends SceneTree - fetch it via the tree root instead.
var game_state: Node
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.reset_formation()
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
controller = scene
cw = scene.get_node("UI/PivotCWButton")
func _angle() -> float:
return game_state.formation[1].angle()
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# Hold the button down without releasing it.
angle_at_hold_start = _angle()
cw.pressed_down = true
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.6:
return false
angle_after_hold = _angle()
var turned: float = angle_difference(angle_at_hold_start, angle_after_hold)
# 0.6s at PIVOT_SPEED should be far beyond a single tap step.
var held_ok: bool = absf(turned) > controller.PIVOT_MIN_STEP * 1.5
print("held 0.6s -> turned %.3f rad (min step %.3f) held_ok=%s" % [
absf(turned), controller.PIVOT_MIN_STEP, held_ok
])
if not held_ok:
print("TEST_PIVOT_CONTROLS: FAIL (holding did not turn continuously)")
quit(1)
return true
cw.pressed_down = false
controller.pivot_hold_amount = 0.0
# --- brief tap ------------------------------------------------
var before_tap: float = _angle()
controller._finish_pivot_tap(1.0)
var tapped: float = absf(angle_difference(before_tap, _angle()))
var tap_ok: bool = tapped >= controller.PIVOT_MIN_STEP - 0.001
print("tap -> turned %.3f rad tap_ok=%s" % [tapped, tap_ok])
if not tap_ok:
print("TEST_PIVOT_CONTROLS: FAIL (tap did not produce the minimum step)")
quit(1)
return true
# --- pivot WHILE the joystick is held -------------------------
# Two fingers at once. This is routed through the same raw-touch
# path the real device uses, because Godot's Control GUI treats
# touch as one emulated pointer and would not deliver a second
# simultaneous finger to a button - the exact reason pivoting
# while steering used to do nothing.
var joy: TouchJoystick = scene.get_node("UI/Joystick")
var stick_down: InputEventScreenTouch = InputEventScreenTouch.new()
stick_down.index = 0
stick_down.pressed = true
stick_down.position = joy.global_position + joy.size / 2.0
controller._handle_touch(stick_down)
var stick_drag: InputEventScreenDrag = InputEventScreenDrag.new()
stick_drag.index = 0
stick_drag.position = joy.global_position + joy.size / 2.0 + Vector2(joy.max_radius, 0.0)
controller._handle_drag(stick_drag)
if joy.direction.x < 0.9:
print("TEST_PIVOT_CONTROLS: FAIL (stick did not engage)")
quit(1)
return true
# Second finger lands on the clockwise pivot button.
var pivot_down: InputEventScreenTouch = InputEventScreenTouch.new()
pivot_down.index = 1
pivot_down.pressed = true
pivot_down.position = cw.global_position + cw.size / 2.0
controller._handle_touch(pivot_down)
var both_ok: bool = cw.pressed_down and joy.direction.x > 0.9
print("simultaneous: stick=%s pivot_held=%s" % [joy.direction, cw.pressed_down])
if not both_ok:
print("TEST_PIVOT_CONTROLS: FAIL (cannot pivot while steering)")
quit(1)
return true
angle_at_hold_start = _angle()
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.4:
return false
# Both fingers still down: the formation must have kept turning
# while the stick kept re-issuing move orders every frame.
var turned_while_steering: float = absf(angle_difference(angle_at_hold_start, _angle()))
print("turned %.3f rad while steering" % turned_while_steering)
if turned_while_steering < 0.05:
print("TEST_PIVOT_CONTROLS: FAIL (no rotation while the stick was held)")
quit(1)
return true
# Steering is re-issued every frame; pivot in the middle of it.
fleet.move_in_direction(Vector2.RIGHT)
var before_moving_pivot: Vector2 = game_state.formation[1]
controller._pivot(PI / 2.0)
var after: Vector2 = game_state.formation[1]
fleet.move_in_direction(Vector2.RIGHT)
# The ship's target must reflect the NEW offset even though
# steering re-issued move orders straight afterwards.
var expected: Vector2 = fleet.fleet_target + after
var followed: bool = fleet.ships[1].move_target.distance_to(expected) < 0.01
var rotated: bool = after.distance_to(before_moving_pivot.rotated(PI / 2.0)) < 0.01
print("moving pivot: %s -> %s rotated=%s ship_target_ok=%s" % [
before_moving_pivot, after, rotated, followed
])
if rotated and followed:
print("TEST_PIVOT_CONTROLS: PASS")
quit(0)
else:
print("TEST_PIVOT_CONTROLS: FAIL (pivot lost while moving)")
quit(1)
return true
return false