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
194
GameState.gd
Normal file
194
GameState.gd
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
extends Node
|
||||
|
||||
# Autoload singleton - the one thing that survives change_scene_to_file()
|
||||
# scene swaps between space (Main.tscn) and a planet's surface
|
||||
# (PlanetSurface.tscn). Everything else in either scene gets torn down
|
||||
# and rebuilt from scratch on each swap, so anything that needs to
|
||||
# survive the trip has to live here instead.
|
||||
|
||||
var has_return_position: bool = false
|
||||
var surviving_ship_count: int = 5
|
||||
var return_world_position: Vector2 = Vector2.ZERO
|
||||
var return_camera_zoom: float = 1.0
|
||||
|
||||
var planet_name: String = ""
|
||||
var planet_type: String = ""
|
||||
|
||||
# Explored-chunk history for the world map's fog-of-war reveal. Lives
|
||||
# here (not on ExplorationTracker, which gets destroyed and recreated on
|
||||
# every scene swap) so exploration progress isn't lost every time the
|
||||
# player lands on and leaves a planet.
|
||||
var explored_chunks: Dictionary = {}
|
||||
|
||||
# --- Resources -------------------------------------------------------------
|
||||
|
||||
# Rocks mined from asteroid-planet surfaces, spent on buildings and ships.
|
||||
# Starts stocked for play-testing so buildings can be tried without mining
|
||||
# first - drop this back to 0 for a real playthrough.
|
||||
var rocks: int = 500
|
||||
|
||||
const MOTHERSHIP_COST: int = 20
|
||||
const SHIP_CRAFTER_COST: int = 12
|
||||
const SHIP_COST: int = 6
|
||||
|
||||
# --- Buildings -------------------------------------------------------------
|
||||
|
||||
# Player-placed structures, as [{"type": String, "position": Vector2}].
|
||||
# Buildings exist only in space (Main.tscn) but are stored here so they
|
||||
# survive the scene swap to a planet's surface and back.
|
||||
const BUILDING_MOTHERSHIP: String = "mothership"
|
||||
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
||||
const BUILDING_COMMAND_CENTER: String = "command_center"
|
||||
# Extractors are not placed in space like the others - they are built ON a
|
||||
# planet's surface and tracked separately (see `extractors`).
|
||||
const BUILDING_EXTRACTOR: String = "extractor"
|
||||
|
||||
const COMMAND_CENTER_COST: int = 15
|
||||
const EXTRACTOR_COST: int = 25
|
||||
|
||||
var buildings: Array = []
|
||||
|
||||
|
||||
func add_building(type: String, at_position: Vector2) -> void:
|
||||
buildings.append({"type": type, "position": at_position})
|
||||
|
||||
|
||||
func has_mothership() -> bool:
|
||||
return get_mothership_position() != null
|
||||
|
||||
|
||||
func remove_buildings_of_type(type: String) -> int:
|
||||
var kept: Array = []
|
||||
var removed: int = 0
|
||||
for b in buildings:
|
||||
if b.type == type:
|
||||
removed += 1
|
||||
else:
|
||||
kept.append(b)
|
||||
buildings = kept
|
||||
return removed
|
||||
|
||||
|
||||
func get_mothership_position() -> Variant:
|
||||
# Where a wiped-out fleet gets rebuilt. Returns null when no mothership
|
||||
# has been built yet, so the caller can fall back to the world origin.
|
||||
for b in buildings:
|
||||
if b.type == BUILDING_MOTHERSHIP:
|
||||
return b.position
|
||||
return null
|
||||
|
||||
|
||||
# --- Fleet formation -------------------------------------------------------
|
||||
|
||||
# Where each vessel sits relative to the fleet centre, editable at a command
|
||||
# centre. Lives here (not as a const on Fleet) so a layout the player has
|
||||
# arranged survives the trip to a planet's surface and back, and so a
|
||||
# rebuilt fleet keeps their arrangement.
|
||||
const DEFAULT_FORMATION: Array = [
|
||||
Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32),
|
||||
]
|
||||
|
||||
var formation: Array = DEFAULT_FORMATION.duplicate()
|
||||
|
||||
|
||||
func reset_formation() -> void:
|
||||
formation = DEFAULT_FORMATION.duplicate()
|
||||
|
||||
|
||||
func rotate_formation(radians: float) -> void:
|
||||
# Pivots every slot about the fleet centre, keeping the shape intact
|
||||
# and only changing which way it faces.
|
||||
for i in range(formation.size()):
|
||||
formation[i] = formation[i].rotated(radians)
|
||||
|
||||
|
||||
# --- Extractors and their haulers -----------------------------------------
|
||||
|
||||
# An extractor sits on a planet and runs a single automated hauler back and
|
||||
# forth to the mothership, delivering ore on each arrival.
|
||||
#
|
||||
# The round trip is SIMULATED HERE, in the autoload, rather than by any
|
||||
# node: it advances whether the player is in space, on a planet's surface,
|
||||
# or far enough away that nothing about the route is loaded. `progress` is
|
||||
# a fraction of one full there-and-back cycle, so a hauler's drawn position
|
||||
# is derived from the same number that decides when ore is delivered - the
|
||||
# visual arrival and the payout cannot drift apart, because they are the
|
||||
# same event.
|
||||
#
|
||||
# Entries are {"position": Vector2 (the planet's position in SPACE),
|
||||
# "planet_name": String, "progress": float}.
|
||||
const HAULER_SPEED: float = 260.0
|
||||
const HAULER_YIELD: int = 4
|
||||
# Progress value at which the hauler reaches the mothership: exactly half
|
||||
# way through the cycle, the outbound leg being the first half.
|
||||
const HAULER_ARRIVAL: float = 0.5
|
||||
|
||||
var extractors: Array = []
|
||||
|
||||
|
||||
func has_extractor_at(planet_name: String) -> bool:
|
||||
for e in extractors:
|
||||
if e.planet_name == planet_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func add_extractor(planet_name: String, at_position: Vector2) -> void:
|
||||
extractors.append({
|
||||
"position": at_position,
|
||||
"planet_name": planet_name,
|
||||
"progress": 0.0,
|
||||
})
|
||||
|
||||
|
||||
func get_hauler_route_time(extractor: Dictionary) -> float:
|
||||
# Full cycle: out to the mothership and back again.
|
||||
var mothership: Variant = get_mothership_position()
|
||||
if mothership == null:
|
||||
return 0.0
|
||||
var distance: float = extractor.position.distance_to(mothership)
|
||||
if distance < 1.0:
|
||||
return 0.0
|
||||
return (distance * 2.0) / HAULER_SPEED
|
||||
|
||||
|
||||
func get_hauler_position(extractor: Dictionary) -> Variant:
|
||||
# Where the hauler is right now, derived purely from `progress`. Returns
|
||||
# null when there is no route (no mothership built yet).
|
||||
var mothership: Variant = get_mothership_position()
|
||||
if mothership == null:
|
||||
return null
|
||||
var t: float = extractor.progress
|
||||
if t <= HAULER_ARRIVAL:
|
||||
return extractor.position.lerp(mothership, t / HAULER_ARRIVAL)
|
||||
return mothership.lerp(extractor.position, (t - HAULER_ARRIVAL) / (1.0 - HAULER_ARRIVAL))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Runs in the autoload, so extraction keeps earning while the player is
|
||||
# on a planet's surface or nowhere near the route.
|
||||
if extractors.is_empty():
|
||||
return
|
||||
for e in extractors:
|
||||
var cycle: float = get_hauler_route_time(e)
|
||||
if cycle <= 0.0:
|
||||
continue # nothing to deliver to yet
|
||||
var before: float = e.progress
|
||||
e.progress += delta / cycle
|
||||
# Ore lands the instant the hauler reaches the mothership - the same
|
||||
# instant get_hauler_position() puts it there.
|
||||
if before < HAULER_ARRIVAL and e.progress >= HAULER_ARRIVAL:
|
||||
rocks += HAULER_YIELD
|
||||
while e.progress >= 1.0:
|
||||
e.progress -= 1.0
|
||||
|
||||
|
||||
func can_afford(cost: int) -> bool:
|
||||
return rocks >= cost
|
||||
|
||||
|
||||
func spend(cost: int) -> bool:
|
||||
if not can_afford(cost):
|
||||
return false
|
||||
rocks -= cost
|
||||
return true
|
||||
Loading…
Reference in a new issue