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

118 lines
4.1 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_crafter_animation.gd
# The ship crafter is drawn as a factory with a hull being assembled on the
# line. Verifies:
# 1. a crafter spawned the normal way (BuildingField, from GameState)
# actually animates - the animation is what tells the player what the
# building does. The mothership animates too (reactor core, orbiting
# satellites)
# 2. the command centre stays static, because per-frame redraws of art
# that never changes were a measurable framerate cost earlier on
# 3. the hull assembles progressively over the cycle rather than popping
# in whole, and every piece is on before the assembly phase ends
# 4. the cycle loops without anim_time growing without bound
var scene: Node
var game_state: Node
var field: Node
var stage: int = 0
var elapsed: float = 0.0
var crafter: Node2D
var time_at_start: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.rocks = 200
game_state.buildings.clear()
game_state.extractors.clear()
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
field = scene.get_node("BuildingField")
func _find(type: String) -> Node2D:
for b in field.spawned:
if is_instance_valid(b) and b.building_type == type:
return b
return null
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(500.0, 0.0))
field.place(GameState.BUILDING_MOTHERSHIP, Vector2(-500.0, 0.0))
field.place(GameState.BUILDING_COMMAND_CENTER, Vector2(0.0, 500.0))
elapsed = 0.0
stage = 1
return false
1:
# Spawning is deferred, so give the tree a frame to attach them.
if elapsed < 0.2:
return false
crafter = _find(GameState.BUILDING_SHIP_CRAFTER)
if crafter == null:
print("TEST_CRAFTER_ANIMATION: FAIL (crafter never spawned)")
quit(1)
return true
var animated: bool = crafter.is_processing()
var mothership: Node2D = _find(GameState.BUILDING_MOTHERSHIP)
var mothership_animates: bool = mothership != null and mothership.is_processing()
var command: Node2D = _find(GameState.BUILDING_COMMAND_CENTER)
var command_static: bool = command != null and not command.is_processing()
print("crafter animates=%s mothership animates=%s command centre static=%s" % [
animated, mothership_animates, command_static
])
if not (animated and mothership_animates and command_static):
print("TEST_CRAFTER_ANIMATION: FAIL (wrong buildings are animating)")
quit(1)
return true
# --- the hull assembles piece by piece ------------------------
var total: int = crafter.SHIP_PARTS.size()
var counts: Array = []
for p in [0.0, 0.1, 0.35, 0.55, 0.9, 1.0]:
counts.append(crafter.attached_part_count(p))
var progressive: bool = counts[0] == 0 and counts[-1] == total
for i in range(1, counts.size()):
if counts[i] < counts[i - 1]:
progressive = false
# Nothing may still be missing when the welding phase ends, or
# the hull would launch half-built.
var complete_before_launch: bool = crafter.attached_part_count(1.0) == total
print("parts attached across the phase: %s (of %d) progressive=%s" % [
counts, total, progressive
])
if not (progressive and complete_before_launch and counts[1] < total):
print("TEST_CRAFTER_ANIMATION: FAIL (hull does not build up gradually)")
quit(1)
return true
time_at_start = crafter.anim_time
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.4:
return false
var advanced: bool = crafter.anim_time != time_at_start
var bounded: bool = crafter.anim_time >= 0.0 and crafter.anim_time < crafter.CYCLE
print("anim_time %.3f -> %.3f (cycle %.2f) advanced=%s bounded=%s" % [
time_at_start, crafter.anim_time, crafter.CYCLE, advanced, bounded
])
if advanced and bounded:
print("TEST_CRAFTER_ANIMATION: PASS")
quit(0)
else:
print("TEST_CRAFTER_ANIMATION: FAIL (animation clock stalled or ran away)")
quit(1)
return true
return false