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
153
tests/test_pivot_controls.gd
Normal file
153
tests/test_pivot_controls.gd
Normal 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
|
||||
Loading…
Reference in a new issue