diff --git a/BuildController.gd b/BuildController.gd index 1d33801..ae3c914 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -563,7 +563,7 @@ func _show_building_info(type: String) -> void: if planet == null: lines.append("No planet in reach - fly over one.") can_build = false - elif GameState.has_extractor_at(planet.planet_name): + elif GameState.has_extractor_at(planet.planet_id): lines.append("%s already has an extractor." % planet.planet_name) can_build = false else: @@ -606,13 +606,13 @@ func _confirm_build() -> void: if definition.placement_mode == "planet": var planet: Node2D = _planet_under_fleet() - if planet == null or GameState.has_extractor_at(planet.planet_name): + if planet == null or GameState.has_extractor_at(planet.planet_id): build_status.text = "No planet in reach any more" return if not GameState.spend(definition.cost): build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks] return - GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type) + GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type, planet.planet_id) build_menu.visible = false return diff --git a/GameState.gd b/GameState.gd index 37d110a..b3d700b 100644 --- a/GameState.gd +++ b/GameState.gd @@ -18,6 +18,7 @@ var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 var planet_name: String = "" +var planet_id: String = "" var planet_definition_id: String = "rocky" # Explored-chunk history for the world map's fog-of-war reveal. Lives @@ -881,15 +882,16 @@ func rotate_formation(radians: float) -> void: # Extractors mine into their OWN local stock. They do not own a magical # hauler: ore becomes spendable only when a hub-stationed transporter picks # up a full load and physically delivers it to the mothership. Entries are -# {"position", "planet_name", "resource_type", "inventory", "mining_progress"}. +# {"position", "planet_id", "planet_name", "resource_type", "inventory", "mining_progress"}. const EXTRACTOR_RATE: float = 2.0 # ore per second var extractors: Array = [] -func has_extractor_at(planet_name: String) -> bool: +func has_extractor_at(planet_key: String) -> bool: for e in extractors: - if e.planet_name == planet_name: + # Old saves have no planet_id; their name remains a migration fallback. + if String(e.get("planet_id", e.get("planet_name", ""))) == planet_key: return true return false @@ -897,9 +899,12 @@ func has_extractor_at(planet_name: String) -> bool: signal extractor_added(planet_name: String) -func add_extractor(planet_name: String, at_position: Vector2, resource_type: String = "ore") -> void: +func add_extractor(planet_name: String, at_position: Vector2, resource_type: String = "ore", + planet_key: String = "") -> void: + var stable_id: String = planet_key if planet_key != "" else planet_name extractors.append({ "position": at_position, + "planet_id": stable_id, "planet_name": planet_name, "resource_type": resource_type, "inventory": INVENTORY.new(), @@ -909,7 +914,7 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str # so the Planet node is already in the tree with its _ready() long since # run. Without this it kept showing no derrick until it happened to # despawn and respawn, which reads as the build having done nothing. - extractor_added.emit(planet_name) + extractor_added.emit(stable_id) func extractor_stock(index: int) -> int: diff --git a/GasVentField.gd b/GasVentField.gd index 831ca4c..8ad7e51 100644 --- a/GasVentField.gd +++ b/GasVentField.gd @@ -6,7 +6,7 @@ var progress: float = 0.0 var emission_phase: float = 0.0 func _ready() -> void: var rng := RandomNumberGenerator.new() - rng.seed = hash(GameState.planet_name) + rng.seed = hash(GameState.planet_id if GameState.planet_id != "" else GameState.planet_name) for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650))) queue_redraw() func _process(delta: float) -> void: diff --git a/Planet.gd b/Planet.gd index 4d17d84..954af57 100644 --- a/Planet.gd +++ b/Planet.gd @@ -14,6 +14,7 @@ const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # while the fleet as a whole was clearly sitting on the planet. @export var planet_name: String = "" +@export var planet_id: String = "" @export var definition_id: String = PLANET_DEFINITIONS.DEFAULT_DEFINITION_ID @export var landing_radius: float = 140.0 # Set by whoever spawns this planet (PlanetField, seeded from the chunk's @@ -67,7 +68,9 @@ func _ready() -> void: visual = definition.visual_scene.instantiate() visual.configure(shape_seed) get_node("VisualHost").add_child(visual) - shows_extractor = GameState.has_extractor_at(planet_name) + if planet_id == "": + planet_id = planet_name + shows_extractor = GameState.has_extractor_at(planet_id) GameState.extractor_added.connect(_on_extractor_added) queue_redraw() @@ -75,7 +78,7 @@ func _ready() -> void: func _on_extractor_added(built_on: String) -> void: # Only the planet it was actually built on, and only a redraw when the # answer really changed - every other planet in range gets this too. - if built_on != planet_name or shows_extractor: + if built_on != planet_id or shows_extractor: return shows_extractor = true queue_redraw() diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index 3a66ea4..9363981 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -113,6 +113,7 @@ func _on_enter_pressed() -> void: 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 var tween: Tween = create_tween() diff --git a/PlanetField.gd b/PlanetField.gd index 6a8c3c6..21ea1fc 100644 --- a/PlanetField.gd +++ b/PlanetField.gd @@ -83,6 +83,7 @@ func _spawn_chunk_planet(chunk: Vector2i) -> void: var planet: Node2D = planet_scene.instantiate() planet.position = origin + data.local_pos planet.planet_name = data.name + planet.planet_id = data.planet_id planet.shape_seed = data.shape_seed planet.definition_id = String(data.get("definition_id", "rocky")) # Deferred: update_chunks() can run synchronously from inside a diff --git a/WorldGen.gd b/WorldGen.gd index 5e73ee5..7cc6c6c 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -91,7 +91,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names var planet_name: String = names[rng.randi() % names.size()] - return {"local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, + var planet_id: String = "chunk:%d:%d" % [chunk.x, chunk.y] + return {"planet_id": planet_id, "local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, "definition_id": definition_id} diff --git a/WorldMapContent.gd b/WorldMapContent.gd index 5d4667b..9577163 100644 --- a/WorldMapContent.gd +++ b/WorldMapContent.gd @@ -50,7 +50,7 @@ func building_markers() -> Array: func chunk_has_extractor_badge(chunk: Vector2i) -> bool: if not WorldGen.has_planet_at_chunk(chunk): return false - return GameState.has_extractor_at(WorldGen.planet_data_at_chunk(chunk).name) + return GameState.has_extractor_at(WorldGen.planet_data_at_chunk(chunk).planet_id) func _regular_polygon(center: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array: diff --git a/tests/test_extractor_building.gd b/tests/test_extractor_building.gd index 0a4b769..0e52c21 100644 --- a/tests/test_extractor_building.gd +++ b/tests/test_extractor_building.gd @@ -135,7 +135,7 @@ func _process(delta: float) -> bool: return true controller._confirm_build() - var built: bool = game_state.has_extractor_at(planet2.planet_name) + var built: bool = game_state.has_extractor_at(planet2.planet_id) var charged: bool = game_state.rocks == rocks_before - GameState.EXTRACTOR_COST print("over planet '%s': enabled=%s built=%s rocks %d->%d charged=%s" % [ planet2.planet_name, button.enabled, built, rocks_before, game_state.rocks, charged diff --git a/tests/test_planet_extractor_marker.gd b/tests/test_planet_extractor_marker.gd index ed4b4cb..ef11c84 100644 --- a/tests/test_planet_extractor_marker.gd +++ b/tests/test_planet_extractor_marker.gd @@ -28,6 +28,7 @@ var scene: Node var map_content: Node2D var planet_chunk: Vector2i var planet_name: String = "" +var planet_id: String = "" var stage: int = 0 var already_spawned: Node2D @@ -43,7 +44,9 @@ func _initialize() -> void: var chunk: Vector2i = Vector2i(x, y) if WorldGen.has_planet_at_chunk(chunk): planet_chunk = chunk - planet_name = WorldGen.planet_data_at_chunk(chunk).name + var data: Dictionary = WorldGen.planet_data_at_chunk(chunk) + planet_name = data.name + planet_id = data.planet_id found = true break if found: @@ -61,6 +64,7 @@ func _initialize() -> void: func _spawn_planet() -> Node2D: var planet: Node2D = load("res://Planet.tscn").instantiate() planet.planet_name = planet_name + planet.planet_id = planet_id planet.shape_seed = 12345 scene.add_child(planet) return planet @@ -90,7 +94,7 @@ func _process(_delta: float) -> bool: # having done nothing at all. Spawning a fresh planet afterwards # (as the checks below do) cannot catch that. already_spawned = clean - game_state.add_extractor(planet_name, WorldGen.chunk_origin(planet_chunk)) + game_state.add_extractor(planet_name, WorldGen.chunk_origin(planet_chunk), "ore", planet_id) stage = 1 return false 1: @@ -141,6 +145,7 @@ func _process(_delta: float) -> bool: # by name, and getting that wrong would light up every planet. var other: Node2D = load("res://Planet.tscn").instantiate() other.planet_name = planet_name + " II" + other.planet_id = planet_id + ":other" other.shape_seed = 999 scene.add_child(other) if other.shows_extractor: diff --git a/tests/test_planet_unique_identity.gd b/tests/test_planet_unique_identity.gd new file mode 100644 index 0000000..d03ff09 --- /dev/null +++ b/tests/test_planet_unique_identity.gd @@ -0,0 +1,39 @@ +extends SceneTree + + +func _initialize() -> void: + var state: Node = root.get_node("GameState") + state.extractors.clear() + var first_by_name: Dictionary = {} + var first: Dictionary = {} + var second: Dictionary = {} + for x in range(-60, 61): + for y in range(-60, 61): + var chunk := Vector2i(x, y) + if not WorldGen.has_planet_at_chunk(chunk): + continue + var data: Dictionary = WorldGen.planet_data_at_chunk(chunk) + if first_by_name.has(data.name): + first = first_by_name[data.name] + second = data + break + first_by_name[data.name] = data + if not second.is_empty(): + break + if second.is_empty(): + return _fail("could not find two generated planets sharing a display name") + if first.planet_id == second.planet_id: + return _fail("duplicate display names also shared a stable id") + state.add_extractor(first.name, Vector2.ZERO, "ore", first.planet_id) + if not state.has_extractor_at(first.planet_id): + return _fail("extractor was not attached to its planet id") + if state.has_extractor_at(second.planet_id): + return _fail("'%s' inherited an extractor from its namesake" % second.name) + print("same name '%s', distinct ids %s and %s" % [first.name, first.planet_id, second.planet_id]) + print("TEST_PLANET_UNIQUE_IDENTITY: PASS") + quit(0) + + +func _fail(message: String) -> void: + print("TEST_PLANET_UNIQUE_IDENTITY: FAIL (%s)" % message) + quit(1)