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

102
Rock.gd Normal file
View file

@ -0,0 +1,102 @@
extends Area2D
# A gatherable crystal of asteroid ore lying on a planet's surface. Picked
# up by flying any ship over it; the fleet doesn't need to stop.
#
# Deliberately drawn as a PERFECT HEXAGON rather than the irregular lumpy
# polygon Asteroid.gd uses. The two are never on screen
# together (asteroids are space-only, rocks surface-only), so the player
# can only tell "collect this" from "this hurts" by learned shape language:
# irregular and jagged reads as a natural hazard, regular and faceted reads
# as something valuable and meant to be picked up.
#
# It also drifts toward a nearby ship (see _physics_process). Nothing
# harmful moves helpfully toward you, so the pull doubles as a readability
# cue on top of making gathering feel less fiddly.
signal collected
@export var attract_radius: float = 220.0
@export var attract_speed: float = 260.0
@export var attract_accel: float = 420.0
var shape_points: PackedVector2Array = PackedVector2Array()
var facet_points: PackedVector2Array = PackedVector2Array()
var size_scale: float = 1.0
var velocity: Vector2 = Vector2.ZERO
func _ready() -> void:
add_to_group("rocks")
z_index = -1
area_entered.connect(_on_area_entered)
queue_redraw()
func build_shape(shape_seed: int) -> void:
# Only the SIZE varies, never the silhouette: every ore node is the same
# regular hexagon, which is the whole point of the cue.
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
rng.seed = shape_seed
size_scale = rng.randf_range(0.85, 1.25)
var r: float = 13.0 * size_scale
shape_points = PackedVector2Array()
for i in range(6):
var a: float = (float(i) / 6.0) * TAU - PI / 2.0
shape_points.append(Vector2(cos(a), sin(a)) * r)
# One flat-to-flat line across the middle, so it reads as a cut face
# rather than a plain outline at small sizes.
facet_points = PackedVector2Array([
shape_points[5], shape_points[2],
])
queue_redraw()
func _physics_process(delta: float) -> void:
# Drift toward the closest ship inside attract_radius. No redraw here:
# position is a transform property and the crystal's shape is fixed.
var target: Node2D = _nearest_ship()
if target == null:
velocity = velocity.move_toward(Vector2.ZERO, attract_accel * delta)
else:
var desired: Vector2 = (target.position - position).normalized() * attract_speed
velocity = velocity.move_toward(desired, attract_accel * delta)
position += velocity * delta
func _nearest_ship() -> Node2D:
var nearest: Node2D = null
var best: float = attract_radius
for ship in get_tree().get_nodes_in_group("player_ships"):
if not is_instance_valid(ship):
continue
var d: float = position.distance_to(ship.position)
if d <= best:
best = d
nearest = ship
return nearest
func _on_area_entered(area: Area2D) -> void:
if area.is_in_group("player_ships"):
GameState.rocks += 1
collected.emit()
queue_free()
func _draw() -> void:
if shape_points.is_empty():
return
# Deep teal with a near-black core: a dark, saturated colour reads
# clearly against the bright lunar regolith (sampled at ~0.75 grey),
# where the original warm yellow disappeared into it.
var glow: Color = Color(0.0, 0.62, 0.7)
var core: Color = Color(0.0, 0.12, 0.15)
NeonDraw.draw_glowing_polygon(self, shape_points, glow, 1.0, core)
# One line only - the hexagon has a single flat-to-flat facet. This used
# to draw two lines from a 4-point array; the hexagon rewrite left only
# 2 points, so the second line indexed out of bounds and every rock
# threw on every draw.
if facet_points.size() >= 2:
NeonDraw.draw_glowing_line(self, facet_points[0], facet_points[1], glow, 0.7, 0.3, core)