Add planet scanner and octopus biome

This commit is contained in:
Loïc Wisniewski 2026-09-04 15:56:07 +02:00
commit 8fbcc340ce
21 changed files with 784 additions and 30 deletions

View file

@ -203,6 +203,13 @@ var dev_mode: bool = false:
# player lands on and leaves a planet.
var explored_chunks: Dictionary = {}
# The four ordinary destinations teach the scanner enough signatures to
# identify the hostile boss world. Kept explicit so adding a sixth authored
# planet later cannot silently move the progression gate.
const SCANNER_SURVEY_TYPES: PackedStringArray = [PLANET_DEFINITIONS.ROCKY_ID,
PLANET_DEFINITIONS.GAS_GIANT_ID, PLANET_DEFINITIONS.BLACK_HOLE_ID,
PLANET_DEFINITIONS.RESEARCH_LAB_ID]
# --- Resources -------------------------------------------------------------
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
@ -430,6 +437,9 @@ const BUILDING_SHIP_LAB: String = "ship_lab"
# Converts gas into energy, both stored/moved through the building's own
# inventory exactly like every other building (#54).
const BUILDING_GAS_BURNER: String = "gas_burner"
# A map-revealing structure, not a passive sensor bonus. Its menu searches
# the same deterministic WorldGen catalogue that PlanetField spawns from.
const BUILDING_PLANET_SCANNER: String = "planet_scanner"
var buildings: Array = []
@ -532,6 +542,13 @@ const SKILL_TREE: Array = [
"x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1,
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
},
{
"id": "deep_space_scanning", "name": "Deep-Space Scanning", "cost": 50,
"costs": {"ore": 50, "gas": 15},
"requires": "coordination", "x": 0.34,
"unlocks": BUILDING_PLANET_SCANNER, "research_points": 1,
"desc": "Build a planet scanner and reveal the nearest world of a chosen type.",
},
{
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
"costs": {"ore": 80, "gas": 30},
@ -791,6 +808,70 @@ func reset_new_game() -> void:
reset_formation()
func scanner_can_find_boss() -> bool:
for definition_id in SCANNER_SURVEY_TYPES:
if not visited_planet_kinds.has(definition_id):
return false
return true
func scanner_available_planet_types() -> PackedStringArray:
var available: PackedStringArray = SCANNER_SURVEY_TYPES.duplicate()
if scanner_can_find_boss():
available.append(PLANET_DEFINITIONS.BOSS_ID)
return available
# Finds the nearest generated planet of this type, reveals its neighbourhood
# on the persistent map, and returns the same identity PlanetField will spawn.
# The search is ring-shaped, so the first hit is nearest in chunk distance and
# results do not depend on dictionary iteration order.
func scan_for_planet(definition_id: String, origin_world_position: Vector2) -> Dictionary:
if not structure_exists(BUILDING_PLANET_SCANNER) \
or definition_id not in scanner_available_planet_types():
return {}
var origin: Vector2i = WORLD_GEN.chunk_coords(origin_world_position)
for radius in range(0, 257):
if radius == 0:
var center_match: Dictionary = _scanner_match_at(origin,
definition_id, radius)
if not center_match.is_empty():
return center_match
continue
# Only the perimeter is new at each radius. The old square-with-a-
# condition loop revisited every inner chunk for every radius, turning
# a wide scan cubic precisely when the phone had the most work to do.
for x in range(-radius, radius + 1):
for y in [-radius, radius]:
var horizontal: Dictionary = _scanner_match_at(
origin + Vector2i(x, y), definition_id, radius)
if not horizontal.is_empty():
return horizontal
for y in range(-radius + 1, radius):
for x in [-radius, radius]:
var vertical: Dictionary = _scanner_match_at(
origin + Vector2i(x, y), definition_id, radius)
if not vertical.is_empty():
return vertical
return {}
func _scanner_match_at(chunk: Vector2i, definition_id: String,
distance_chunks: int) -> Dictionary:
if not WORLD_GEN.has_planet_at_chunk(chunk):
return {}
var data: Dictionary = WORLD_GEN.planet_data_at_chunk(chunk)
if String(data.get("definition_id", "")) != definition_id:
return {}
for reveal_y in range(-1, 2):
for reveal_x in range(-1, 2):
explored_chunks[chunk + Vector2i(reveal_x, reveal_y)] = true
data["chunk"] = chunk
data["world_position"] = WORLD_GEN.chunk_origin(chunk) + Vector2(data.local_pos)
data["distance_chunks"] = distance_chunks
return data
# Amounts a dev game opens with: enough of everything to reach any part of
# the game immediately, without being so large that costs stop registering.
const DEV_RESOURCE_AMOUNT: int = 2000

View file

@ -233,13 +233,21 @@ const CHAIN: Array = [
"brief": "An empty outpost is only a building. Open its hangar and station a ship there. It will hold that ground whether we are there or not.",
"done": "The outpost has a crew. It watches that approach now, so we do not have to.",
},
{
"id": "planet_scanner",
"title": "LISTENING POST",
"objective": "Build a planet scanner",
"goal": {"kind": GOAL_BUILDING, "building": "planet_scanner"},
"brief": "Blind searching is wasting our reach. Research Deep-Space Scanning at the mothership, then build a planet scanner. Give it a world signature and it will put the nearest match on our map.",
"done": "The scanner is listening. Now we can choose where the search goes next.",
},
{
"id": "worlds",
"title": "THE SURVEY",
"objective": "Land on three different kinds of world",
"goal": {"kind": GOAL_PLANET_KINDS, "count": 3},
"brief": "Rock, gas, and whatever else is out here. Land on three different kinds of world. If the World Ship came this way, it passed something.",
"done": "Three kinds of world surveyed. Not one of them holds any sign of the convoy.",
"objective": "Land on four different kinds of world",
"goal": {"kind": GOAL_PLANET_KINDS, "count": 4},
"brief": "Rock, gas, the abandoned labs, and the lightless wells. Use the scanner and land on all four kinds of world. If the World Ship came this way, it passed something.",
"done": "Four kinds of world surveyed. No convoy—but their signatures resolve a fifth signal, hidden inside something alive.",
"important": true,
},
{