spaceship/surface/PlanetSurfaceController.gd
Loïc Wisniewski 6e13b6397c Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 13:31:25 +02:00

133 lines
6.2 KiB
GDScript

extends Node2D
const SURFACE_PROFILES = preload("res://surface/PlanetSurfaceProfiles.gd")
const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
const PLANET_SETUPS = preload("res://surface/PlanetSetups.gd")
const SETUP_HOST_SCENE: PackedScene = preload("res://surface/PlanetSetupHost.tscn")
# The crew's own log, the same voice the prologue uses for the things that
# happen TO them rather than the things they are told (#212).
const CONQUEST_SPEAKER: String = "KESTREL - LOG"
# Bootstraps the surviving fleet, including each vessel type, 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
# fades back out to Main.tscn, restoring the fleet at wherever it left
# space from.
@export var fade_duration: float = 0.4
@onready var fleet: Node2D = get_node("../Fleet")
@onready var camera: Camera2D = get_node("../Camera2D")
@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/Background")
@onready var feature_host: Node2D = get_node("../SurfaceFeatures")
var transitioning: bool = false
func _ready() -> void:
var planet_definition: Resource = PLANET_DEFINITIONS.get_definition(GameState.planet_definition_id)
_compose_surface(SURFACE_PROFILES.get_profile(planet_definition.surface_profile_id))
# The Distorder's sustained travel boost is a space-navigation tool. A
# landing always returns the fleet to normal surface speed; the separate
# aimed dash remains available through its own ability button.
GameState.set_fleet_speed_multiplier(1.0)
fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO,
GameState.surviving_vessel_types)
camera.position = Vector2.ZERO
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
planet_name_label.text = GameState.planet_name
exit_button.pressed.connect(_on_exit_pressed)
fade_rect.color.a = 1.0
var tween: Tween = create_tween()
tween.tween_property(fade_rect, "color:a", 0.0, fade_duration)
func _compose_surface(profile: Resource) -> void:
# Shared with PlanetSetupEditor, deliberately: the backdrop is one
# instanced scene configured through one method, so what an author sees
# while placing markers is what a player sees after landing. See the
# header of SurfaceBackground.gd.
background.apply_profile(profile)
for caption_path in ["../UI/BankCaption", "../UI/FleetCaption", "../UI/CaptureLabel"]:
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)
var capture_label: Label = get_node("../UI/CaptureLabel")
# This controller readies before the UI sibling. Store the policy on the
# label itself so its first process tick cannot make a scripted encounter's
# deliberately hidden generic objective readout visible again.
capture_label.display_enabled = bool(profile.show_capture_status)
capture_label.visible = bool(profile.show_capture_status)
if profile.features_scene != null:
var features: Node = profile.features_scene.instantiate()
feature_host.add_child(features)
var setup: Resource = PLANET_SETUPS.setup_for_planet(GameState.planet_id, profile.profile_id)
if setup != null:
# Untyped on purpose: a static PlanetSetupHost annotation here would
# need the global class_name cache to already know about it, which
# is only true after an editor/import pass has run (see #31/#35's
# load-order notes) - a plain Node reference plus duck-typed
# apply_setup() sidesteps that entirely, same as BuildingDefinitions
# and VesselDefinitions already do for their own resource types.
var setup_host: Node = SETUP_HOST_SCENE.instantiate()
feature_host.add_child(setup_host)
setup_host.apply_setup(setup)
elif profile.encounter_scene == null:
# No PlanetSetup exists for this profile at all (#55: "void" has
# none authored) - register zero required points directly, rather
# than leaving this planet's conquest state unregistered forever.
# capture_state()'s fallback for an unregistered planet_id defaults
# to conquered=false (see test_planet_conquest_gate.gd's own "a
# fresh planet reported conquered" check), which would otherwise
# permanently block extractor placement here - a garrison of zero is
# vacuously already beaten.
GameState.register_planet_garrison(GameState.planet_id, 0)
# #212: conquest is otherwise announced only by a caption changing and a
# HUD number the player has no reason to be watching at that moment. The
# surface already carries a narration banner, so the moment gets the same
# treatment every other turn of the story does.
if not GameState.planet_conquered.is_connected(_on_planet_conquered):
GameState.planet_conquered.connect(_on_planet_conquered)
if profile.encounter_scene != null:
var encounter: Node = profile.encounter_scene.instantiate()
feature_host.add_child(encounter)
func _on_exit_pressed() -> void:
if transitioning:
return
transitioning = true
GameState.surviving_ship_count = fleet.ships.size()
GameState.surviving_vessel_types = fleet.vessel_types()
var tween: Tween = create_tween()
tween.tween_property(fade_rect, "color:a", 1.0, fade_duration)
tween.tween_callback(_return_to_space)
func _return_to_space() -> void:
get_tree().change_scene_to_file("res://world/Main.tscn")
# The banner is a sibling under UI and may not exist in a stripped-down test
# harness, so this is fetched rather than held and is allowed to be missing.
func _on_planet_conquered(planet_id: String, research_points_awarded: int) -> void:
if planet_id != GameState.planet_id:
return
var banner: Node = get_node_or_null("../UI/NarrationBanner")
if banner == null:
return
banner.say("The last of the garrison is down. This world is ours.",
CONQUEST_SPEAKER)
# Said even when it is none, because "this one paid nothing" is the
# information a player deciding where to go next actually needs.
banner.say("Research points recovered: %d." % research_points_awarded,
CONQUEST_SPEAKER)