Gameplay and UI - Research tree at the mothership gates which structures can be built and grants permanent fleet-capacity upgrades (GameState.SKILL_TREE, SkillTreeView). Node depth is derived from prerequisites, so the layout cannot drift out of step with the dependencies. - Building info popup before committing to a build, with per-building cost and description. - Ship crafter redrawn as a space factory with an assembly animation; mothership redrawn as a radial flagship; command centre as a plotting hub. - Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build. - UI and font scaled up for phone use, via a custom Theme resource - gui/theme/default_font_size does not reach the default theme. - Asteroids no longer spawn on top of the fleet's start position. - Fleet formation origin derived from the layout's own offsets rather than the centroid, fixing the lurch when a lopsided formation started moving. - Fleet.stop() brakes from the ship's TRUE motion (velocity + fleet_velocity). Ship zeroes its velocity when seated in its slot even while the anchor drags that slot forward, so braking from velocity alone stopped the fleet dead instead of coasting - and whether it did depended on the frame rate. Animation architecture (issue #10) - Buildings, projectiles and explosion VFX no longer animate by redrawing. Static geometry is rasterised once in _draw(); moving parts are NeonPiece children animated through node properties, which reuse the cached geometry. Every NeonDraw call expands into four antialiased polylines, so redrawing to move something re-tessellated all of it. Test suite (issue #8): 1075s -> ~55s - Runs from a copy on Termux's own filesystem. The project lives on Android's FUSE bridge, read through proot, where load("res://Main.tscn") alone cost 8.5s per test against ~2s natively - and nearly every test loads it. - Runs 6 tests at a time, in a single proot-distro login, with output buffered per test and printed only on failure. - --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's result no longer depends on how busy the machine is. Without it, parallelism made two tests fail on 2 of 3 runs. - A test passes only if it both exits 0 and prints ": PASS". - 38 tests, green on three consecutive runs. Bundles Carrois Gothic SC (Apache 2.0) with its licence text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.2 KiB
GDScript
114 lines
4.2 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_asteroid_safe_spawn.gd
|
|
# Asteroid chunks are populated around wherever the fleet is - including the
|
|
# chunk it is standing in - and positions used to be uniform within a chunk.
|
|
# So an asteroid could be created directly on top of a ship and destroy it
|
|
# with no chance to react. Worst on the frame a scene loads: coming back from
|
|
# a planet's surface could cost a vessel instantly, which is also what made
|
|
# test_return_position_matches_planet fail intermittently.
|
|
#
|
|
# Verifies:
|
|
# 1. _safe_position never returns a point inside the bubble, for candidates
|
|
# all around and including the exact centre. This is checked directly
|
|
# rather than by looking at where asteroids happened to land: with ~54
|
|
# asteroids spread over 25 chunks, any single run only rarely produces a
|
|
# near miss, so an observational check passes just as happily with the
|
|
# guard removed and proves nothing.
|
|
# 2. candidates already outside the bubble are left exactly where they are,
|
|
# so the field is not distorted away from the fleet
|
|
# 3. the field still spawns a full complement, and a stationary fleet loses
|
|
# no ships
|
|
|
|
var scene: Node
|
|
var fleet: Node
|
|
var field: Node
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
var ships_at_start: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
root.get_node("GameState").reset_formation()
|
|
scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
field = scene.get_node("AsteroidField")
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
ships_at_start = fleet.ships.size()
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
# Let the field populate around the fleet a few times over.
|
|
if elapsed < 1.5:
|
|
return false
|
|
|
|
var centre: Vector2 = fleet.get_centroid()
|
|
var safe: float = field.safe_spawn_radius
|
|
|
|
# --- the guard itself, deterministically ----------------------
|
|
var worst_inside: float = 1e9
|
|
var moved_an_outside_one: bool = false
|
|
for i in range(72):
|
|
var a: float = (float(i) / 72.0) * TAU
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
# Candidates well inside, just inside, and exactly on centre.
|
|
for d in [0.0, 1.0, safe * 0.25, safe * 0.99]:
|
|
var got: Vector2 = field._safe_position(centre + dir * d, centre)
|
|
worst_inside = minf(worst_inside, got.distance_to(centre))
|
|
# Candidates already outside must be returned untouched.
|
|
var outside: Vector2 = centre + dir * (safe * 1.5)
|
|
if field._safe_position(outside, centre).distance_to(outside) > 0.001:
|
|
moved_an_outside_one = true
|
|
print("closest result for an inside candidate: %.1f (safe radius %.1f)" % [
|
|
worst_inside, safe
|
|
])
|
|
if worst_inside < safe - 0.001:
|
|
print("TEST_ASTEROID_SAFE_SPAWN: FAIL (guard let a spawn land inside the fleet's bubble)")
|
|
quit(1)
|
|
return true
|
|
if moved_an_outside_one:
|
|
print("TEST_ASTEROID_SAFE_SPAWN: FAIL (guard displaced an asteroid that was already clear)")
|
|
quit(1)
|
|
return true
|
|
|
|
var asteroids: Array = get_nodes_in_group("asteroids")
|
|
var closest: float = 1e9
|
|
for a in asteroids:
|
|
if is_instance_valid(a):
|
|
closest = minf(closest, a.position.distance_to(centre))
|
|
|
|
print("%d asteroids, closest is %.1f units from the fleet (safe radius %.1f)" % [
|
|
asteroids.size(), closest, safe
|
|
])
|
|
if asteroids.is_empty():
|
|
print("TEST_ASTEROID_SAFE_SPAWN: FAIL (field spawned nothing - test proves nothing)")
|
|
quit(1)
|
|
return true
|
|
# Asteroids drift, so one may have travelled inward since it was
|
|
# created. Allow for that, but nothing should be anywhere near
|
|
# on top of the fleet.
|
|
if closest < safe * 0.5:
|
|
print("TEST_ASTEROID_SAFE_SPAWN: FAIL (asteroid created on top of the fleet)")
|
|
quit(1)
|
|
return true
|
|
|
|
var kept_ships: bool = fleet.ships.size() == ships_at_start
|
|
print("ships %d -> %d (kept=%s)" % [ships_at_start, fleet.ships.size(), kept_ships])
|
|
if not kept_ships:
|
|
print("TEST_ASTEROID_SAFE_SPAWN: FAIL (a stationary fleet lost a ship to a spawn)")
|
|
quit(1)
|
|
return true
|
|
|
|
print("TEST_ASTEROID_SAFE_SPAWN: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|