diff --git a/GameState.gd b/GameState.gd index 6b19551..11a4b7a 100644 --- a/GameState.gd +++ b/GameState.gd @@ -13,8 +13,7 @@ var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 var planet_name: String = "" -var planet_type: String = "" -var surface_profile_id: String = "rocky" +var planet_definition_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/GasGiantDefinition.tres b/GasGiantDefinition.tres new file mode 100644 index 0000000..0a5109c --- /dev/null +++ b/GasGiantDefinition.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://GasGiantVisual.tscn" id="2"] + +[resource] +script = ExtResource("1") +definition_id = "gas_giant" +display_name = "Gas giant" +names = PackedStringArray("Aurelia", "Boreas", "Cerulean Deep", "Helios Major", "Vesper") +visual_scene = ExtResource("2") +surface_profile_id = "gas" +resource_id = "gas" +map_color = Color(0.65, 0.42, 0.9, 1) diff --git a/GasGiantVisual.gd b/GasGiantVisual.gd new file mode 100644 index 0000000..e9172ca --- /dev/null +++ b/GasGiantVisual.gd @@ -0,0 +1,31 @@ +extends Node2D + +var gas_phase: float = 0.0 + + +func configure(_shape_seed: int) -> void: + queue_redraw() + + +func extractor_anchor() -> Vector2: + return Vector2(0.0, -118.0) + + +func _process(delta: float) -> void: + gas_phase += delta + queue_redraw() + + +func _draw() -> void: + var rim := PackedVector2Array() + for i in range(32): + var angle := TAU * float(i) / 32.0 + rim.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0)) + NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9) + for row in [-62.0, -28.0, 8.0, 43.0, 73.0]: + var band := PackedVector2Array() + var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row)) + for i in range(15): + var x := lerpf(-half_width, half_width, float(i) / 14.0) + band.append(Vector2(x, row + sin(x * 0.045 + gas_phase * 1.6 + row) * 7.0)) + NeonDraw.draw_glowing_path(self, band, Color(0.85, 0.52, 1.0), 0.75, 0.65) diff --git a/GasGiantVisual.gd.uid b/GasGiantVisual.gd.uid new file mode 100644 index 0000000..af718cf --- /dev/null +++ b/GasGiantVisual.gd.uid @@ -0,0 +1 @@ +uid://bxfw4ja2hw4pf diff --git a/GasGiantVisual.tscn b/GasGiantVisual.tscn new file mode 100644 index 0000000..513b87e --- /dev/null +++ b/GasGiantVisual.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://GasGiantVisual.gd" id="1"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") diff --git a/Planet.gd b/Planet.gd index 71b1e9f..4d17d84 100644 --- a/Planet.gd +++ b/Planet.gd @@ -1,5 +1,7 @@ extends Node2D +const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") + # A landable world-object - unlike regular Asteroid.gd, planets are # stationary ("Big Stable Asteroid" - stable means it doesn't drift or @@ -12,16 +14,33 @@ extends Node2D # while the fleet as a whole was clearly sitting on the planet. @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 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 # own coordinates) so the outline shape is stable across respawns of the # same chunk, instead of reading as a "different" planet every time. @export var shape_seed: int = 0 -var shape_points: PackedVector2Array = PackedVector2Array() +var definition: Resource +var visual: Node2D + +var resource_type: String: + get: return String(definition.resource_id) if definition != null else "ore" + +var surface_profile_id: String: + get: return String(definition.surface_profile_id) if definition != null else "rocky" + +var shape_points: PackedVector2Array: + get: + if visual != null and "shape_points" in visual: + return visual.shape_points + return PackedVector2Array() + +var gas_phase: float: + get: + if visual != null and "gas_phase" in visual: + return float(visual.gas_phase) + return 0.0 # An extractor is built down on this planet's SURFACE, in a different scene # the player is no longer looking at, so from space nothing distinguished a @@ -39,31 +58,20 @@ const DERRICK_HALF_WIDTH: float = 16.0 # only at _ready() left the planet looking untouched until it happened to # despawn and respawn. var shows_extractor: bool = false -var gas_phase: float = 0.0 func _ready() -> void: add_to_group("planets") z_index = -1 - - var rng: RandomNumberGenerator = RandomNumberGenerator.new() - rng.seed = shape_seed - var point_count: int = 10 - for i in range(point_count): - var angle: float = (float(i) / point_count) * TAU - var radius: float = rng.randf_range(70.0, 100.0) - shape_points.append(Vector2(cos(angle), sin(angle)) * radius) + definition = PLANET_DEFINITIONS.get_definition(definition_id) + visual = definition.visual_scene.instantiate() + visual.configure(shape_seed) + get_node("VisualHost").add_child(visual) shows_extractor = GameState.has_extractor_at(planet_name) GameState.extractor_added.connect(_on_extractor_added) queue_redraw() -func _process(delta: float) -> void: - if planet_type == "gas_giant": - gas_phase += delta - queue_redraw() - - 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. @@ -74,14 +82,9 @@ func _on_extractor_added(built_on: String) -> void: func extractor_derrick_anchor() -> Vector2: - # The rim vertex the derrick stands on: the shape's own topmost point, - # rather than a fixed radius, so it sits ON the outline whatever lumpy - # shape the seed drew instead of floating off it or sinking inside it. - var base: Vector2 = Vector2(0.0, -100.0) - for p in shape_points: - if p.y < base.y: - base = p - return base + if visual != null and visual.has_method("extractor_anchor"): + return visual.extractor_anchor() + return Vector2(0.0, -100.0) func extractor_derrick_tip() -> Vector2: @@ -113,28 +116,5 @@ func extractor_derrick_points() -> PackedVector2Array: func _draw() -> void: - if planet_type == "gas_giant": - _draw_gas_giant() - if shows_extractor: - NeonDraw.draw_glowing_polygon(self, extractor_derrick_points(), EXTRACTOR_MARKER_COLOR, 1.0) - return - NeonDraw.draw_glowing_polygon(self, shape_points, Color(0.6, 0.55, 0.5), 1.0) if shows_extractor: NeonDraw.draw_glowing_polygon(self, extractor_derrick_points(), EXTRACTOR_MARKER_COLOR, 1.0) - - -func _draw_gas_giant() -> void: - # A round, animated giant with drifting latitudinal bands. It is visibly - # unlike the rocky outline and keeps moving even while the fleet is idle. - var rim := PackedVector2Array() - for i in range(32): - var a := TAU * float(i) / 32.0 - rim.append(Vector2(cos(a) * 118.0, sin(a) * 118.0)) - NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9) - for row in [-62.0, -28.0, 8.0, 43.0, 73.0]: - var band := PackedVector2Array() - var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row)) - for i in range(15): - var x := lerpf(-half_width, half_width, float(i) / 14.0) - band.append(Vector2(x, row + sin(x * 0.045 + gas_phase * 1.6 + row) * 7.0)) - NeonDraw.draw_glowing_path(self, band, Color(0.85, 0.52, 1.0), 0.75, 0.65) diff --git a/Planet.tscn b/Planet.tscn index 844672b..0e7fe1f 100644 --- a/Planet.tscn +++ b/Planet.tscn @@ -4,3 +4,5 @@ [node name="Planet" type="Node2D"] script = ExtResource("1") + +[node name="VisualHost" type="Node2D" parent="."] diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index b13e69f..3a66ea4 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -113,8 +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_type = current_planet.planet_type - GameState.surface_profile_id = current_planet.surface_profile_id + GameState.planet_definition_id = current_planet.definition_id var tween: Tween = create_tween() tween.tween_property(fade_rect, "color:a", 1.0, fade_duration) diff --git a/PlanetDefinition.gd b/PlanetDefinition.gd new file mode 100644 index 0000000..e184241 --- /dev/null +++ b/PlanetDefinition.gd @@ -0,0 +1,10 @@ +class_name PlanetDefinition +extends Resource + +@export var definition_id: String = "rocky" +@export var display_name: String = "Rocky planet" +@export var names: PackedStringArray = [] +@export var visual_scene: PackedScene +@export var surface_profile_id: String = "rocky" +@export var resource_id: String = "ore" +@export var map_color: Color = Color(0.7, 0.65, 0.55) diff --git a/PlanetDefinition.gd.uid b/PlanetDefinition.gd.uid new file mode 100644 index 0000000..ee09e6d --- /dev/null +++ b/PlanetDefinition.gd.uid @@ -0,0 +1 @@ +uid://dt6tsdmmlnfaf diff --git a/PlanetDefinitions.gd b/PlanetDefinitions.gd new file mode 100644 index 0000000..fa9dd2d --- /dev/null +++ b/PlanetDefinitions.gd @@ -0,0 +1,14 @@ +class_name PlanetDefinitions +extends RefCounted + +const ROCKY_ID: String = "rocky" +const GAS_GIANT_ID: String = "gas_giant" +const DEFAULT_DEFINITION_ID: String = ROCKY_ID +const DEFINITIONS: Dictionary = { + ROCKY_ID: preload("res://RockyPlanetDefinition.tres"), + GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"), +} + + +static func get_definition(definition_id: String) -> Resource: + return DEFINITIONS.get(definition_id, DEFINITIONS[DEFAULT_DEFINITION_ID]) diff --git a/PlanetDefinitions.gd.uid b/PlanetDefinitions.gd.uid new file mode 100644 index 0000000..188741c --- /dev/null +++ b/PlanetDefinitions.gd.uid @@ -0,0 +1 @@ +uid://ipsld2ex2t8e diff --git a/PlanetField.gd b/PlanetField.gd index 53b6ddf..6a8c3c6 100644 --- a/PlanetField.gd +++ b/PlanetField.gd @@ -84,9 +84,7 @@ func _spawn_chunk_planet(chunk: Vector2i) -> void: planet.position = origin + data.local_pos planet.planet_name = data.name 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")) + planet.definition_id = String(data.get("definition_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/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 9f632b4..7c683d4 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -1,6 +1,7 @@ extends Node2D const SURFACE_PROFILES = preload("res://PlanetSurfaceProfiles.gd") +const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # Bootstraps the surviving fleet on this scene's own startup (from # GameState, since this is a fresh scene tree - nothing here carries @@ -22,7 +23,8 @@ var transitioning: bool = false func _ready() -> void: - _compose_surface(SURFACE_PROFILES.get_profile(GameState.surface_profile_id)) + var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id) + _compose_surface(SURFACE_PROFILES.get_profile(planet_definition.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) diff --git a/RockyPlanetDefinition.tres b/RockyPlanetDefinition.tres new file mode 100644 index 0000000..b0a3e9f --- /dev/null +++ b/RockyPlanetDefinition.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://RockyPlanetVisual.tscn" id="2"] + +[resource] +script = ExtResource("1") +definition_id = "rocky" +display_name = "Rocky planet" +names = PackedStringArray("Kepler's Rest", "Ceres Minor", "Vesta's Shadow", "Ironhold", "Drift Anchor", "Stonefall", "Grey Watch", "Silent Quarry", "Baros Rock", "Umbra Point") +visual_scene = ExtResource("2") +surface_profile_id = "rocky" +resource_id = "ore" +map_color = Color(0.7, 0.65, 0.55, 1) diff --git a/RockyPlanetVisual.gd b/RockyPlanetVisual.gd new file mode 100644 index 0000000..dc2a889 --- /dev/null +++ b/RockyPlanetVisual.gd @@ -0,0 +1,26 @@ +extends Node2D + +var shape_points: PackedVector2Array = PackedVector2Array() + + +func configure(shape_seed: int) -> void: + shape_points.clear() + var rng := RandomNumberGenerator.new() + rng.seed = shape_seed + for i in range(10): + var angle: float = (float(i) / 10.0) * TAU + var radius: float = rng.randf_range(70.0, 100.0) + shape_points.append(Vector2(cos(angle), sin(angle)) * radius) + queue_redraw() + + +func extractor_anchor() -> Vector2: + var anchor := Vector2(0.0, -100.0) + for point in shape_points: + if point.y < anchor.y: + anchor = point + return anchor + + +func _draw() -> void: + NeonDraw.draw_glowing_polygon(self, shape_points, Color(0.6, 0.55, 0.5), 1.0) diff --git a/RockyPlanetVisual.gd.uid b/RockyPlanetVisual.gd.uid new file mode 100644 index 0000000..b6ecadb --- /dev/null +++ b/RockyPlanetVisual.gd.uid @@ -0,0 +1 @@ +uid://b82xh8nosedlm diff --git a/RockyPlanetVisual.tscn b/RockyPlanetVisual.tscn new file mode 100644 index 0000000..ee6a302 --- /dev/null +++ b/RockyPlanetVisual.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://RockyPlanetVisual.gd" id="1"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") diff --git a/WorldGen.gd b/WorldGen.gd index feea7f6..5e73ee5 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -1,6 +1,8 @@ class_name WorldGen extends RefCounted +const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") + # Deterministic procedural world generation: the world is divided into # square chunks, and each chunk's biome is derived from a seeded # low-frequency noise field sampled at the chunk's own coordinates - so @@ -22,13 +24,6 @@ const GAS_GIANT_THRESHOLD: float = 0.55 const NEARBY_GAS_GIANT_CHUNK: Vector2i = Vector2i.ZERO const NEARBY_GAS_GIANT_LOCAL_POS: Vector2 = Vector2(900.0, 600.0) -const PLANET_NAMES: Array = [ - "Kepler's Rest", "Ceres Minor", "Vesta's Shadow", "Ironhold", - "Drift Anchor", "Stonefall", "Grey Watch", "Silent Quarry", - "Baros Rock", "Umbra Point", -] -const GAS_GIANT_NAMES: Array = ["Aurelia", "Boreas", "Cerulean Deep", "Helios Major", "Vesper"] - static var _noise: FastNoiseLite static var _planet_noise: FastNoiseLite @@ -88,16 +83,16 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: var guaranteed_nearby_giant: bool = chunk == NEARBY_GAS_GIANT_CHUNK var local_pos: Vector2 = NEARBY_GAS_GIANT_LOCAL_POS if guaranteed_nearby_giant \ else Vector2(rng.randf() * CHUNK_SIZE, rng.randf() * CHUNK_SIZE) - var planet_name: String = PLANET_NAMES[rng.randi() % PLANET_NAMES.size()] var shape_seed: int = rng.randi() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE - if gas_giant: - planet_name = GAS_GIANT_NAMES[rng.randi() % GAS_GIANT_NAMES.size()] + var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID + 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, - "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"} + "definition_id": definition_id} static func biome_color(biome: int) -> Color: diff --git a/WorldMapContent.gd b/WorldMapContent.gd index e8ba8df..33212a0 100644 --- a/WorldMapContent.gd +++ b/WorldMapContent.gd @@ -1,5 +1,7 @@ extends Node2D +const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") + const WORLD_TO_LOCAL: float = 0.04 # Mirrors Building.gd's own MOTHERSHIP_COLOR/CRAFTER_COLOR/COMMAND_COLOR so a @@ -105,7 +107,8 @@ func _draw() -> void: # spawn_radius_chunks of the fleet). var data: Dictionary = WorldGen.planet_data_at_chunk(chunk) var planet_local: Vector2 = origin_local + data.local_pos * WORLD_TO_LOCAL - draw_circle(planet_local, 5.0, Color(0.7, 0.65, 0.55)) + var definition: Resource = PLANET_DEFINITIONS.get_definition(String(data.definition_id)) + draw_circle(planet_local, 5.0, definition.map_color) draw_arc(planet_local, 5.0, 0.0, TAU, 16, Color(1.0, 1.0, 1.0, 0.9), 1.0, true) if chunk_has_extractor_badge(chunk): diff --git a/tests/_gas_surface_preview.gd b/tests/_gas_surface_preview.gd index 41cde0a..5dafbe5 100644 --- a/tests/_gas_surface_preview.gd +++ b/tests/_gas_surface_preview.gd @@ -3,8 +3,7 @@ extends Node2D func _ready() -> void: GameState.planet_name = "Violet Tempest" - GameState.planet_type = "gas_giant" - GameState.surface_profile_id = "gas" + GameState.planet_definition_id = "gas_giant" 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 f0797d1..94edc1d 100644 --- a/tests/test_gas_system.gd +++ b/tests/test_gas_system.gd @@ -27,8 +27,7 @@ func _process(delta: float) -> bool: return _fail("nearby gas giant chunk does not contain a planet") if WorldGen.biome_at_chunk(nearby_chunk) != WorldGen.Biome.SPACE: return _fail("nearby gas giant is not in the normal space biome") - if String(nearby_data.get("planet_type", "")) != "gas_giant" \ - or String(nearby_data.get("resource_type", "")) != "gas": + if String(nearby_data.get("definition_id", "")) != "gas_giant": return _fail("nearby guaranteed planet is not a gas source") if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0: return _fail("nearby gas giant spawned too far away: %s" % nearby_position) @@ -36,8 +35,7 @@ func _process(delta: float) -> bool: # Gas surfaces have vents only: no deferred ore pickups, and the vent # 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" + game_state.planet_definition_id = "gas_giant" var surface: Node = load("res://PlanetSurface.tscn").instantiate() root.add_child(surface) var gas_field: Node = surface.get_node("SurfaceFeatures/ResourceProvider") @@ -53,7 +51,7 @@ func _process(delta: float) -> bool: return _fail("gas-source emission animation did not advance") surface.free() - game_state.surface_profile_id = "rocky" + game_state.planet_definition_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") @@ -69,16 +67,17 @@ func _process(delta: float) -> bool: for x in range(-12, 13): for y in range(-12, 13): var data: Dictionary = WorldGen.planet_data_at_chunk(Vector2i(x, y)) - if String(data.get("planet_type", "")) == "gas_giant": - found_giant = String(data.get("resource_type", "")) == "gas" + if String(data.get("definition_id", "")) == "gas_giant": + found_giant = true break if found_giant: break if not found_giant: return _fail("world generation produced no deterministic gas giant") var giant: Node2D = load("res://Planet.tscn").instantiate() - giant.planet_type = "gas_giant" - giant._process(0.5) + giant.definition_id = "gas_giant" + root.add_child(giant) + giant.visual._process(0.5) if giant.gas_phase <= 0.0: return _fail("gas giant animation did not advance") giant.free()