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):