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
This commit is contained in:
parent
78836882af
commit
6e13b6397c
42 changed files with 540 additions and 749 deletions
|
|
@ -1,9 +1,9 @@
|
|||
extends Label
|
||||
|
||||
# Polls GameState directly, same convention as ResourceReadout, so it stays
|
||||
# correct without anyone having to push updates into it. Hidden
|
||||
# entirely when the current planet has no capture points (e.g. no setup
|
||||
# resolved for it) rather than showing "0/0".
|
||||
# correct without anyone having to push updates into it. Hidden entirely when
|
||||
# the current planet has no garrison (e.g. no setup resolved for it) rather
|
||||
# than showing "0/0".
|
||||
|
||||
# A direct script preload, not the bare "PlanetDefinitions" global class_name
|
||||
# - the same load-order hazard PlanetSurfaceController's setup_host comment
|
||||
|
|
@ -20,7 +20,7 @@ func _process(_delta: float) -> void:
|
|||
visible = false
|
||||
return
|
||||
var planet_id: String = GameState.planet_id
|
||||
var total: int = GameState.total_capture_points(planet_id)
|
||||
var total: int = GameState.garrison_total(planet_id)
|
||||
if total <= 0:
|
||||
visible = false
|
||||
return
|
||||
|
|
@ -39,7 +39,8 @@ func _process(_delta: float) -> void:
|
|||
if not _was_conquered:
|
||||
_flash()
|
||||
else:
|
||||
text = "CAPTURED %d/%d" % [GameState.captured_point_count(planet_id), total]
|
||||
# #211: what is left to fight, counted down, rather than rings held.
|
||||
text = "GARRISON %d/%d" % [GameState.garrison_remaining(planet_id), total]
|
||||
_was_conquered = conquered
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
class_name CapturePoint
|
||||
extends Area2D
|
||||
|
||||
# One point on a landed planet's surface that must be held (fleet ship
|
||||
# nearby, no enemy nearby) for capture_time seconds. Progress is polled by
|
||||
# distance each frame rather than through physics signals - same idiom as
|
||||
# RockField/GasVentField - since no collision response is needed, only
|
||||
# proximity.
|
||||
|
||||
signal captured
|
||||
|
||||
enum State { NEUTRAL, BLOCKED, CAPTURING, CAPTURED }
|
||||
|
||||
@export var point_index: int = 0
|
||||
@export var capture_radius: float = 220.0
|
||||
# #177: holding an already-cleared point should resolve briskly. Eight seconds
|
||||
# made every point feel like waiting after the fight was over.
|
||||
@export var capture_time: float = 4.0
|
||||
@export var enemy_clear_radius: float = 700.0
|
||||
@export var drain_rate: float = 0.5
|
||||
|
||||
const NEUTRAL_COLOR: Color = Color(0.55, 0.85, 1.0)
|
||||
const BLOCKED_COLOR: Color = Color(1.0, 0.3, 0.3)
|
||||
const CAPTURING_COLOR: Color = Color(0.4, 1.0, 0.55)
|
||||
const CAPTURED_COLOR: Color = Color(1.0, 0.85, 0.3)
|
||||
const CORE_RADIUS: float = 60.0
|
||||
|
||||
var progress: float = 0.0
|
||||
var state: int = State.NEUTRAL
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Behind the fleet and enemies, same convention as every other world
|
||||
# object that must never occlude them (Planet.gd, Rock.gd, Building.gd,
|
||||
# Hauler.gd, Transporter.gd, VesselDelivery.gd all set this in _ready()).
|
||||
# "Is an enemy inside my capture radius" is the core read of this
|
||||
# mechanic, so a ship or enemy sitting in the ring must stay fully visible.
|
||||
z_index = -1
|
||||
queue_redraw()
|
||||
|
||||
|
||||
# Called by PlanetSetupHost when this point was already captured on a
|
||||
# previous visit, so re-landing restores it instead of starting the fill
|
||||
# over. Permanent for the run - _process() below returns early once
|
||||
# CAPTURED and never re-evaluates it.
|
||||
func mark_captured() -> void:
|
||||
progress = 1.0
|
||||
state = State.CAPTURED
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if state == State.CAPTURED:
|
||||
return
|
||||
|
||||
var enemy_nearby: bool = _enemy_nearby()
|
||||
var ship_in_range: bool = _ship_in_range()
|
||||
|
||||
if enemy_nearby:
|
||||
state = State.BLOCKED
|
||||
progress = maxf(progress - drain_rate * delta / capture_time, 0.0)
|
||||
elif ship_in_range:
|
||||
state = State.CAPTURING
|
||||
progress = minf(progress + delta / capture_time, 1.0)
|
||||
if progress >= 1.0:
|
||||
state = State.CAPTURED
|
||||
GameState.mark_point_captured(GameState.planet_id, point_index)
|
||||
captured.emit()
|
||||
else:
|
||||
state = State.NEUTRAL
|
||||
progress = maxf(progress - drain_rate * delta / capture_time, 0.0)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _ship_in_range() -> bool:
|
||||
for ship in get_tree().get_nodes_in_group("player_ships"):
|
||||
if is_instance_valid(ship) and global_position.distance_to(ship.global_position) <= capture_radius:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _enemy_nearby() -> bool:
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(enemy) and global_position.distance_to(enemy.global_position) <= enemy_clear_radius:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _state_color() -> Color:
|
||||
match state:
|
||||
State.BLOCKED:
|
||||
return BLOCKED_COLOR
|
||||
State.CAPTURING:
|
||||
return CAPTURING_COLOR
|
||||
State.CAPTURED:
|
||||
return CAPTURED_COLOR
|
||||
_:
|
||||
return NEUTRAL_COLOR
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var color: Color = _state_color()
|
||||
# Outer ring: the actual capture radius, faint - tells the player how
|
||||
# close they need to stay without dominating the screen.
|
||||
_draw_ring(capture_radius, 0.0, TAU, color, 0.35)
|
||||
# Inner beacon: fixed size regardless of capture_radius, so it reads
|
||||
# clearly even when zoomed out. A filling arc sweeps clockwise from the
|
||||
# top as progress advances, same "fill toward completion" idea as
|
||||
# VesselGasBomberComponent's hull fill, adapted to a ring since a
|
||||
# capture point has no hull.
|
||||
_draw_ring(CORE_RADIUS, 0.0, TAU, color, 1.0)
|
||||
if progress > 0.001:
|
||||
var sweep: float = TAU * progress
|
||||
_draw_ring(CORE_RADIUS, -PI / 2.0, -PI / 2.0 + sweep, color, 1.0, 10.0)
|
||||
|
||||
|
||||
func _draw_ring(radius: float, from_angle: float, to_angle: float, color: Color, alpha: float,
|
||||
core_width: float = 6.0) -> void:
|
||||
draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(color, 0.16 * alpha), core_width * 2.6, true)
|
||||
draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(color, 0.5 * alpha), core_width, true)
|
||||
draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(1.0, 0.96, 0.9, 0.9 * alpha), core_width * 0.3, true)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://dwt256xl1txjo
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://surface/CapturePoint.gd" id="1"]
|
||||
|
||||
[node name="CapturePoint" type="Area2D"]
|
||||
script = ExtResource("1")
|
||||
monitoring = false
|
||||
monitorable = false
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
class_name PlanetSetup
|
||||
extends Resource
|
||||
|
||||
# A hand-authored capture-point/enemy layout for a planet surface. Chosen
|
||||
# A hand-authored enemy layout for a planet surface. Chosen
|
||||
# deterministically per planet (see PlanetSetups.setup_for_planet) rather
|
||||
# than generated - #40 wants curated setups, not procedural ones - and
|
||||
# stored as a committed .tres under res://PlanetSetups/, not in a save.
|
||||
|
|
@ -11,13 +11,14 @@ extends Resource
|
|||
# Which PlanetSurfaceProfiles id (e.g. "rocky", "gas") this setup was
|
||||
# authored for; only setups matching a planet's own profile are eligible.
|
||||
@export var surface_profile_id: String = "rocky"
|
||||
# Surface-local positions (the fleet lands at Vector2.ZERO).
|
||||
@export var capture_points: PackedVector2Array = PackedVector2Array()
|
||||
# Surface-local positions (the fleet lands at Vector2.ZERO). #211 removed the
|
||||
# capture_points that used to sit beside these: the garrison IS the objective
|
||||
# now, so the enemies are the whole of what a setup places.
|
||||
@export var enemy_spawns: Array[Resource] = []
|
||||
# Research points awarded for conquering this map, set in the map editor.
|
||||
# A harder lab is worth more, and how hard a lab IS depends on the layout
|
||||
# authored here - how many capture points, how many and what kind of
|
||||
# defenders - which the planet type cannot know. 0 means "use whatever the
|
||||
# authored here - how many and what kind of defenders - which the planet
|
||||
# type cannot know. 0 means "use whatever the
|
||||
# planet type awards", so every setup written before this existed keeps its
|
||||
# old value rather than silently dropping to nothing.
|
||||
@export var research_point_reward: int = 0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
extends Node2D
|
||||
|
||||
# Authoring tool for PlanetSetup resources (#40/#48): place/move/delete
|
||||
# capture points and enemy spawns for a chosen surface profile, then save
|
||||
# enemy spawns for a chosen surface profile, then save
|
||||
# to res://PlanetSetups/<id>.tres so the game picks it up the same way it
|
||||
# picks up every other authored setup. Deliberately does not touch
|
||||
# GameState - this tool only edits committed resources, never gameplay
|
||||
|
|
@ -38,7 +38,6 @@ const ORIGIN_COLOR: Color = Color(1.0, 0.9, 0.4)
|
|||
# cross-class - a global class_name lookup at parse time needs the class
|
||||
# cache already warm (see #42/#44's load-order notes), which a plain
|
||||
# local const sidesteps entirely.
|
||||
const CAPTURE_MARKER_COLOR: Color = Color(0.55, 0.85, 1.0)
|
||||
const PALETTE_STEP: float = 92.0
|
||||
|
||||
@onready var camera: Camera2D = $Camera2D
|
||||
|
|
@ -68,7 +67,11 @@ var current_setup: Resource
|
|||
# saved. Saving writes HERE rather than to a path derived from setup_id - see
|
||||
# PlanetSetups.path_for_id for why deriving it forks a duplicate.
|
||||
var current_setup_path: String = ""
|
||||
var selected_palette: String = "capture_point"
|
||||
# #211 took the capture points out of the palette, so the opening tool is the
|
||||
# first enemy kind. Resolved in _build_palette() rather than hardcoded here,
|
||||
# so removing an enemy definition cannot leave the editor pointing at a tool
|
||||
# that no longer exists.
|
||||
var selected_palette: String = ""
|
||||
var selected_item: Dictionary = {}
|
||||
var _profile_ids: Array = []
|
||||
var _profile_cursor: int = 0
|
||||
|
|
@ -130,7 +133,7 @@ func new_setup(setup_id: String, display_name: String, profile_id: String) -> vo
|
|||
current_setup.setup_id = setup_id
|
||||
current_setup.display_name = display_name
|
||||
current_setup.surface_profile_id = profile_id
|
||||
# capture_points/enemy_spawns are left at PlanetSetup's own declared
|
||||
# enemy_spawns is left at PlanetSetup's own declared
|
||||
# defaults (already empty) rather than reassigned here - assigning a
|
||||
# plain `[]` to enemy_spawns from outside PlanetSetup.gd's own file
|
||||
# fails at runtime, since the compiler here only sees current_setup as
|
||||
|
|
@ -220,34 +223,6 @@ func delete_setup(setup_id: String) -> Dictionary:
|
|||
return {"ok": true, "message": "Deleted '%s'." % setup_id}
|
||||
|
||||
|
||||
# --- model: capture points ---------------------------------------------
|
||||
|
||||
func add_capture_point(at: Vector2) -> int:
|
||||
if current_setup == null:
|
||||
return -1
|
||||
var points: PackedVector2Array = current_setup.capture_points
|
||||
points.append(at)
|
||||
current_setup.capture_points = points
|
||||
_rebuild_markers()
|
||||
return points.size() - 1
|
||||
|
||||
|
||||
func move_capture_point(index: int, at: Vector2) -> void:
|
||||
if current_setup == null or index < 0 or index >= current_setup.capture_points.size():
|
||||
return
|
||||
current_setup.capture_points[index] = at
|
||||
_rebuild_markers()
|
||||
|
||||
|
||||
func remove_capture_point(index: int) -> void:
|
||||
if current_setup == null or index < 0 or index >= current_setup.capture_points.size():
|
||||
return
|
||||
var points: PackedVector2Array = current_setup.capture_points
|
||||
points.remove_at(index)
|
||||
current_setup.capture_points = points
|
||||
_rebuild_markers()
|
||||
|
||||
|
||||
# --- model: enemy spawns ------------------------------------------------
|
||||
|
||||
func add_enemy(enemy_id: String, at: Vector2) -> int:
|
||||
|
|
@ -300,24 +275,10 @@ func _rebuild_markers() -> void:
|
|||
child.queue_free()
|
||||
if current_setup == null:
|
||||
return
|
||||
for i in range(current_setup.capture_points.size()):
|
||||
_add_capture_marker(i, current_setup.capture_points[i])
|
||||
for i in range(current_setup.enemy_spawns.size()):
|
||||
_add_enemy_marker(i, current_setup.enemy_spawns[i])
|
||||
|
||||
|
||||
func _add_capture_marker(index: int, at: Vector2) -> void:
|
||||
var is_selected: bool = selected_item.get("kind", "") == "capture" and selected_item.get("index", -1) == index
|
||||
var color: Color = Color(1.0, 0.95, 0.9) if is_selected else CAPTURE_MARKER_COLOR
|
||||
var marker: NeonPiece = NeonPiece.create(item_host, at, color)
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(16):
|
||||
var angle: float = float(i) / 16.0 * TAU
|
||||
points.append(Vector2(cos(angle), sin(angle)) * 60.0)
|
||||
marker.add_polygon(points, 0.6)
|
||||
marker.queue_redraw()
|
||||
|
||||
|
||||
func _add_enemy_marker(index: int, spawn: Resource) -> void:
|
||||
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
|
||||
var is_selected: bool = selected_item.get("kind", "") == "enemy" and selected_item.get("index", -1) == index
|
||||
|
|
@ -335,16 +296,22 @@ func _add_enemy_marker(index: int, spawn: Resource) -> void:
|
|||
# --- palette ---------------------------------------------------------------
|
||||
|
||||
func _build_palette() -> void:
|
||||
var entries: Array = [{"id": "capture_point", "is_point": true, "definition": null}]
|
||||
var entries: Array = []
|
||||
for definition in ENEMY_DEFINITIONS.all():
|
||||
entries.append({"id": definition.enemy_id, "is_point": false, "definition": definition})
|
||||
entries.append({"id": definition.enemy_id, "definition": definition})
|
||||
if entries.is_empty():
|
||||
return
|
||||
# The palette opens on its first tool. Before #211 that was the capture
|
||||
# point, which was always present; an enemy list could in principle be
|
||||
# reordered, so the default is read off the list itself.
|
||||
if selected_palette == "" or not entries.any(func(e): return e.id == selected_palette):
|
||||
selected_palette = String(entries[0].id)
|
||||
|
||||
var x: float = 0.0
|
||||
for entry in entries:
|
||||
var button: Control = PALETTE_ENTRY_BUTTON_SCRIPT.new()
|
||||
button.position = Vector2(x, 0)
|
||||
button.size = Vector2(84, 84)
|
||||
button.is_capture_point = entry.is_point
|
||||
button.enemy_definition = entry.definition
|
||||
button.selected = entry.id == selected_palette
|
||||
button.pressed.connect(_on_palette_pressed.bind(entry.id))
|
||||
|
|
@ -360,8 +327,7 @@ func _on_palette_pressed(id: String) -> void:
|
|||
|
||||
func _refresh_palette_selection() -> void:
|
||||
for button in _palette_buttons:
|
||||
var entry_id: String = "capture_point" if button.is_capture_point else button.enemy_definition.enemy_id
|
||||
button.selected = entry_id == selected_palette
|
||||
button.selected = String(button.enemy_definition.enemy_id) == selected_palette
|
||||
|
||||
|
||||
# --- UI handlers -------------------------------------------------------
|
||||
|
|
@ -468,10 +434,7 @@ func _on_delete_pressed() -> void:
|
|||
if selected_item.is_empty():
|
||||
_set_status("Nothing selected to delete.")
|
||||
return
|
||||
if selected_item.kind == "capture":
|
||||
remove_capture_point(selected_item.index)
|
||||
else:
|
||||
remove_enemy(selected_item.index)
|
||||
remove_enemy(selected_item.index)
|
||||
selected_item = {}
|
||||
_refresh_selected_label()
|
||||
|
||||
|
|
@ -506,21 +469,15 @@ func _refresh_selected_label() -> void:
|
|||
selected_label.text = ""
|
||||
return
|
||||
var index: int = int(selected_item.index)
|
||||
if selected_item.kind == "capture":
|
||||
selected_label.text = "Selected: Capture Point #%d" % (index + 1)
|
||||
else:
|
||||
var spawn: Resource = current_setup.enemy_spawns[index]
|
||||
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
|
||||
selected_label.text = "Selected: %s #%d" % [definition.display_name, index + 1]
|
||||
var spawn: Resource = current_setup.enemy_spawns[index]
|
||||
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
|
||||
selected_label.text = "Selected: %s #%d" % [definition.display_name, index + 1]
|
||||
|
||||
|
||||
# --- placement/selection helpers ----------------------------------------
|
||||
|
||||
func _place_selected(world_pos: Vector2) -> void:
|
||||
if selected_palette == "capture_point":
|
||||
add_capture_point(world_pos)
|
||||
else:
|
||||
add_enemy(selected_palette, world_pos)
|
||||
add_enemy(selected_palette, world_pos)
|
||||
|
||||
|
||||
func _find_marker_near(world_pos: Vector2) -> Dictionary:
|
||||
|
|
@ -528,11 +485,6 @@ func _find_marker_near(world_pos: Vector2) -> Dictionary:
|
|||
return {}
|
||||
var best: Dictionary = {}
|
||||
var best_dist: float = HIT_RADIUS
|
||||
for i in range(current_setup.capture_points.size()):
|
||||
var d: float = world_pos.distance_to(current_setup.capture_points[i])
|
||||
if d <= best_dist:
|
||||
best_dist = d
|
||||
best = {"kind": "capture", "index": i}
|
||||
for i in range(current_setup.enemy_spawns.size()):
|
||||
var d: float = world_pos.distance_to(current_setup.enemy_spawns[i].position)
|
||||
if d <= best_dist:
|
||||
|
|
@ -544,10 +496,7 @@ func _find_marker_near(world_pos: Vector2) -> Dictionary:
|
|||
func _move_selected(world_pos: Vector2) -> void:
|
||||
if dragging_item.is_empty():
|
||||
return
|
||||
if dragging_item.kind == "capture":
|
||||
move_capture_point(dragging_item.index, world_pos)
|
||||
else:
|
||||
move_enemy(dragging_item.index, world_pos)
|
||||
move_enemy(dragging_item.index, world_pos)
|
||||
|
||||
|
||||
func _screen_to_world(screen_pos: Vector2) -> Vector2:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
class_name PlanetSetupHost
|
||||
extends Node2D
|
||||
|
||||
# Instantiates the contents of a PlanetSetup onto a landed surface: one
|
||||
# CapturePoint per authored point - registering/restoring conquest progress
|
||||
# through GameState so re-landing on a partly-taken planet keeps its
|
||||
# progress - and one enemy per authored spawn.
|
||||
# Instantiates the contents of a PlanetSetup onto a landed surface: one enemy
|
||||
# per authored spawn, registered with GameState as the planet's garrison so
|
||||
# that killing the last of them takes the planet.
|
||||
#
|
||||
# #211 removed the capture points this used to place alongside them. Holding a
|
||||
# ring for four seconds, once per ring, was a second task laid on top of the
|
||||
# fight the player had already won; conquest is the fight now.
|
||||
|
||||
const CAPTURE_POINT_SCENE: PackedScene = preload("res://surface/CapturePoint.tscn")
|
||||
const ENEMY_SCENE: PackedScene = preload("res://enemies/Enemy.tscn")
|
||||
|
||||
var setup: Resource
|
||||
|
|
@ -19,42 +21,26 @@ func apply_setup(new_setup: Resource) -> void:
|
|||
if setup == null:
|
||||
return
|
||||
|
||||
GameState.register_capture_points(GameState.planet_id, setup.capture_points.size(),
|
||||
GameState.planet_definition_id, int(setup.research_point_reward))
|
||||
for i in range(setup.capture_points.size()):
|
||||
_add_capture_point(i, setup.capture_points[i])
|
||||
if _garrison_is_defeated():
|
||||
# Registered with the count about to be placed, every landing. A planet
|
||||
# already taken keeps that status and gets no garrison at all.
|
||||
GameState.register_planet_garrison(GameState.planet_id,
|
||||
setup.enemy_spawns.size(), GameState.planet_definition_id,
|
||||
int(setup.research_point_reward))
|
||||
if GameState.is_planet_conquered(GameState.planet_id):
|
||||
return
|
||||
for spawn in setup.enemy_spawns:
|
||||
_add_enemy(spawn)
|
||||
|
||||
|
||||
# A conquered planet stays conquered. Its garrison was destroyed to take it,
|
||||
# so re-landing must not quietly restore it - the capture points already come
|
||||
# back marked captured, and enemies standing on a planet the player owns read
|
||||
# as the conquest having been undone.
|
||||
#
|
||||
# Guarded on there being capture points to take, not on the conquered flag
|
||||
# alone: register_capture_points() treats a setup with none as vacuously
|
||||
# conquered (see PlanetSurfaceController's note on unregistered planets), and
|
||||
# a setup that authored enemies but no capture points is a garrison nobody
|
||||
# can ever have beaten. Suppressing those would delete them on first sight.
|
||||
func _garrison_is_defeated() -> bool:
|
||||
return not setup.capture_points.is_empty() \
|
||||
and GameState.is_planet_conquered(GameState.planet_id)
|
||||
|
||||
|
||||
func _add_capture_point(index: int, at: Vector2) -> void:
|
||||
var point: Node = CAPTURE_POINT_SCENE.instantiate()
|
||||
point.point_index = index
|
||||
point.position = at
|
||||
add_child(point)
|
||||
if GameState.is_point_captured(GameState.planet_id, index):
|
||||
point.mark_captured()
|
||||
|
||||
|
||||
func _add_enemy(spawn: Resource) -> void:
|
||||
var enemy: Node = ENEMY_SCENE.instantiate()
|
||||
enemy.enemy_id = spawn.enemy_id
|
||||
enemy.position = spawn.position
|
||||
# The planet is captured by the count reaching zero, so the tally is bound
|
||||
# to the enemy's own death rather than to whatever killed it. Bound to the
|
||||
# id captured NOW: the player may have left the surface by the time a
|
||||
# straggler dies, and the planet that was being fought over is this one.
|
||||
var planet_key: String = GameState.planet_id
|
||||
enemy.destroyed.connect(func():
|
||||
GameState.mark_garrison_enemy_destroyed(planet_key))
|
||||
add_child(enemy)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ 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
|
||||
|
|
@ -81,9 +84,16 @@ func _compose_surface(profile: Resource) -> void:
|
|||
# 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 - register_capture_
|
||||
# points() treats a total of 0 as vacuously already conquered.
|
||||
GameState.register_capture_points(GameState.planet_id, 0)
|
||||
# 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()
|
||||
|
|
@ -105,3 +115,19 @@ func _on_exit_pressed() -> void:
|
|||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue