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
102
Rock.gd
Normal file
102
Rock.gd
Normal 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)
|
||||
Loading…
Reference in a new issue