#227: resume at the mothership, with the crew the save had
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
This commit is contained in:
parent
d4ece16377
commit
62acb0052c
4 changed files with 172 additions and 0 deletions
|
|
@ -20,6 +20,10 @@ const CATALOG_CHECK = preload("res://core/CatalogCheck.gd")
|
||||||
# survive the trip has to live here instead.
|
# survive the trip has to live here instead.
|
||||||
|
|
||||||
var has_return_position: bool = false
|
var has_return_position: bool = false
|
||||||
|
# Set by the menu when a save has just been loaded, cleared by whoever acts
|
||||||
|
# on it - the same one-shot handoff as has_return_position above, and
|
||||||
|
# deliberately NOT saved: it describes this launch, not the run.
|
||||||
|
var resumed_from_save: bool = false
|
||||||
var surviving_ship_count: int = STARTING_SHIP_COUNT
|
var surviving_ship_count: int = STARTING_SHIP_COUNT
|
||||||
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
||||||
# #106: a mirror of what is flying, pushed by Fleet._sync_composition_state()
|
# #106: a mirror of what is flying, pushed by Fleet._sync_composition_state()
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,23 @@ func _ready() -> void:
|
||||||
camera.position = GameState.return_world_position
|
camera.position = GameState.return_world_position
|
||||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||||
GameState.has_return_position = false
|
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:
|
elif GameState.intro_active:
|
||||||
# #108: the prologue opens with a convoy, and crews it itself. This
|
# #108: the prologue opens with a convoy, and crews it itself. This
|
||||||
# node still runs first, so spawning the ordinary opening fleet here
|
# node still runs first, so spawning the ordinary opening fleet here
|
||||||
|
|
|
||||||
150
tests/test_resume_spawn.gd
Normal file
150
tests/test_resume_spawn.gd
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Forgejo #227: a resumed run comes back at its own mothership.
|
||||||
|
#
|
||||||
|
# Loading a save used to fall through to the brand-new-game branch in
|
||||||
|
# PlanetAtmosphereController, which spawns STARTING_SHIP_COUNT interceptors
|
||||||
|
# at the world origin. So a resume stranded the player in empty space with
|
||||||
|
# the wrong ships, while their base sat wherever they had left it - which on
|
||||||
|
# a charted map is thousands of units away.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. resuming puts the fleet at the mothership, not at the origin
|
||||||
|
# B. and crews it with the ships the save actually had
|
||||||
|
# C. with no mothership standing, it falls back to where the save was made
|
||||||
|
# D. a NEW game is untouched - still the starting fleet at the origin
|
||||||
|
|
||||||
|
const HOME: Vector2 = Vector2(7400.0, -3100.0)
|
||||||
|
const AWAY: Vector2 = Vector2(-5200.0, 900.0)
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var scene: Node
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
var stage: int = 0
|
||||||
|
var expected_crew: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
SaveGame.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _fresh_state() -> void:
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.reset_new_game()
|
||||||
|
game_state.reset_quests()
|
||||||
|
game_state.intro_active = false
|
||||||
|
|
||||||
|
|
||||||
|
func _load_scene() -> void:
|
||||||
|
if scene != null and is_instance_valid(scene):
|
||||||
|
scene.free()
|
||||||
|
scene = load("res://world/Main.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
current_scene = scene
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if elapsed < 0.3:
|
||||||
|
return false
|
||||||
|
# A run with a base at HOME and the fleet parked far from it.
|
||||||
|
_fresh_state()
|
||||||
|
game_state.add_building(GameState.BUILDING_MOTHERSHIP, HOME)
|
||||||
|
# Capacity for the crew below. Since #219 the fleet holds
|
||||||
|
# BASE_FLEET_SIZE (1) plus one berth per supplied fleet office,
|
||||||
|
# so a save with three ships flying is a save that had two
|
||||||
|
# offices standing - without them spawn_ships clamps to 1 and
|
||||||
|
# this would assert nothing.
|
||||||
|
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, HOME + Vector2(400.0, 0.0))
|
||||||
|
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, HOME + Vector2(800.0, 0.0))
|
||||||
|
game_state.fleet_vessel_types = PackedStringArray([
|
||||||
|
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
|
||||||
|
GameState.VESSEL_INTERCEPTOR])
|
||||||
|
game_state.fleet_position = AWAY
|
||||||
|
expected_crew = game_state.fleet_vessel_types.size()
|
||||||
|
if game_state.max_fleet_size() < expected_crew:
|
||||||
|
return _fail("the fleet cannot hold %d, so a resumed crew would prove nothing (cap %d)" \
|
||||||
|
% [expected_crew, game_state.max_fleet_size()])
|
||||||
|
if not SaveGame.save(game_state):
|
||||||
|
return _fail("could not write the save this test resumes from")
|
||||||
|
_fresh_state()
|
||||||
|
var result: Dictionary = SaveGame.load_into(game_state)
|
||||||
|
if not bool(result.get("ok", false)):
|
||||||
|
return _fail("the save would not load: %s" % result.get("reason", ""))
|
||||||
|
game_state.resumed_from_save = true
|
||||||
|
_load_scene()
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
if elapsed < 0.6:
|
||||||
|
return false
|
||||||
|
var fleet: Node2D = scene.get_node("Fleet")
|
||||||
|
# --- A. at the mothership, not the origin -------------------
|
||||||
|
var at: Vector2 = fleet.get_centroid()
|
||||||
|
if at.distance_to(HOME) > 400.0:
|
||||||
|
return _fail("resumed at %s, but the mothership is at %s" % [at, HOME])
|
||||||
|
if at.distance_to(Vector2.ZERO) < 400.0:
|
||||||
|
return _fail("resumed at the world origin, which is the new-game behaviour")
|
||||||
|
print("resumed at the mothership (%.0f units from it)" % at.distance_to(HOME))
|
||||||
|
|
||||||
|
# --- B. with the crew the save had --------------------------
|
||||||
|
if fleet.ships.size() != expected_crew:
|
||||||
|
return _fail("resumed with %d ships, the save had %d" \
|
||||||
|
% [fleet.ships.size(), expected_crew])
|
||||||
|
print("resumed with the saved crew of %d" % fleet.ships.size())
|
||||||
|
if game_state.resumed_from_save:
|
||||||
|
return _fail("the resume flag was left raised, so the next scene would re-use it")
|
||||||
|
|
||||||
|
# --- C. no mothership: fall back to where the save was made --
|
||||||
|
_fresh_state()
|
||||||
|
game_state.fleet_vessel_types = PackedStringArray([GameState.VESSEL_INTERCEPTOR])
|
||||||
|
game_state.fleet_position = AWAY
|
||||||
|
game_state.resumed_from_save = true
|
||||||
|
_load_scene()
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 2
|
||||||
|
return false
|
||||||
|
2:
|
||||||
|
if elapsed < 0.6:
|
||||||
|
return false
|
||||||
|
var fleet2: Node2D = scene.get_node("Fleet")
|
||||||
|
var at2: Vector2 = fleet2.get_centroid()
|
||||||
|
if at2.distance_to(AWAY) > 400.0:
|
||||||
|
return _fail("with no mothership, resumed at %s rather than the saved %s" % [at2, AWAY])
|
||||||
|
print("with no mothership, resumed where the save was written")
|
||||||
|
|
||||||
|
# --- D. a new game is untouched -----------------------------
|
||||||
|
_fresh_state()
|
||||||
|
game_state.resumed_from_save = false
|
||||||
|
_load_scene()
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 3
|
||||||
|
return false
|
||||||
|
3:
|
||||||
|
if elapsed < 0.6:
|
||||||
|
return false
|
||||||
|
var fleet3: Node2D = scene.get_node("Fleet")
|
||||||
|
if fleet3.get_centroid().distance_to(Vector2.ZERO) > 400.0:
|
||||||
|
return _fail("a new game no longer starts at the origin (%s)" % fleet3.get_centroid())
|
||||||
|
if fleet3.ships.size() != GameState.STARTING_SHIP_COUNT:
|
||||||
|
return _fail("a new game starts with %d ships, expected %d" \
|
||||||
|
% [fleet3.ships.size(), GameState.STARTING_SHIP_COUNT])
|
||||||
|
print("a new game still starts with %d ships at the origin" % fleet3.ships.size())
|
||||||
|
|
||||||
|
SaveGame.clear()
|
||||||
|
print("TEST_RESUME_SPAWN: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_RESUME_SPAWN: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
|
@ -140,6 +140,7 @@ func _on_continue_pressed() -> void:
|
||||||
# A resumed run never replays the prologue, whatever it was doing when
|
# A resumed run never replays the prologue, whatever it was doing when
|
||||||
# it was written.
|
# it was written.
|
||||||
GameState.intro_active = false
|
GameState.intro_active = false
|
||||||
|
GameState.resumed_from_save = true
|
||||||
_start("res://world/Main.tscn")
|
_start("res://world/Main.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue