Loading a save fell through to the brand-new-game branch in PlanetAtmosphereController, which spawns STARTING_SHIP_COUNT interceptors at the world origin. So resuming stranded the player in empty space with the wrong ships while their base sat wherever they had left it - thousands of units away on a charted map. fleet_position was in the save all along, but nothing ever read it: only Fleet writes it, as a mirror for payment-range checks. A resumed run now spawns at get_mothership_position(). That is already the canonical home - BuildController._check_fleet_wipe rebuilds a wiped fleet there - so this is the existing answer to "where does the fleet belong", not a new one. It falls back to the position the save was written at, and only then to the origin, because a run can have no mothership standing. resumed_from_save is a one-shot handoff like has_return_position, and is deliberately NOT saved: it describes this launch, not the run. Verified by reverting the branch: "resumed at (0.0, -48.0), but the mothership is at (7400.0, -3100.0)". One note for whoever writes the next fleet test: asserting a three-ship crew proves nothing without capacity. Since #219 the fleet holds BASE_FLEET_SIZE (1) plus one berth per supplied fleet office, so spawn_ships clamps to 1 and the assertion passes against the wrong number. Case A gives the save two offices and checks max_fleet_size() first. 176/176 at JOBS=3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
161 lines
7.2 KiB
GDScript
161 lines
7.2 KiB
GDScript
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,
|
|
GameState.surviving_vessel_types)
|
|
camera.position = GameState.return_world_position
|
|
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
|
GameState.has_return_position = false
|
|
elif GameState.resumed_from_save:
|
|
# #227: a resumed run used to fall through to the brand-new-game
|
|
# branch below, which put the STARTING fleet at the world origin -
|
|
# so loading a save stranded the player in empty space with the
|
|
# wrong ships while their base sat wherever they had left it.
|
|
#
|
|
# The mothership is where a wiped-out fleet is already rebuilt (see
|
|
# BuildController._check_fleet_wipe), so it is the run's home whether
|
|
# or not this is a resume. Falls back to the position the save was
|
|
# written at, and only then to the origin, because a run can have no
|
|
# mothership standing.
|
|
var home: Variant = GameState.get_mothership_position()
|
|
var at: Vector2 = home if home != null else GameState.fleet_position
|
|
fleet.spawn_ships(GameState.fleet_vessel_types.size(), at,
|
|
GameState.fleet_vessel_types)
|
|
camera.position = at
|
|
GameState.resumed_from_save = false
|
|
elif GameState.intro_active:
|
|
# #108: the prologue opens with a convoy, and crews it itself. This
|
|
# node still runs first, so spawning the ordinary opening fleet here
|
|
# would be immediately thrown away - and worse, would be the fleet
|
|
# for the one frame before the director replaced it.
|
|
pass
|
|
else:
|
|
# A brand new game. STARTING_SHIP_COUNT rather than a literal: this
|
|
# said 5, which was the old BASE_FLEET_SIZE written out by hand, and
|
|
# it silently became "however many slots exist" once max_fleet_size()
|
|
# clamped it. The opening fleet size is one decision and it lives in
|
|
# GameState.
|
|
fleet.spawn_ships(GameState.STARTING_SHIP_COUNT, 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.
|
|
# DEFERRED, not called straight through. Everything above runs inside the
|
|
# single frame that builds Main.tscn, and these two populate 25 and 49
|
|
# chunks of world on top of it - 150-odd asteroids plus planets and their
|
|
# visuals. On a phone browser that frame is where the scene appeared to
|
|
# hang forever: nothing can paint until it ends, so a slow frame and a
|
|
# freeze look identical. One frame later is still immediate to the eye,
|
|
# and it lets the scene draw first.
|
|
asteroid_field.update_chunks.call_deferred()
|
|
planet_field.update_chunks.call_deferred()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if transitioning:
|
|
return
|
|
|
|
if world_map.visible or build_controller.is_showing_building_prompt():
|
|
# Context actions belong to BuildController. This controller owns the
|
|
# debounced planet selection and label, and suppresses both while a modal
|
|
# screen is open.
|
|
current_planet = null
|
|
pending_planet = null
|
|
planet_name_label.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
|
|
else:
|
|
planet_name_label.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()
|
|
GameState.surviving_vessel_types = fleet.vessel_types()
|
|
# 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_id = current_planet.planet_id
|
|
GameState.planet_definition_id = current_planet.definition_id
|
|
# #133: quests ask about worlds LANDED ON, which is a different question
|
|
# from the ones flown past that explored_chunks answers. Recorded at the
|
|
# moment of committing to the descent - the same moment the three lines
|
|
# above are written - so the two can never disagree.
|
|
GameState.note_planet_visited(current_planet.planet_id, current_planet.definition_id)
|
|
|
|
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://surface/PlanetSurface.tscn")
|