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
124
PlanetAtmosphereController.gd
Normal file
124
PlanetAtmosphereController.gd
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
extends Node2D
|
||||
|
||||
# Space-side: bootstraps the fleet on Main.tscn startup (a fresh 5-ship
|
||||
# fleet at the origin on first launch, or GameState's surviving ship
|
||||
# count/position if we just returned from a planet's surface), shows an
|
||||
# "enter atmosphere" prompt (name + button) when the fleet is standing on
|
||||
# a landable planet, and - on press - fades out and swaps to a completely
|
||||
# separate PlanetSurface.tscn scene (not an overlay toggled in place), so
|
||||
# asteroids/enemies genuinely cannot exist there rather than merely being
|
||||
# hidden. Fleet size and the world position/zoom to return to are handed
|
||||
# off via the GameState autoload, since change_scene_to_file() tears down
|
||||
# this whole scene tree.
|
||||
|
||||
@export var fade_duration: float = 0.4
|
||||
@export var prompt_debounce_time: float = 0.15
|
||||
|
||||
@onready var planet_field: Node2D = get_node("../PlanetField")
|
||||
@onready var asteroid_field: Node2D = get_node("../AsteroidField")
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
@onready var camera: Camera2D = get_node("../Camera2D")
|
||||
@onready var planet_name_label: Label = get_node("../UI/PlanetNameLabel")
|
||||
@onready var enter_button: IconButton = get_node("../UI/EnterAtmosphereButton")
|
||||
@onready var world_map: Control = get_node("../UI/WorldMap")
|
||||
@onready var build_controller: Node2D = get_node("../BuildController")
|
||||
@onready var fade_rect: ColorRect = get_node("../FadeOverlay/FadeRect")
|
||||
|
||||
var current_planet: Node2D = null
|
||||
var transitioning: bool = false
|
||||
var pending_planet: Node2D = null
|
||||
var pending_time: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
enter_button.pressed.connect(_on_enter_pressed)
|
||||
|
||||
if GameState.has_return_position:
|
||||
fleet.spawn_ships(GameState.surviving_ship_count, GameState.return_world_position)
|
||||
camera.position = GameState.return_world_position
|
||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||
GameState.has_return_position = false
|
||||
else:
|
||||
fleet.spawn_ships(5, Vector2.ZERO)
|
||||
|
||||
# AsteroidField/PlanetField normally only populate nearby chunks once
|
||||
# their own check_interval timer elapses (~1s) - fine during normal
|
||||
# continuous play (things appear far off-screen, gradually), but
|
||||
# right after a fresh scene load it means an empty-looking world for
|
||||
# a second before asteroids/the planet visibly pop in. Force an
|
||||
# immediate synchronous populate now that the fleet is correctly
|
||||
# positioned, so nothing is missing on the very first visible frame.
|
||||
asteroid_field.update_chunks()
|
||||
planet_field.update_chunks()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if transitioning:
|
||||
return
|
||||
|
||||
if world_map.visible or build_controller.is_showing_building_prompt():
|
||||
# EnterAtmosphereButton shares one "action" slot with the map's
|
||||
# RecenterButton and with BuildController's building prompt - it doesn't make sense to enter
|
||||
# a planet's atmosphere while looking at the star map anyway, so
|
||||
# suppress the prompt entirely rather than let it sit hidden
|
||||
# behind the map's backdrop only to pop back in the instant the
|
||||
# map closes.
|
||||
current_planet = null
|
||||
pending_planet = null
|
||||
planet_name_label.visible = false
|
||||
enter_button.visible = false
|
||||
return
|
||||
|
||||
# Debounced: only commit a visible/hidden change once the raw
|
||||
# landing-detection result has stayed the same for prompt_debounce_time
|
||||
# in a row, instead of reacting to every single frame's reading. Belt
|
||||
# and suspenders on top of get_planet_fleet_is_on() already using the
|
||||
# fleet's centroid (a single stable number - see Planet.gd) rather
|
||||
# than any individual ship's position, so this only has to absorb
|
||||
# genuine sub-debounce-window boundary crossings, not constant noise.
|
||||
var detected_planet: Node2D = planet_field.get_planet_fleet_is_on()
|
||||
if detected_planet != pending_planet:
|
||||
pending_planet = detected_planet
|
||||
pending_time = 0.0
|
||||
else:
|
||||
pending_time += delta
|
||||
|
||||
if pending_time < prompt_debounce_time or pending_planet == current_planet:
|
||||
return
|
||||
|
||||
current_planet = pending_planet
|
||||
if current_planet != null:
|
||||
planet_name_label.text = current_planet.planet_name
|
||||
planet_name_label.visible = true
|
||||
enter_button.visible = true
|
||||
else:
|
||||
planet_name_label.visible = false
|
||||
enter_button.visible = false
|
||||
|
||||
|
||||
func _on_enter_pressed() -> void:
|
||||
if transitioning or current_planet == null:
|
||||
return
|
||||
transitioning = true
|
||||
planet_name_label.visible = false
|
||||
enter_button.visible = false
|
||||
|
||||
GameState.has_return_position = true
|
||||
GameState.surviving_ship_count = fleet.ships.size()
|
||||
# The planet's own position, not fleet.get_centroid() - the fleet only
|
||||
# needs to be somewhere within the planet's landing radius (not
|
||||
# centered exactly on it) for the prompt to show, so the centroid can
|
||||
# be tens of units off from the planet. Returning should always place
|
||||
# the fleet back at the planet itself.
|
||||
GameState.return_world_position = current_planet.position
|
||||
GameState.return_camera_zoom = camera.zoom.x
|
||||
GameState.planet_name = current_planet.planet_name
|
||||
GameState.planet_type = current_planet.planet_type
|
||||
|
||||
var tween: Tween = create_tween()
|
||||
tween.tween_property(fade_rect, "color:a", 1.0, fade_duration)
|
||||
tween.tween_callback(_enter_surface)
|
||||
|
||||
|
||||
func _enter_surface() -> void:
|
||||
get_tree().change_scene_to_file("res://PlanetSurface.tscn")
|
||||
Loading…
Reference in a new issue