From 2f68515f331bdec173a28b43ae67f0c29bf4ebda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 14 Aug 2026 16:05:32 +0200 Subject: [PATCH] Compose planet surfaces from profiles --- GameState.gd | 2 ++ GasSurfaceFeatures.tscn | 7 +++++++ GasSurfaceProfile.tres | 14 +++++++++++++ GasVentField.gd | 3 ++- Planet.gd | 2 ++ PlanetAtmosphereController.gd | 1 + PlanetField.gd | 2 ++ PlanetSurface.tscn | 9 +-------- PlanetSurfaceController.gd | 38 ++++++++++++++++++----------------- PlanetSurfaceProfile.gd | 13 ++++++++++++ PlanetSurfaceProfile.gd.uid | 1 + PlanetSurfaceProfiles.gd | 12 +++++++++++ PlanetSurfaceProfiles.gd.uid | 1 + RockField.gd | 9 ++------- RockySurfaceFeatures.tscn | 7 +++++++ RockySurfaceProfile.tres | 14 +++++++++++++ WorldGen.gd | 3 ++- tests/_gas_surface_preview.gd | 1 + tests/test_gas_system.gd | 23 +++++++++++++++++---- 19 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 GasSurfaceFeatures.tscn create mode 100644 GasSurfaceProfile.tres create mode 100644 PlanetSurfaceProfile.gd create mode 100644 PlanetSurfaceProfile.gd.uid create mode 100644 PlanetSurfaceProfiles.gd create mode 100644 PlanetSurfaceProfiles.gd.uid create mode 100644 RockySurfaceFeatures.tscn create mode 100644 RockySurfaceProfile.tres diff --git a/GameState.gd b/GameState.gd index f855f2a..6b19551 100644 --- a/GameState.gd +++ b/GameState.gd @@ -1,5 +1,6 @@ extends Node + # Autoload singleton - the one thing that survives change_scene_to_file() # scene swaps between space (Main.tscn) and a planet's surface # (PlanetSurface.tscn). Everything else in either scene gets torn down @@ -13,6 +14,7 @@ var return_camera_zoom: float = 1.0 var planet_name: String = "" var planet_type: String = "" +var surface_profile_id: String = "rocky" # Explored-chunk history for the world map's fog-of-war reveal. Lives # here (not on ExplorationTracker, which gets destroyed and recreated on diff --git a/GasSurfaceFeatures.tscn b/GasSurfaceFeatures.tscn new file mode 100644 index 0000000..576b66d --- /dev/null +++ b/GasSurfaceFeatures.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://GasVentField.gd" id="1"] + +[node name="ResourceProvider" type="Node2D"] +script = ExtResource("1") +fleet_path = NodePath("../../Fleet") diff --git a/GasSurfaceProfile.tres b/GasSurfaceProfile.tres new file mode 100644 index 0000000..0267c48 --- /dev/null +++ b/GasSurfaceProfile.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] +[ext_resource type="PackedScene" path="res://GasSurfaceFeatures.tscn" id="2"] + +[resource] +script = ExtResource("1") +profile_id = "gas" +features_scene = ExtResource("2") +base_color = Color(0.13, 0.025, 0.22, 1) +crater_color = Color(0.055, 0.008, 0.11, 1) +highlight_color = Color(0.52, 0.17, 0.7, 1) +caption_color = Color(0.78, 0.88, 0.94, 1) +caption_outline_color = Color(0.02, 0.01, 0.05, 0.9) diff --git a/GasVentField.gd b/GasVentField.gd index 9cdd338..831ca4c 100644 --- a/GasVentField.gd +++ b/GasVentField.gd @@ -1,5 +1,6 @@ extends Node2D -@onready var fleet: Node2D = get_node("../Fleet") +@export var fleet_path: NodePath = NodePath("../Fleet") +@onready var fleet: Node2D = get_node(fleet_path) var vents: Array[Vector2] = [] var progress: float = 0.0 var emission_phase: float = 0.0 diff --git a/Planet.gd b/Planet.gd index c4eddbd..71b1e9f 100644 --- a/Planet.gd +++ b/Planet.gd @@ -1,5 +1,6 @@ extends Node2D + # A landable world-object - unlike regular Asteroid.gd, planets are # stationary ("Big Stable Asteroid" - stable means it doesn't drift or # destroy ships on touch). Whether the fleet counts as "landed" is decided @@ -13,6 +14,7 @@ extends Node2D @export var planet_name: String = "" @export var planet_type: String = "rocky" @export var resource_type: String = "ore" +@export var surface_profile_id: String = "rocky" @export var landing_radius: float = 140.0 # Set by whoever spawns this planet (PlanetField, seeded from the chunk's # own coordinates) so the outline shape is stable across respawns of the diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index cc497d4..b13e69f 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -114,6 +114,7 @@ func _on_enter_pressed() -> void: GameState.return_camera_zoom = camera.zoom.x GameState.planet_name = current_planet.planet_name GameState.planet_type = current_planet.planet_type + GameState.surface_profile_id = current_planet.surface_profile_id var tween: Tween = create_tween() tween.tween_property(fade_rect, "color:a", 1.0, fade_duration) diff --git a/PlanetField.gd b/PlanetField.gd index 4dbbdd8..53b6ddf 100644 --- a/PlanetField.gd +++ b/PlanetField.gd @@ -1,5 +1,6 @@ extends Node2D + # Spawns "Big Stable Asteroid" planets in ASTEROID_FIELD biome chunks near # the fleet where WorldGen says one belongs, and despawns them once the # fleet has moved far enough away - mirrors AsteroidField's chunk-lifecycle @@ -85,6 +86,7 @@ func _spawn_chunk_planet(chunk: Vector2i) -> void: planet.shape_seed = data.shape_seed planet.planet_type = String(data.get("planet_type", "rocky")) planet.resource_type = String(data.get("resource_type", "ore")) + planet.surface_profile_id = String(data.get("surface_profile_id", "rocky")) # Deferred: update_chunks() can run synchronously from inside a # parent's own _ready() chain (forced once right after the fleet is # positioned, to avoid a pop-in on scene load) - Godot refuses a diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index 507ec2b..81cea2d 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -7,9 +7,7 @@ [ext_resource type="Script" path="res://PlanetSurfaceController.gd" id="5"] [ext_resource type="Script" path="res://IconButton.gd" id="6"] [ext_resource type="Script" path="res://TouchJoystick.gd" id="7"] -[ext_resource type="Script" path="res://RockField.gd" id="8"] [ext_resource type="Script" path="res://ResourceCounter.gd" id="9"] -[ext_resource type="Script" path="res://GasVentField.gd" id="10"] [ext_resource type="Script" path="res://GasCounter.gd" id="11"] [ext_resource type="Script" path="res://CargoCounter.gd" id="12"] [ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"] @@ -49,12 +47,7 @@ ship_core_color = Color(0.02, 0.09, 0.15, 1) position = Vector2(0, 0) zoom = Vector2(1, 1) -[node name="RockField" type="Node2D" parent="."] -script = ExtResource("8") - -[node name="GasVentField" type="Node2D" parent="."] -visible = false -script = ExtResource("10") +[node name="SurfaceFeatures" type="Node2D" parent="."] [node name="SurfaceController" type="Node2D" parent="."] script = ExtResource("5") diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 502d8d4..9f632b4 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -1,5 +1,7 @@ extends Node2D +const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd") + # Bootstraps the surviving fleet on this scene's own startup (from # GameState, since this is a fresh scene tree - nothing here carries # over automatically) and handles the always-available exit prompt that @@ -13,29 +15,14 @@ extends Node2D @onready var planet_name_label: Label = get_node("../UI/PlanetNameLabel") @onready var exit_button: IconButton = get_node("../UI/ExitButton") @onready var fade_rect: ColorRect = get_node("../FadeOverlay/FadeRect") +@onready var background: ColorRect = get_node("../BackgroundLayer/LunarBackground") +@onready var feature_host: Node2D = get_node("../SurfaceFeatures") var transitioning: bool = false func _ready() -> void: - if GameState.planet_type == "gas_giant": - # A gas giant is not lunar terrain: its surface view uses a deep, - # coloured atmosphere instead of the rocky field. The background is - # shader-driven, so changing ColorRect.color does not tint it; update - # the actual palette uniforms. - get_node("../RockField").visible = false - get_node("../GasVentField").visible = true - var background: ColorRect = get_node("../BackgroundLayer/LunarBackground") - var atmosphere: ShaderMaterial = background.material - atmosphere.set_shader_parameter("base_color", Color(0.13, 0.025, 0.22, 1.0)) - atmosphere.set_shader_parameter("crater_color", Color(0.055, 0.008, 0.11, 1.0)) - atmosphere.set_shader_parameter("highlight_color", Color(0.52, 0.17, 0.7, 1.0)) - # The lunar surface uses dark captions on a bright ground. Flip only - # those captions for the night-dark gas atmosphere. - for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]: - var caption: Label = get_node(caption_path) - caption.add_theme_color_override("font_color", Color(0.78, 0.88, 0.94)) - caption.add_theme_color_override("font_outline_color", Color(0.02, 0.01, 0.05, 0.9)) + _compose_surface(SURFACE_PROFILES.get_profile(GameState.surface_profile_id)) fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO) camera.position = Vector2.ZERO camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) @@ -47,6 +34,21 @@ func _ready() -> void: tween.tween_property(fade_rect, "color:a", 0.0, fade_duration) +func _compose_surface(profile: Resource) -> void: + var atmosphere: ShaderMaterial = background.material + atmosphere.set_shader_parameter("base_color", profile.base_color) + atmosphere.set_shader_parameter("crater_color", profile.crater_color) + atmosphere.set_shader_parameter("highlight_color", profile.highlight_color) + for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]: + var caption: Label = get_node(caption_path) + caption.add_theme_color_override("font_color", profile.caption_color) + caption.add_theme_color_override("font_outline_color", profile.caption_outline_color) + if profile.features_scene == null: + return + var features: Node = profile.features_scene.instantiate() + feature_host.add_child(features) + + func _on_exit_pressed() -> void: if transitioning: return diff --git a/PlanetSurfaceProfile.gd b/PlanetSurfaceProfile.gd new file mode 100644 index 0000000..d2ea9be --- /dev/null +++ b/PlanetSurfaceProfile.gd @@ -0,0 +1,13 @@ +class_name PlanetSurfaceProfile +extends Resource + +# Declarative recipe for a landed planet surface. The host scene only knows +# how to apply one of these; the feature scene owns the actual resource nodes. + +@export var profile_id: String = "rocky" +@export var features_scene: PackedScene +@export var base_color: Color = Color(0.75, 0.75, 0.76, 1.0) +@export var crater_color: Color = Color(0.44, 0.44, 0.45, 1.0) +@export var highlight_color: Color = Color(0.89, 0.89, 0.9, 1.0) +@export var caption_color: Color = Color(0.08, 0.16, 0.2, 1.0) +@export var caption_outline_color: Color = Color(0.9, 0.95, 0.96, 0.8) diff --git a/PlanetSurfaceProfile.gd.uid b/PlanetSurfaceProfile.gd.uid new file mode 100644 index 0000000..884fe16 --- /dev/null +++ b/PlanetSurfaceProfile.gd.uid @@ -0,0 +1 @@ +uid://b31vef6ge1tjh diff --git a/PlanetSurfaceProfiles.gd b/PlanetSurfaceProfiles.gd new file mode 100644 index 0000000..97da418 --- /dev/null +++ b/PlanetSurfaceProfiles.gd @@ -0,0 +1,12 @@ +class_name PlanetSurfaceProfiles +extends RefCounted + +const DEFAULT_PROFILE_ID: String = "rocky" +const PROFILES: Dictionary = { + "rocky": preload("res://RockySurfaceProfile.tres"), + "gas": preload("res://GasSurfaceProfile.tres"), +} + + +static func get_profile(profile_id: String) -> Resource: + return PROFILES.get(profile_id, PROFILES[DEFAULT_PROFILE_ID]) diff --git a/PlanetSurfaceProfiles.gd.uid b/PlanetSurfaceProfiles.gd.uid new file mode 100644 index 0000000..c0f3f93 --- /dev/null +++ b/PlanetSurfaceProfiles.gd.uid @@ -0,0 +1 @@ +uid://bufr6mnwbobxb diff --git a/RockField.gd b/RockField.gd index a82389c..dd839ed 100644 --- a/RockField.gd +++ b/RockField.gd @@ -17,7 +17,8 @@ extends Node2D @export var despawn_radius: float = 2600.0 @export var check_interval: float = 0.5 -@onready var fleet: Node2D = get_node("../Fleet") +@export var fleet_path: NodePath = NodePath("../Fleet") +@onready var fleet: Node2D = get_node(fleet_path) var rock_scene: PackedScene = preload("res://Rock.tscn") var rocks: Array = [] @@ -26,12 +27,6 @@ var rng: RandomNumberGenerator = RandomNumberGenerator.new() func _ready() -> void: - # Gas giants have their own gas vents and must never receive ore pickups. - # This check happens before the initial population pass, unlike merely - # hiding the field later from PlanetSurfaceController. - if GameState.planet_type == "gas_giant": - set_process(false) - return rng.randomize() # Populate immediately rather than waiting out the first interval, so # the surface isn't visibly empty for half a second on arrival. diff --git a/RockySurfaceFeatures.tscn b/RockySurfaceFeatures.tscn new file mode 100644 index 0000000..f31b5c8 --- /dev/null +++ b/RockySurfaceFeatures.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://RockField.gd" id="1"] + +[node name="ResourceProvider" type="Node2D"] +script = ExtResource("1") +fleet_path = NodePath("../../Fleet") diff --git a/RockySurfaceProfile.tres b/RockySurfaceProfile.tres new file mode 100644 index 0000000..ab220e0 --- /dev/null +++ b/RockySurfaceProfile.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] +[ext_resource type="PackedScene" path="res://RockySurfaceFeatures.tscn" id="2"] + +[resource] +script = ExtResource("1") +profile_id = "rocky" +features_scene = ExtResource("2") +base_color = Color(0.75, 0.75, 0.76, 1) +crater_color = Color(0.44, 0.44, 0.45, 1) +highlight_color = Color(0.89, 0.89, 0.9, 1) +caption_color = Color(0.08, 0.16, 0.2, 1) +caption_outline_color = Color(0.9, 0.95, 0.96, 0.8) diff --git a/WorldGen.gd b/WorldGen.gd index e36a51f..feea7f6 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -96,7 +96,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: if gas_giant: planet_name = GAS_GIANT_NAMES[rng.randi() % GAS_GIANT_NAMES.size()] return {"local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, - "planet_type": "gas_giant" if gas_giant else "rocky", "resource_type": "gas" if gas_giant else "ore"} + "planet_type": "gas_giant" if gas_giant else "rocky", "resource_type": "gas" if gas_giant else "ore", + "surface_profile_id": "gas" if gas_giant else "rocky"} static func biome_color(biome: int) -> Color: diff --git a/tests/_gas_surface_preview.gd b/tests/_gas_surface_preview.gd index ba68e1c..41cde0a 100644 --- a/tests/_gas_surface_preview.gd +++ b/tests/_gas_surface_preview.gd @@ -4,6 +4,7 @@ extends Node2D func _ready() -> void: GameState.planet_name = "Violet Tempest" GameState.planet_type = "gas_giant" + GameState.surface_profile_id = "gas" GameState.surviving_ship_count = 3 GameState.rocks = 128 GameState.gas = 46 diff --git a/tests/test_gas_system.gd b/tests/test_gas_system.gd index 898e43d..f0797d1 100644 --- a/tests/test_gas_system.gd +++ b/tests/test_gas_system.gd @@ -37,19 +37,34 @@ func _process(delta: float) -> bool: # emission advances continuously so the source visibly looks active. game_state.planet_name = "Gas Test" game_state.planet_type = "gas_giant" + game_state.surface_profile_id = "gas" var surface: Node = load("res://PlanetSurface.tscn").instantiate() root.add_child(surface) - var rock_field: Node = surface.get_node("RockField") - var gas_field: Node = surface.get_node("GasVentField") - if not rock_field.rocks.is_empty() or rock_field.is_processing(): + var gas_field: Node = surface.get_node("SurfaceFeatures/ResourceProvider") + if gas_field.get_script() != load("res://GasVentField.gd"): surface.free() - return _fail("gas surface spawned or continued processing ore rocks") + return _fail("gas surface did not compose the gas feature scene") + if surface.get_node_or_null("RockField") != null: + surface.free() + return _fail("gas surface tree contains an ore producer") gas_field._process(0.5) if gas_field.emission_phase <= 0.0: surface.free() return _fail("gas-source emission animation did not advance") surface.free() + game_state.surface_profile_id = "rocky" + var rocky_surface: Node = load("res://PlanetSurface.tscn").instantiate() + root.add_child(rocky_surface) + var rocky_features: Node = rocky_surface.get_node("SurfaceFeatures/ResourceProvider") + if rocky_features.get_script() != load("res://RockField.gd"): + rocky_surface.free() + return _fail("rocky surface did not compose the ore feature scene") + if rocky_surface.get_node_or_null("GasVentField") != null: + rocky_surface.free() + return _fail("rocky surface tree contains a gas producer") + rocky_surface.free() + var found_giant: bool = false for x in range(-12, 13): for y in range(-12, 13):