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

@ -1,4 +1,4 @@
[gd_scene load_steps=46 format=3]
[gd_scene load_steps=48 format=3]
[ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"]
[ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"]
[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"]
@ -9,6 +9,7 @@
[ext_resource type="PackedScene" path="res://ui/menus/GarrisonMenu.tscn" id="menu_garrison"]
[ext_resource type="PackedScene" path="res://ui/menus/SkillMenu.tscn" id="menu_skill"]
[ext_resource type="PackedScene" path="res://ui/menus/DevMenu.tscn" id="menu_dev"]
[ext_resource type="PackedScene" path="res://ui/menus/ScannerMenu.tscn" id="menu_scanner"]
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
@ -45,6 +46,7 @@
[ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"]
[ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"]
[ext_resource type="Script" path="res://ui/QuestPanel.gd" id="quest_panel"]
[ext_resource type="Script" path="res://world/OctopusField.gd" id="octopus_field"]
[sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("5")
@ -104,6 +106,9 @@ script = ExtResource("6")
[node name="AsteroidField" type="Node2D" parent="."]
script = ExtResource("7")
[node name="OctopusField" type="Node2D" parent="."]
script = ExtResource("octopus_field")
[node name="PlanetField" type="Node2D" parent="."]
script = ExtResource("11")
@ -513,6 +518,7 @@ icon = 1
[node name="GarrisonMenu" parent="UI" instance=ExtResource("menu_garrison")]
[node name="SkillMenu" parent="UI" instance=ExtResource("menu_skill")]
[node name="DevMenu" parent="UI" instance=ExtResource("menu_dev")]
[node name="ScannerMenu" parent="UI" instance=ExtResource("menu_scanner")]
[node name="QuestPanel" type="Control" parent="UI" groups=["modal_menus"]]
visible = false
mouse_filter = 2

62
world/OctopusField.gd Normal file
View file

@ -0,0 +1,62 @@
extends Node2D
# A bounded ambient threat attached to the OCTOPUS biome. Like AsteroidField,
# this node owns only the nearby presentation: the biome itself and its one
# boss planet remain deterministic WorldGen data.
@export var spawn_interval: float = 2.4
@export var max_octopuses: int = 7
@export var spawn_margin: float = 180.0
@export var retire_distance: float = 2600.0
@onready var fleet: Node2D = get_node("../Fleet")
var octopus_scene: PackedScene = preload("res://boss/BossMinion.tscn")
var active: Array = []
var spawn_left: float = 0.0
var spawn_count: int = 0
func _process(delta: float) -> void:
active = active.filter(func(o): return is_instance_valid(o))
var in_biome: bool = WorldGen.biome_at_chunk(
WorldGen.chunk_coords(fleet.get_centroid())) == WorldGen.Biome.OCTOPUS
if not in_biome or GameState.intro_active:
retire_all()
spawn_left = 0.0
return
for octopus in active.duplicate():
if octopus.global_position.distance_to(fleet.get_centroid()) > retire_distance:
octopus.retire()
active.erase(octopus)
spawn_left -= delta
if spawn_left <= 0.0 and active.size() < max_octopuses:
spawn_one()
spawn_left = spawn_interval
func spawn_one() -> Node2D:
if fleet == null or fleet.ships.is_empty():
return null
var visible_rect: Rect2 = ViewUtils.get_visible_world_rect(self)
var radius: float = maxf(visible_rect.size.length() * 0.5 + spawn_margin,
620.0)
var angle: float = randf() * TAU
var octopus: Node2D = octopus_scene.instantiate()
# Ambient hatchlings are sturdy enough to harass a travelling fleet, but
# never marked: only the boss encounter's golden hatchlings open eyes.
octopus.max_hp = 7
octopus.position = fleet.get_centroid() + Vector2.from_angle(angle) * radius
get_parent().add_child(octopus)
octopus.uses_dash_pattern = spawn_count % 3 == 2
octopus.add_to_group("roaming_octopuses")
active.append(octopus)
spawn_count += 1
return octopus
func retire_all() -> void:
for octopus in active:
if is_instance_valid(octopus):
octopus.retire()
active.clear()

View file

@ -15,6 +15,8 @@ extends ColorRect
@onready var camera: Camera2D = get_node("../../Camera2D")
var current_chunk: Vector2i = Vector2i(2147483647, 2147483647)
func _process(_delta: float) -> void:
var viewport_size: Vector2 = get_viewport_rect().size
@ -26,3 +28,8 @@ func _process(_delta: float) -> void:
material.set_shader_parameter("camera_frac", p - cell)
material.set_shader_parameter("camera_zoom", 1.0 / camera.zoom.x)
material.set_shader_parameter("viewport_size", viewport_size)
var chunk: Vector2i = WorldGen.chunk_coords(camera.global_position)
if chunk != current_chunk:
current_chunk = chunk
material.set_shader_parameter("bg_color",
WorldGen.biome_background_color(WorldGen.biome_at_chunk(chunk)))

View file

@ -9,7 +9,7 @@ const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
# revisiting the same chunk always yields the same biome, and biomes form
# smooth multi-chunk regions instead of a per-chunk random checkerboard.
enum Biome { SPACE, ASTEROID_FIELD }
enum Biome { SPACE, ASTEROID_FIELD, OCTOPUS }
const CHUNK_SIZE: float = 1200.0
const NOISE_SEED: int = 1337
@ -37,10 +37,18 @@ const BLACK_HOLE_CHANCE: float = 0.05
# from 0.08 after playtesting found that rate too scarce for how central
# research points had become to progression.
const RESEARCH_LAB_CHANCE: float = 0.16
# A boss world is rarer than every ordinary discovery. Its roll uses a
# separately seeded generator below, so adding it cannot rename or re-roll
# any planet in an existing world.
const BOSS_PLANET_CHANCE: float = 0.025
# The hostile region is authored into procedural space as one finite,
# deterministic island. A probability per chunk can only say "rare"; over an
# infinite world it inevitably creates infinitely many boss worlds. One
# anchor and one shape make the actual rule representable: one multi-chunk
# octopus biome, with its boss at the centre.
const OCTOPUS_BIOME_CENTER: Vector2i = Vector2i(12, -9)
const OCTOPUS_BIOME_OFFSETS: Array[Vector2i] = [
Vector2i(0, 0), Vector2i(-1, 0), Vector2i(1, 0),
Vector2i(0, -1), Vector2i(0, 1),
Vector2i(-1, -1), Vector2i(1, -1),
Vector2i(-1, 1), Vector2i(1, 1),
]
# DEV MODE ONLY: one of every planet type, parked in the chunks immediately
# around the spawn so all five can be reached in seconds.
@ -113,6 +121,8 @@ static func chunk_origin(chunk: Vector2i) -> Vector2:
static func biome_at_chunk(chunk: Vector2i) -> int:
if is_octopus_chunk(chunk):
return Biome.OCTOPUS
var n: float = _get_noise().get_noise_2d(float(chunk.x), float(chunk.y))
if n > ASTEROID_FIELD_THRESHOLD:
return Biome.ASTEROID_FIELD
@ -122,6 +132,10 @@ static func biome_at_chunk(chunk: Vector2i) -> int:
static func has_planet_at_chunk(chunk: Vector2i) -> bool:
if _dev_planets_active() and DEV_PLANET_CHUNKS.has(chunk):
return true
if chunk == OCTOPUS_BIOME_CENTER:
return true
if is_octopus_chunk(chunk):
return false
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
return (biome_at_chunk(chunk) == Biome.ASTEROID_FIELD and n > PLANET_THRESHOLD) or (biome_at_chunk(chunk) == Biome.SPACE and n > GAS_GIANT_THRESHOLD)
@ -148,17 +162,12 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
var black_hole_roll: float = rng.randf()
# Same principle, its own independent roll (#63).
var research_lab_roll: float = rng.randf()
# Independent from the established generation stream: existing chunks keep
# their old names and shapes when this new rare outcome is introduced.
var boss_rng := RandomNumberGenerator.new()
boss_rng.seed = hash("boss:%d:%d" % [chunk.x, chunk.y])
var boss_roll: float = boss_rng.randf()
# A stable per-chunk roll makes gas giants persistent landmarks rather
# than a different resource whenever the scene reloads.
var gas_giant: bool = biome_at_chunk(chunk) == Biome.SPACE
var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID
# Rare authored outcomes, seeded deterministically like everything else.
if boss_roll < BOSS_PLANET_CHANCE:
if chunk == OCTOPUS_BIOME_CENTER:
definition_id = PLANET_DEFINITIONS.BOSS_ID
elif black_hole_roll < BLACK_HOLE_CHANCE:
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
@ -186,5 +195,26 @@ static func biome_color(biome: int) -> Color:
match biome:
Biome.ASTEROID_FIELD:
return Color(0.55, 0.36, 0.18, 1.0)
Biome.OCTOPUS:
return Color(0.34, 0.07, 0.24, 1.0)
_:
return Color(0.10, 0.13, 0.28, 1.0)
static func biome_background_color(biome: int) -> Color:
match biome:
Biome.OCTOPUS:
return Color(0.075, 0.012, 0.065, 1.0)
_:
return Color(0.02, 0.02, 0.08, 1.0)
static func is_octopus_chunk(chunk: Vector2i) -> bool:
return chunk - OCTOPUS_BIOME_CENTER in OCTOPUS_BIOME_OFFSETS
static func octopus_biome_chunks() -> Array[Vector2i]:
var chunks: Array[Vector2i] = []
for offset in OCTOPUS_BIOME_OFFSETS:
chunks.append(OCTOPUS_BIOME_CENTER + offset)
return chunks

View file

@ -58,7 +58,11 @@ func close_map() -> void:
func recenter_view() -> void:
var fleet_local: Vector2 = fleet.get_centroid() * content.WORLD_TO_LOCAL
focus_world_position(fleet.get_centroid())
func focus_world_position(world_position: Vector2) -> void:
var fleet_local: Vector2 = world_position * content.WORLD_TO_LOCAL
var viewport_center: Vector2 = get_viewport_rect().size / 2.0
content.position = viewport_center - fleet_local * content.scale.x