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
|
|
@ -38,6 +38,5 @@ script = ExtResource("2")
|
|||
setup_id = "research_lab_1"
|
||||
display_name = "Research Lab Garrison"
|
||||
surface_profile_id = "research_lab"
|
||||
capture_points = PackedVector2Array(-450.0, -60.0, 460.0, -40.0, 0.0, 480.0)
|
||||
enemy_spawns = Array[Resource]([SubResource("Resource_ta1"), SubResource("Resource_ta2"), SubResource("Resource_ta3"), SubResource("Resource_ta4"), SubResource("Resource_ta5"), SubResource("Resource_ta6")])
|
||||
research_point_reward = 1
|
||||
|
|
|
|||
|
|
@ -46,6 +46,5 @@ script = ExtResource("1")
|
|||
setup_id = "gas_default"
|
||||
display_name = "Gas Default"
|
||||
surface_profile_id = "gas"
|
||||
capture_points = PackedVector2Array(600, -200, -550, -250)
|
||||
enemy_spawns = Array[Resource]([SubResource("Resource_che8f"), SubResource("Resource_js1ws"), SubResource("Resource_y4at1"), SubResource("Resource_rosps"), SubResource("Resource_68rmi"), SubResource("Resource_yx160"), SubResource("Resource_clcbd"), SubResource("Resource_yk26s"), SubResource("Resource_7m420")])
|
||||
research_point_reward = 1
|
||||
|
|
|
|||
|
|
@ -41,6 +41,5 @@ script = ExtResource("2_oxc3b")
|
|||
setup_id = "researtch_lab_garrison"
|
||||
display_name = "Researtch Lab Garrison"
|
||||
surface_profile_id = "research_lab"
|
||||
capture_points = PackedVector2Array(-450, -60, 460, -40, 0, 480)
|
||||
enemy_spawns = Array[Resource]([SubResource("Resource_p34gm"), SubResource("Resource_oxc3b"), SubResource("Resource_ujs3j"), SubResource("Resource_4h5ny"), SubResource("Resource_1djs2"), SubResource("Resource_isvqt"), SubResource("Resource_ff4yc")])
|
||||
research_point_reward = 1
|
||||
|
|
|
|||
|
|
@ -31,6 +31,5 @@ position = Vector2(-985.817, -129.31296)
|
|||
script = ExtResource("1")
|
||||
setup_id = "rocky_default"
|
||||
display_name = "Rocky Default"
|
||||
capture_points = PackedVector2Array(700, 100, -593.01917, 1.8398132)
|
||||
enemy_spawns = Array[Resource]([SubResource("Resource_kkunv"), SubResource("Resource_gk6ea"), SubResource("Resource_ob70o"), SubResource("Resource_np188"), SubResource("Resource_ifyky"), SubResource("Resource_e10vg")])
|
||||
research_point_reward = 1
|
||||
|
|
|
|||
|
|
@ -21,10 +21,9 @@ var encounter_active: bool = false
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
# One persistent objective lets boss victory flow through the same conquest
|
||||
# state as authored capture-point maps without pretending the boss is a
|
||||
# configurable capture point.
|
||||
game_state.register_capture_points(game_state.planet_id, 1,
|
||||
# A garrison of one lets boss victory flow through the same conquest state
|
||||
# as an authored map without pretending the boss is an ordinary defender.
|
||||
game_state.register_planet_garrison(game_state.planet_id, 1,
|
||||
game_state.planet_definition_id)
|
||||
if game_state.is_planet_conquered(game_state.planet_id):
|
||||
boss.suppress_as_defeated()
|
||||
|
|
@ -151,7 +150,7 @@ func _on_boss_health_changed(current: int, maximum: int) -> void:
|
|||
func _on_boss_defeated() -> void:
|
||||
game_state.encounter_locks_movement = false
|
||||
camera.offset = Vector2.ZERO
|
||||
game_state.mark_point_captured(game_state.planet_id, 0)
|
||||
game_state.mark_garrison_enemy_destroyed(game_state.planet_id)
|
||||
health_bar.value = 0
|
||||
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
|
||||
hint_label.text = ""
|
||||
|
|
|
|||
|
|
@ -2817,31 +2817,40 @@ func _advance_distorder_drain(delta: float, boost_in_use: bool = true) -> void:
|
|||
boost_depleted_notice = true
|
||||
|
||||
|
||||
# Planet conquest: a planet must have every one of its capture points held
|
||||
# before an extractor can be built there (see BuildController's placement
|
||||
# gates). Keyed by the same stable planet_id as `extractors`, entirely
|
||||
# separate from it - a planet can be conquered long before anyone builds an
|
||||
# extractor on it, or never get one at all.
|
||||
# Planet conquest: a planet is held once its garrison is dead, and only then
|
||||
# can an extractor be built there (see BuildController's placement gates).
|
||||
# Keyed by the same stable planet_id as `extractors`, entirely separate from
|
||||
# it - a planet can be conquered long before anyone builds an extractor on
|
||||
# it, or never get one at all.
|
||||
#
|
||||
# #211 replaced capture points - stand in a ring, wait out a timer, once per
|
||||
# point - with the fight itself. Taking a planet is now exactly "kill what is
|
||||
# defending it", which is what players were doing anyway before walking to
|
||||
# the rings.
|
||||
var planet_capture: Dictionary = {}
|
||||
|
||||
signal planet_conquered(planet_id: String)
|
||||
# #212: carries the research points the conquest actually paid, so whatever
|
||||
# announces it does not have to reconstruct the award from the definition and
|
||||
# the setup's override and get a different answer.
|
||||
signal planet_conquered(planet_id: String, research_points_awarded: int)
|
||||
|
||||
|
||||
func register_capture_points(planet_key: String, total: int, definition_id: String = "",
|
||||
reward_override: int = 0) -> void:
|
||||
# Idempotent: landing again on a planet must not wipe points already
|
||||
# captured on a previous visit.
|
||||
if planet_capture.has(planet_key):
|
||||
# Declares what is defending a planet. Called every time the surface is
|
||||
# composed, because the garrison is re-placed every time the player lands:
|
||||
# the count has to start again with the enemies it counts, or a planet left
|
||||
# half-cleared would fall to a single kill on the next visit.
|
||||
#
|
||||
# A planet already conquered is left alone entirely - its garrison is dead
|
||||
# and stays dead.
|
||||
func register_planet_garrison(planet_key: String, enemy_count: int,
|
||||
definition_id: String = "", reward_override: int = 0) -> void:
|
||||
var existing: Dictionary = planet_capture.get(planet_key, {})
|
||||
if bool(existing.get("conquered", false)):
|
||||
return
|
||||
var captured: Array = []
|
||||
captured.resize(total)
|
||||
captured.fill(false)
|
||||
# A planet demanding zero points is vacuously already held (#55: a
|
||||
# black hole's "void" profile has no authored PlanetSetup at all, so
|
||||
# this is the only registration it ever gets) - every planet WITH real
|
||||
# points still starts unconquered, exactly as before. The planet definition
|
||||
# supplies an optional conquest reward, avoiding planet-id special cases in
|
||||
# the capture system.
|
||||
# A planet defending itself with nothing is vacuously already held (#55: a
|
||||
# black hole's "void" profile has no authored PlanetSetup at all, so this
|
||||
# is the only registration it ever gets). The planet definition supplies an
|
||||
# optional conquest reward, avoiding planet-id special cases here.
|
||||
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
|
||||
var point_reward: int = 0 if definition == null else int(definition.conquest_research_points)
|
||||
# A map may be worth more than its planet type's flat rate - a harder
|
||||
|
|
@ -2849,63 +2858,64 @@ func register_capture_points(planet_key: String, total: int, definition_id: Stri
|
|||
# is. Zero means the setup did not express an opinion.
|
||||
if reward_override > 0:
|
||||
point_reward = reward_override
|
||||
planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0,
|
||||
"research_point_reward": point_reward, "research_awarded": false}
|
||||
planet_capture[planet_key] = {
|
||||
"remaining": enemy_count,
|
||||
"total": enemy_count,
|
||||
"conquered": enemy_count <= 0,
|
||||
# Carried across landings: the reward is paid once, and a planet
|
||||
# re-registered on a second visit must not become payable again.
|
||||
"research_point_reward": point_reward,
|
||||
"research_awarded": bool(existing.get("research_awarded", false)),
|
||||
}
|
||||
|
||||
|
||||
func capture_state(planet_key: String) -> Dictionary:
|
||||
return planet_capture.get(planet_key, {"captured": [], "conquered": false})
|
||||
return planet_capture.get(planet_key,
|
||||
{"remaining": 0, "total": 0, "conquered": false})
|
||||
|
||||
|
||||
func is_point_captured(planet_key: String, index: int) -> bool:
|
||||
var captured: Array = capture_state(planet_key).get("captured", [])
|
||||
if index < 0 or index >= captured.size():
|
||||
return false
|
||||
return bool(captured[index])
|
||||
# How much of the garrison is still standing, and how big it was. Both are
|
||||
# for the surface readout; conquest itself is decided below.
|
||||
func garrison_remaining(planet_key: String) -> int:
|
||||
return int(capture_state(planet_key).get("remaining", 0))
|
||||
|
||||
|
||||
func mark_point_captured(planet_key: String, index: int) -> void:
|
||||
func garrison_total(planet_key: String) -> int:
|
||||
return int(capture_state(planet_key).get("total", 0))
|
||||
|
||||
|
||||
# One defender down. Called from the enemy's own destroyed signal, so it
|
||||
# counts kills by whatever means - a beam, a bomb, a ramming hull.
|
||||
func mark_garrison_enemy_destroyed(planet_key: String) -> void:
|
||||
if not planet_capture.has(planet_key):
|
||||
return
|
||||
var state: Dictionary = planet_capture[planet_key]
|
||||
var captured: Array = state.captured
|
||||
if index < 0 or index >= captured.size():
|
||||
if bool(state.conquered):
|
||||
return
|
||||
captured[index] = true
|
||||
if state.conquered:
|
||||
return
|
||||
if captured.all(func(c): return bool(c)):
|
||||
state.remaining = maxi(int(state.get("remaining", 0)) - 1, 0)
|
||||
if int(state.remaining) <= 0:
|
||||
state.conquered = true
|
||||
planet_conquered.emit(planet_key)
|
||||
# #63: definition-authored research points, exactly once per planet.
|
||||
# state.conquered flipping above already makes a second call to this
|
||||
# whole function a no-op via the early return above it, but
|
||||
# research_awarded is its own explicit guard on the award itself
|
||||
# regardless of how conquest was reached.
|
||||
var reward: int = int(state.get("research_point_reward", 0))
|
||||
var awarded: int = 0
|
||||
if reward > 0 and not bool(state.get("research_awarded", false)):
|
||||
state.research_awarded = true
|
||||
awarded = reward
|
||||
research_points += reward
|
||||
research_points_changed.emit(research_points)
|
||||
# Emitted AFTER the award, so a listener that reports the reward is
|
||||
# reporting one that has already been paid (#212).
|
||||
planet_conquered.emit(planet_key, awarded)
|
||||
|
||||
|
||||
func is_planet_conquered(planet_key: String) -> bool:
|
||||
return bool(capture_state(planet_key).get("conquered", false))
|
||||
|
||||
|
||||
func captured_point_count(planet_key: String) -> int:
|
||||
var captured: Array = capture_state(planet_key).get("captured", [])
|
||||
var count: int = 0
|
||||
for c in captured:
|
||||
if c:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func total_capture_points(planet_key: String) -> int:
|
||||
return capture_state(planet_key).get("captured", []).size()
|
||||
|
||||
|
||||
# --- waves (#100) ----------------------------------------------------------
|
||||
#
|
||||
# #97 replaces the roaming spawner - one enemy every few seconds, forever -
|
||||
|
|
@ -2973,13 +2983,22 @@ func wave_strength(fleet_size: int) -> int:
|
|||
|
||||
|
||||
# Spent most-expensive-first, so a large budget buys heavier enemies rather
|
||||
# than an ever-larger swarm of chasers. min_strength keeps the openings
|
||||
# honest: the first waves are chasers whatever the arithmetic says.
|
||||
# than an ever-larger swarm of chasers.
|
||||
#
|
||||
# Two gates, not one. min_strength was meant to keep the openings honest, but
|
||||
# strength counts the wave NUMBER as well, so a player who had grown nothing
|
||||
# still met lancers and then snipers simply by surviving - #218's "the first
|
||||
# waves are too complex".
|
||||
#
|
||||
# min_fleet is the gate that actually holds: an enemy type stays out of the
|
||||
# roster until the fleet is big enough to have earned it. At BASE_FLEET_SIZE
|
||||
# the player meets chasers and nothing else, however long they take, and each
|
||||
# vessel past the starting three opens exactly one new kind of trouble.
|
||||
const WAVE_ROSTER: Array = [
|
||||
{"id": "artillery", "cost": 9, "min_strength": 26},
|
||||
{"id": "sniper", "cost": 6, "min_strength": 16},
|
||||
{"id": "lancer", "cost": 4, "min_strength": 12},
|
||||
{"id": "chaser", "cost": 3, "min_strength": 0},
|
||||
{"id": "artillery", "cost": 9, "min_strength": 26, "min_fleet": 6},
|
||||
{"id": "sniper", "cost": 6, "min_strength": 16, "min_fleet": 5},
|
||||
{"id": "lancer", "cost": 4, "min_strength": 12, "min_fleet": 4},
|
||||
{"id": "chaser", "cost": 3, "min_strength": 0, "min_fleet": 0},
|
||||
]
|
||||
const WAVE_MAX_ENEMIES: int = 18
|
||||
|
||||
|
|
@ -2989,7 +3008,8 @@ func wave_composition(fleet_size: int) -> PackedStringArray:
|
|||
var strength: int = budget
|
||||
var out: PackedStringArray = PackedStringArray()
|
||||
for entry in WAVE_ROSTER:
|
||||
if strength < int(entry.min_strength):
|
||||
if strength < int(entry.min_strength) \
|
||||
or fleet_size < int(entry.get("min_fleet", 0)):
|
||||
continue
|
||||
var cost: int = int(entry.cost)
|
||||
# The cheapest tier is last and takes whatever is left, so the budget
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ extends RefCounted
|
|||
#
|
||||
# 1. Nothing here may ask for something the player cannot yet reach. The
|
||||
# sharp edge is RESEARCH POINTS - they come only from conquering a
|
||||
# planet (GameState.mark_point_captured), so every node with a
|
||||
# planet (GameState.mark_garrison_enemy_destroyed), so every node with a
|
||||
# research_points cost sits after `conquest` below. Ore-only nodes -
|
||||
# Shipyards, Logistics - may come before it.
|
||||
# 2. A quest names ONE goal, and the brief says what it will take. Asking
|
||||
|
|
|
|||
|
|
@ -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,9 +434,6 @@ 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)
|
||||
selected_item = {}
|
||||
_refresh_selected_label()
|
||||
|
|
@ -506,9 +469,6 @@ 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]
|
||||
|
|
@ -517,9 +477,6 @@ func _refresh_selected_label() -> void:
|
|||
# --- 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)
|
||||
|
||||
|
||||
|
|
@ -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,9 +496,6 @@ 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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
extends Node2D
|
||||
|
||||
# Preloaded by path rather than an `is CapturePoint` check - this preview
|
||||
# runs through screenshot.sh, which (unlike run_tests.sh) does not run an
|
||||
# import pass first, so the global class_name cache may not be warm yet.
|
||||
const CAPTURE_POINT_SCRIPT = preload("res://surface/CapturePoint.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.planet_name = "Kepler's Rest"
|
||||
GameState.planet_id = "chunk:3:7"
|
||||
GameState.planet_definition_id = "rocky"
|
||||
GameState.surviving_ship_count = 3
|
||||
GameState.rocks = 128
|
||||
var surface: Node = load("res://surface/PlanetSurface.tscn").instantiate()
|
||||
surface.get_node("SurfaceController").fade_duration = 0.0
|
||||
add_child(surface)
|
||||
|
||||
# Zoomed out so all three ~220-radius capture rings fit the frame with
|
||||
# margin, regardless of wherever the real setup's points happen to sit.
|
||||
surface.get_node("Camera2D").zoom = Vector2(0.55, 0.55)
|
||||
|
||||
var host: Node = surface.get_node("SurfaceFeatures/PlanetSetupHost")
|
||||
var points: Array = []
|
||||
for child in host.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT:
|
||||
points.append(child)
|
||||
|
||||
# Lay the three points out in a compact, clearly-separated triangle for
|
||||
# the screenshot, rather than the real (far-flung) authored positions.
|
||||
# Point 0 sits at the origin - exactly where the fleet spawns - so the
|
||||
# screenshot also proves the #50 z_index fix: the ring must not occlude
|
||||
# the ships parked on top of it.
|
||||
var layout: Array = [Vector2(0, 0), Vector2(-300, 150), Vector2(300, -200)]
|
||||
for i in range(mini(points.size(), layout.size())):
|
||||
points[i].position = layout[i]
|
||||
|
||||
# Show all three states at once: captured, mid-fill, and blocked. Each
|
||||
# point's own _process() would otherwise immediately recompute state
|
||||
# from the real (empty, this far from any ship) fleet/enemy proximity
|
||||
# and overwrite these on the very next frame, so freeze it after setting
|
||||
# the display state - same technique as freezing enemy.speed for the
|
||||
# gas bomber screenshot.
|
||||
if points.size() >= 1:
|
||||
points[0].mark_captured()
|
||||
points[0].set_process(false)
|
||||
GameState.mark_point_captured(GameState.planet_id, points[0].point_index)
|
||||
if points.size() >= 2:
|
||||
points[1].state = CAPTURE_POINT_SCRIPT.State.CAPTURING
|
||||
points[1].progress = 0.55
|
||||
points[1].queue_redraw()
|
||||
points[1].set_process(false)
|
||||
if points.size() >= 3:
|
||||
points[2].state = CAPTURE_POINT_SCRIPT.State.BLOCKED
|
||||
points[2].progress = 0.3
|
||||
points[2].queue_redraw()
|
||||
points[2].set_process(false)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://njieetp3l0xc
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_capture_points_preview.gd" id="1"]
|
||||
|
||||
[node name="CapturePointsPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -5,11 +5,11 @@ func _ready() -> void:
|
|||
var editor: Node = load("res://surface/PlanetSetupEditor.tscn").instantiate()
|
||||
add_child(editor)
|
||||
editor.new_setup("preview_setup", "Preview Setup", "rocky")
|
||||
editor.add_capture_point(Vector2(300, 150))
|
||||
editor.add_capture_point(Vector2(-250, 300))
|
||||
editor.add_enemy("chaser", Vector2(300, 150))
|
||||
editor.add_enemy("chaser", Vector2(-250, 300))
|
||||
editor.add_enemy("chaser", Vector2(200, -300))
|
||||
editor.add_enemy("sniper", Vector2(-300, -150))
|
||||
editor.add_enemy("artillery", Vector2(0, 350))
|
||||
editor.selected_item = {"kind": "capture", "index": 0}
|
||||
editor.selected_item = {"kind": "enemy", "index": 0}
|
||||
editor._rebuild_markers()
|
||||
editor._refresh_selected_label()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ extends SceneTree
|
|||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered. See test_capture_points.gd
|
||||
# can happen before autoloads are registered. See test_planet_garrison.gd
|
||||
# for the full explanation.
|
||||
var ARTILLERY_SHELL_SCRIPT: Script
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ extends SceneTree
|
|||
# C. landing on a black hole spawns no resource provider and no capture
|
||||
# points at all (void's features_scene is null, and it has no
|
||||
# authored PlanetSetup) - yet the planet is immediately conquered
|
||||
# (register_capture_points treats zero required points as vacuous),
|
||||
# (a garrison of zero is vacuously already beaten),
|
||||
# so an extractor is buildable there right away
|
||||
# D. an extractor on a black hole mines dark matter locally exactly like
|
||||
# any other extractor, and a hub transporter banks it through the
|
||||
|
|
@ -132,7 +132,7 @@ func _process(delta: float) -> bool:
|
|||
# resolved at compile time, before autoloads are registered. Same
|
||||
# hazard the header of test_planet_setups.gd documents.
|
||||
if not game_state.is_planet_conquered(game_state.planet_id) \
|
||||
and int(game_state.total_capture_points(game_state.planet_id)) == 0:
|
||||
and int(game_state.garrison_total(game_state.planet_id)) == 0:
|
||||
return _fail("a black hole with no capture points required is not marked conquered")
|
||||
print("no resource provider; %d authored feature host(s)" % feature_host.get_child_count())
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ func _process(delta: float) -> bool:
|
|||
return _fail("landing did not compose BossEncounter")
|
||||
if surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost") != null:
|
||||
return _fail("boss planet was routed through configurable PlanetSetup")
|
||||
if state.total_capture_points(state.planet_id) != 1 \
|
||||
if state.garrison_total(state.planet_id) != 1 \
|
||||
or state.is_planet_conquered(state.planet_id):
|
||||
return _fail("boss planet did not start as one unfinished persistent objective")
|
||||
if surface.get_node("UI/CaptureLabel").visible:
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_capture_points.gd
|
||||
# Verifies:
|
||||
# 1. PlanetSetupHost restores an already-captured point immediately on
|
||||
# (re-)composition, instead of restarting its fill
|
||||
# 2. a ship parked in range with no enemies nearby fills a point to
|
||||
# CAPTURED in about capture_time seconds, and GameState reflects it
|
||||
# (captured_point_count, is_planet_conquered - which is what
|
||||
# BuildController's extractor gate reads, see test_planet_conquest_gate)
|
||||
# 3. an enemy within enemy_clear_radius blocks further progress
|
||||
# (state == BLOCKED) without erasing what was already filled
|
||||
# 4. moving out of range drains progress gradually, not instantly, and
|
||||
# never below what a completed point already holds
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)`. A top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered - and CapturePoint.gd
|
||||
# references GameState in a function body, which GDScript resolves at
|
||||
# compile time. preload()ing it from inside _initialize() defers that
|
||||
# until after the engine has finished booting, same as every plain load()
|
||||
# call elsewhere in this file.
|
||||
var CAPTURE_POINT_SCRIPT: Script
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var point: Node
|
||||
var enemy: Node
|
||||
var planet_id: String = "test_capture_planet"
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var progress_before_block: float = 0.0
|
||||
var progress_before_drain: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
CAPTURE_POINT_SCRIPT = load("res://surface/CapturePoint.gd")
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.planet_capture.clear()
|
||||
|
||||
# --- 1. restoration: an already-captured point stays captured -----
|
||||
var restore_planet_id: String = "test_capture_restore_planet"
|
||||
var fake_setup: Resource = PLANET_SETUP_SCRIPT.new()
|
||||
fake_setup.setup_id = "test_restore"
|
||||
fake_setup.surface_profile_id = "rocky"
|
||||
fake_setup.capture_points = PackedVector2Array([Vector2(0, 0), Vector2(500, 0)])
|
||||
|
||||
game_state.planet_id = restore_planet_id
|
||||
var host1: Node = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
host1.apply_setup(fake_setup)
|
||||
game_state.mark_point_captured(restore_planet_id, 0) # simulates a completed prior visit
|
||||
|
||||
var host2: Node = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
host2.apply_setup(fake_setup)
|
||||
var restored: Node = null
|
||||
var fresh: Node = null
|
||||
for child in host2.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 0:
|
||||
restored = child
|
||||
elif child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 1:
|
||||
fresh = child
|
||||
if restored == null or restored.state != CAPTURE_POINT_SCRIPT.State.CAPTURED or restored.progress < 0.999:
|
||||
return _fail("re-landing did not restore an already-captured point")
|
||||
if fresh == null or fresh.state == CAPTURE_POINT_SCRIPT.State.CAPTURED:
|
||||
return _fail("an uncaptured point came back already CAPTURED")
|
||||
|
||||
# --- 2-4. capture mechanics, in a live scene with a real fleet -----
|
||||
game_state.planet_capture.clear()
|
||||
game_state.planet_id = planet_id
|
||||
game_state.register_capture_points(planet_id, 1)
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.rocks = 200
|
||||
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
if elapsed < 0.1:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
for a in get_nodes_in_group("asteroids"):
|
||||
a.queue_free()
|
||||
|
||||
point = load("res://surface/CapturePoint.tscn").instantiate()
|
||||
point.point_index = 0
|
||||
if point.capture_time > 4.0:
|
||||
return _fail("default capture still takes %.1fs" % point.capture_time)
|
||||
point.capture_time = 1.0
|
||||
point.capture_radius = 200.0
|
||||
point.enemy_clear_radius = 300.0
|
||||
point.drain_rate = 0.5
|
||||
point.position = Vector2(4000, 0) # well clear of spawn-time activity
|
||||
main.add_child(point)
|
||||
|
||||
var far_away: Vector2 = point.position + Vector2(2500, 0)
|
||||
for ship in fleet.ships:
|
||||
ship.position = far_away
|
||||
fleet.move_to(far_away)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# Out of range: no progress at all.
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if point.progress > 0.001 or point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL:
|
||||
return _fail("progress rose while no ship was in capture_radius")
|
||||
|
||||
for ship in fleet.ships:
|
||||
ship.position = point.position
|
||||
fleet.move_to(point.position)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# Partway through capture_time: rising, not yet complete.
|
||||
if elapsed < point.capture_time * 0.4:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURING or point.progress <= 0.05:
|
||||
return _fail("point did not start filling with a ship in range (state=%d progress=%.2f)" % [
|
||||
point.state, point.progress
|
||||
])
|
||||
|
||||
# An enemy shows up within enemy_clear_radius: must block further
|
||||
# fill without erasing what's already there. A bare "enemies"
|
||||
# group member (not a real Enemy.tscn) is deliberate: CapturePoint
|
||||
# only cares that something is in the group and in range, and
|
||||
# this avoids a real chaser/weapon exchange destroying the fleet
|
||||
# ships this test also needs alive in later stages.
|
||||
enemy = Node2D.new()
|
||||
enemy.add_to_group("enemies")
|
||||
enemy.position = point.position + Vector2(50, 0)
|
||||
main.add_child(enemy)
|
||||
progress_before_block = point.progress
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.BLOCKED:
|
||||
return _fail("point did not report BLOCKED with an enemy nearby (state=%d)" % point.state)
|
||||
if point.progress > progress_before_block + 0.01:
|
||||
return _fail("progress rose while an enemy was inside enemy_clear_radius")
|
||||
|
||||
# Enemy leaves and the ship backs off out of range: progress
|
||||
# should drain, but gradually - not snap to zero.
|
||||
enemy.queue_free()
|
||||
var far_away: Vector2 = point.position + Vector2(2500, 0)
|
||||
for ship in fleet.ships:
|
||||
ship.position = far_away
|
||||
fleet.move_to(far_away)
|
||||
progress_before_drain = point.progress
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if point.progress >= progress_before_drain - 0.001:
|
||||
return _fail("progress did not drain while out of range")
|
||||
if point.progress <= 0.001:
|
||||
return _fail("progress dropped to zero instantly instead of draining gradually")
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL:
|
||||
return _fail("point state did not return to NEUTRAL while draining out of range")
|
||||
|
||||
# Move back in and let it finish this time.
|
||||
for ship in fleet.ships:
|
||||
ship.position = point.position
|
||||
fleet.move_to(point.position)
|
||||
elapsed = 0.0
|
||||
stage = 5
|
||||
return false
|
||||
5:
|
||||
# Draining first means completion now takes longer than a bare
|
||||
# capture_time - give it a generous margin.
|
||||
if elapsed < point.capture_time * 2.0 + 0.5:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURED:
|
||||
return _fail("point never reached CAPTURED (state=%d progress=%.2f after %.2fs)" % [
|
||||
point.state, point.progress, elapsed
|
||||
])
|
||||
if game_state.captured_point_count(planet_id) != 1:
|
||||
return _fail("GameState.captured_point_count did not record the capture")
|
||||
# is_planet_conquered is exactly what BuildController's extractor
|
||||
# gate reads (see test_planet_conquest_gate.gd) - not re-tested
|
||||
# through the build UI here to avoid duplicating that coverage.
|
||||
if not game_state.is_planet_conquered(planet_id):
|
||||
return _fail("single-point planet was not marked conquered")
|
||||
if point.z_index >= 0:
|
||||
return _fail("capture point does not draw behind the fleet/enemies (z_index=%d)" % point.z_index)
|
||||
|
||||
print("TEST_CAPTURE_POINTS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_CAPTURE_POINTS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://cmordwxwbr5ca
|
||||
|
|
@ -3,20 +3,22 @@ extends SceneTree
|
|||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_conquered_planet_clear.gd
|
||||
# A planet's garrison was destroyed to take it, so it must not be standing
|
||||
# there again the next time the player lands. Capture points already came
|
||||
# back marked captured; the enemies came back alive, which read as the
|
||||
# conquest having quietly been undone.
|
||||
# there again the next time the player lands.
|
||||
#
|
||||
# #211 made the garrison the whole of conquest: killing the last defender
|
||||
# takes the planet, and there are no capture points beside them any more.
|
||||
#
|
||||
# Covers:
|
||||
# A. an unconquered planet lays out its authored enemies, as before
|
||||
# B. once every capture point is taken, re-landing places no enemies - and
|
||||
# still places the capture points, marked captured
|
||||
# C. a setup that authors enemies but NO capture points keeps them. Such a
|
||||
# planet counts as vacuously conquered (nothing to capture), so keying
|
||||
# only off the conquered flag would delete a garrison nobody has beaten
|
||||
# B. killing the last of them conquers it, and re-landing places none
|
||||
# C. a setup with NO enemies counts as vacuously conquered - there is
|
||||
# nothing there to beat
|
||||
# D. a half-cleared planet the player left comes back whole, and its count
|
||||
# with it, rather than falling to a single kill on the next visit
|
||||
|
||||
const PLANET_KEY: String = "test_conquered_planet_clear"
|
||||
const OPEN_PLANET_KEY: String = "test_conquered_planet_clear_open"
|
||||
const LEFT_PLANET_KEY: String = "test_conquered_planet_clear_left"
|
||||
|
||||
var game_state: Node
|
||||
var host_scene: PackedScene
|
||||
|
|
@ -41,42 +43,54 @@ func _process(delta: float) -> bool:
|
|||
# --- A. unconquered: the garrison is there ---------------------------
|
||||
game_state.planet_id = PLANET_KEY
|
||||
game_state.planet_definition_id = "rocky"
|
||||
var setup: Resource = _make_setup(2, 3)
|
||||
var setup: Resource = _make_setup(3)
|
||||
var enemies: int = _apply_and_count_enemies(setup)
|
||||
if enemies != 3:
|
||||
return _fail("an unconquered planet placed %d of its 3 authored enemies" % enemies)
|
||||
if game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("a planet with 2 uncaptured points reported itself conquered")
|
||||
return _fail("a planet with 3 defenders standing reported itself conquered")
|
||||
print("unconquered planet: 3 enemies placed")
|
||||
|
||||
# --- B. conquered: the garrison stays destroyed ----------------------
|
||||
game_state.mark_point_captured(PLANET_KEY, 0)
|
||||
game_state.mark_point_captured(PLANET_KEY, 1)
|
||||
# --- B. the last kill takes it, and it stays taken -------------------
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
if game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("the planet fell with a defender still standing")
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
if not game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("capturing every point did not conquer the planet")
|
||||
return _fail("killing the whole garrison did not conquer the planet")
|
||||
|
||||
var host: Node = host_scene.instantiate()
|
||||
root.add_child(host)
|
||||
host.apply_setup(setup)
|
||||
var remaining: int = _count_enemies(host)
|
||||
var points: int = _count_capture_points(host)
|
||||
host.free()
|
||||
var remaining: int = _apply_and_count_enemies(setup)
|
||||
if remaining != 0:
|
||||
return _fail("re-landing on a conquered planet placed %d enemies" % remaining)
|
||||
if points != 2:
|
||||
return _fail("a conquered planet placed %d of its 2 capture points - conquest should clear the garrison, not the map" % points)
|
||||
print("conquered planet: 0 enemies, both capture points still placed")
|
||||
print("conquered planet: last kill took it, re-landing places nobody")
|
||||
|
||||
# --- C. a setup with nothing to capture keeps its enemies ------------
|
||||
# --- C. nothing to fight is nothing to take --------------------------
|
||||
game_state.planet_id = OPEN_PLANET_KEY
|
||||
var open_setup: Resource = _make_setup(0, 2)
|
||||
var open_setup: Resource = _make_setup(0)
|
||||
var open_enemies: int = _apply_and_count_enemies(open_setup)
|
||||
if not game_state.is_planet_conquered(OPEN_PLANET_KEY):
|
||||
return _fail("a setup with no capture points was expected to register as vacuously conquered, and did not - so this case no longer tests what it claims to")
|
||||
if open_enemies != 2:
|
||||
return _fail("a setup with no capture points lost %d of its 2 enemies to the conquest rule" \
|
||||
% (2 - open_enemies))
|
||||
print("setup with no capture points: both enemies kept, despite counting as conquered")
|
||||
return _fail("a setup with no enemies was expected to be vacuously conquered")
|
||||
if open_enemies != 0:
|
||||
return _fail("a setup authoring no enemies placed %d" % open_enemies)
|
||||
print("setup with no garrison: vacuously conquered, nobody placed")
|
||||
|
||||
# --- D. leaving mid-fight does not bank the kills --------------------
|
||||
# The garrison is re-placed in full on the next landing, so its count
|
||||
# has to start again with it - otherwise a planet left at one-from-
|
||||
# clear would fall to a single kill against three fresh defenders.
|
||||
game_state.planet_id = LEFT_PLANET_KEY
|
||||
var left_setup: Resource = _make_setup(3)
|
||||
_apply_and_count_enemies(left_setup)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
var returned: int = _apply_and_count_enemies(left_setup)
|
||||
if returned != 3:
|
||||
return _fail("returning to a half-cleared planet placed %d of 3 defenders" % returned)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
if game_state.is_planet_conquered(LEFT_PLANET_KEY):
|
||||
return _fail("one kill took a planet defended by three - the count did not reset with the garrison")
|
||||
print("returning mid-fight: the garrison and its count both start again")
|
||||
|
||||
print("TEST_CONQUERED_PLANET_CLEAR: PASS")
|
||||
quit(0)
|
||||
|
|
@ -84,14 +98,10 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _make_setup(capture_count: int, enemy_count: int) -> Resource:
|
||||
func _make_setup(enemy_count: int) -> Resource:
|
||||
var setup: Resource = load("res://surface/PlanetSetup.gd").new()
|
||||
setup.setup_id = "in_memory"
|
||||
setup.surface_profile_id = "rocky"
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(capture_count):
|
||||
points.append(Vector2(200.0 * float(i + 1), 0.0))
|
||||
setup.capture_points = points
|
||||
var spawn_script: Script = load("res://surface/PlanetSetupSpawn.gd")
|
||||
for i in range(enemy_count):
|
||||
var spawn: Resource = spawn_script.new()
|
||||
|
|
@ -121,16 +131,6 @@ func _count_enemies(host: Node) -> int:
|
|||
return count
|
||||
|
||||
|
||||
func _count_capture_points(host: Node) -> int:
|
||||
var count: int = 0
|
||||
for child in host.get_children():
|
||||
if child.is_in_group("enemies"):
|
||||
continue
|
||||
if "point_index" in child:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_CONQUERED_PLANET_CLEAR: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ func _initialize() -> void:
|
|||
# This test is about the planet-proximity/duplicate rules, not conquest
|
||||
# (see test_planet_conquest_gate), so pre-conquer the target planet.
|
||||
var target_planet_id: String = "chunk:%d:%d" % [target_chunk.x, target_chunk.y]
|
||||
game_state.register_capture_points(target_planet_id, 1)
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_planet_garrison(target_planet_id, 1)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
# Extractors are gated behind research (see test_skill_tree). This test
|
||||
# is about the planet-proximity rule, so grant the research up front.
|
||||
game_state.reset_progression()
|
||||
|
|
|
|||
|
|
@ -408,6 +408,27 @@ func _process(delta: float) -> bool:
|
|||
print("the vortex took %d vessels and left one interceptor" % (convoy_size - 1))
|
||||
if not director.whiteout_teleported:
|
||||
return _fail("the screen reached white without teleporting the fleet")
|
||||
# #209: the prologue's charted space belongs to a place 16000
|
||||
# units away. Carried over, it also completed "chart 30 sectors"
|
||||
# before the player had flown anywhere.
|
||||
#
|
||||
# Not "empty": the tracker charts the neighbourhood around the
|
||||
# fleet the moment it arrives, so what is asserted is that
|
||||
# nothing REMOTE survived - every chunk still on the map is one
|
||||
# the survivors can see from where they now are.
|
||||
# Measured from where the fleet actually IS: the tracker charts a
|
||||
# neighbourhood around the formation's centroid, which sits a
|
||||
# little off the spawn point, and the survivor drifts after
|
||||
# arriving. Two sectors of slack over the sensor's own radius
|
||||
# covers both without admitting anything from the convoy's route,
|
||||
# which is thousands of sectors away.
|
||||
var here: Vector2i = WorldGen.chunk_coords(fleet.get_centroid())
|
||||
var reach: int = 1 + 2
|
||||
for chunk in game_state.explored_chunks:
|
||||
var away: int = maxi(absi(chunk.x - here.x), absi(chunk.y - here.y))
|
||||
if away > reach:
|
||||
return _fail("chunk %s, %d sectors from the survivors, survived the breach" \
|
||||
% [chunk, away])
|
||||
if fleet.get_formation_origin().distance_to(director.DEFAULT_SPAWN) > 1.0:
|
||||
return _fail("whiteout revealed the fleet at %s instead of default spawn %s" \
|
||||
% [fleet.get_formation_origin(), director.DEFAULT_SPAWN])
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ func _process(delta: float) -> bool:
|
|||
setup.setup_id = "reward_probe"
|
||||
setup.display_name = "Reward Probe"
|
||||
setup.surface_profile_id = "rocky"
|
||||
setup.capture_points = PackedVector2Array([Vector2(10, 10), Vector2(-10, -10)])
|
||||
setup.research_point_reward = 5
|
||||
var path: String = "user://test_reward_probe.tres"
|
||||
if ResourceSaver.save(setup, path) != OK:
|
||||
|
|
@ -55,9 +54,9 @@ func _process(delta: float) -> bool:
|
|||
# A research_lab planet's own definition pays a flat 1.
|
||||
var flat_rate: int = int(load("res://planets/ResearchLabPlanetDefinition.tres").conquest_research_points)
|
||||
game_state.research_points = 0
|
||||
game_state.register_capture_points("hard_lab", 2, "research_lab", 6)
|
||||
game_state.mark_point_captured("hard_lab", 0)
|
||||
game_state.mark_point_captured("hard_lab", 1)
|
||||
game_state.register_planet_garrison("hard_lab", 2, "research_lab", 6)
|
||||
game_state.mark_garrison_enemy_destroyed("hard_lab")
|
||||
game_state.mark_garrison_enemy_destroyed("hard_lab")
|
||||
if game_state.research_points != 6:
|
||||
return _fail("a hard lab authored at 6 points awarded %d" % game_state.research_points)
|
||||
if flat_rate >= 6:
|
||||
|
|
@ -66,8 +65,8 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- C. silence still means the type's rate ------------------
|
||||
game_state.research_points = 0
|
||||
game_state.register_capture_points("plain_lab", 1, "research_lab", 0)
|
||||
game_state.mark_point_captured("plain_lab", 0)
|
||||
game_state.register_planet_garrison("plain_lab", 1, "research_lab", 0)
|
||||
game_state.mark_garrison_enemy_destroyed("plain_lab")
|
||||
if game_state.research_points != flat_rate:
|
||||
return _fail("a setup with no authored reward paid %d, expected the type's %d" \
|
||||
% [game_state.research_points, flat_rate])
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ extends SceneTree
|
|||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_planet_conquest_gate.gd
|
||||
# Verifies:
|
||||
# 1. GameState starts every planet unconquered, and register_capture_points
|
||||
# is idempotent (a second call with the same total doesn't wipe progress)
|
||||
# 2. mark_point_captured only conquers once every point is captured, and
|
||||
# the planet_conquered signal fires exactly once
|
||||
# 1. GameState starts every planet unconquered, and re-registering a
|
||||
# garrison on a conquered planet cannot un-conquer it
|
||||
# 2. a planet falls only when its LAST defender does, and the
|
||||
# planet_conquered signal fires exactly once, carrying what it paid
|
||||
# 3. over an unconquered planet, the extractor build option is disabled,
|
||||
# the info popup explains why, and confirming spends nothing and builds
|
||||
# nothing
|
||||
|
|
@ -22,6 +22,9 @@ var target_chunk: Vector2i
|
|||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var conquered_signal_count: int = 0
|
||||
# #212 needs the award to travel WITH the signal, so whatever announces
|
||||
# conquest reports a number that was actually paid.
|
||||
var conquered_signal_points: int = -1
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -57,22 +60,47 @@ func _initialize() -> void:
|
|||
# Unit-level checks on GameState itself, before any scene involvement.
|
||||
if game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("a fresh planet reported conquered")
|
||||
game_state.register_capture_points(target_planet_id, 3)
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_capture_points(target_planet_id, 3) # idempotent re-registration
|
||||
if game_state.captured_point_count(target_planet_id) != 1:
|
||||
return _fail("re-registering wiped already-captured progress")
|
||||
game_state.planet_conquered.connect(func(_id): conquered_signal_count += 1)
|
||||
game_state.mark_point_captured(target_planet_id, 1)
|
||||
game_state.register_planet_garrison(target_planet_id, 3)
|
||||
if game_state.garrison_remaining(target_planet_id) != 3:
|
||||
return _fail("a garrison of 3 registered as %d" \
|
||||
% game_state.garrison_remaining(target_planet_id))
|
||||
game_state.planet_conquered.connect(func(_id, points):
|
||||
conquered_signal_count += 1
|
||||
conquered_signal_points = points)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
if game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("conquered with points still missing")
|
||||
game_state.mark_point_captured(target_planet_id, 2)
|
||||
return _fail("conquered with a defender still standing")
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
if not game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("all points captured but planet not marked conquered")
|
||||
# Marking an already-captured point again must not double-fire the signal.
|
||||
game_state.mark_point_captured(target_planet_id, 2)
|
||||
return _fail("the whole garrison is down but the planet is not conquered")
|
||||
# A stray kill reported after the planet has fallen must not fire again.
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
# Nor may re-landing hand the planet back: registration is refused
|
||||
# outright once it is taken.
|
||||
game_state.register_planet_garrison(target_planet_id, 3)
|
||||
if not game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("re-registering a garrison un-conquered a planet already taken")
|
||||
if conquered_signal_count != 1:
|
||||
return _fail("planet_conquered fired %d times, expected exactly 1" % conquered_signal_count)
|
||||
# This planet was registered with no definition and no override, so the
|
||||
# conquest paid nothing - and the signal has to say so rather than leave
|
||||
# the announcement to guess.
|
||||
if conquered_signal_points != 0:
|
||||
return _fail("an unrewarded conquest reported %d research points" \
|
||||
% conquered_signal_points)
|
||||
|
||||
# A rewarded one reports what it paid.
|
||||
var paid_planet_id: String = target_planet_id + ":paid"
|
||||
var points_before: int = game_state.research_points
|
||||
game_state.register_planet_garrison(paid_planet_id, 1, "", 4)
|
||||
conquered_signal_points = -1
|
||||
game_state.mark_garrison_enemy_destroyed(paid_planet_id)
|
||||
if conquered_signal_points != 4:
|
||||
return _fail("a 4-point conquest reported %d" % conquered_signal_points)
|
||||
if game_state.research_points != points_before + 4:
|
||||
return _fail("the reported points were not the ones actually banked")
|
||||
print("conquest reports the research points it paid")
|
||||
|
||||
# Reset conquest for the scene-level gate check below - this run tests
|
||||
# the unconquered path first, then conquers it.
|
||||
|
|
@ -134,9 +162,9 @@ func _process(delta: float) -> bool:
|
|||
popup.visible = false
|
||||
|
||||
# Now conquer it and confirm the same flow succeeds.
|
||||
game_state.register_capture_points(planet.planet_id, 2)
|
||||
game_state.mark_point_captured(planet.planet_id, 0)
|
||||
game_state.mark_point_captured(planet.planet_id, 1)
|
||||
game_state.register_planet_garrison(planet.planet_id, 2)
|
||||
game_state.mark_garrison_enemy_destroyed(planet.planet_id)
|
||||
game_state.mark_garrison_enemy_destroyed(planet.planet_id)
|
||||
|
||||
controller._on_build_pressed()
|
||||
if not button.enabled:
|
||||
|
|
|
|||
143
tests/test_planet_garrison.gd
Normal file
143
tests/test_planet_garrison.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_planet_garrison.gd
|
||||
# Forgejo #211: taking a planet is killing what defends it. The capture
|
||||
# points this replaced - stand in a ring, wait out a timer, once per ring -
|
||||
# are gone, so what has to be proven is that the FIGHT reaches the conquest
|
||||
# state, through the enemies' own deaths rather than through a bookkeeping
|
||||
# call a test makes on their behalf.
|
||||
#
|
||||
# test_conquered_planet_clear.gd covers the counting rules around landing and
|
||||
# re-landing. This one covers the wiring: a real Enemy, destroyed the way
|
||||
# anything destroys one, moves the planet toward being held.
|
||||
#
|
||||
# Covers:
|
||||
# A. an authored garrison registers itself when the surface is composed
|
||||
# B. killing one defender is progress, not conquest
|
||||
# C. killing the last one conquers the planet, exactly once, and the
|
||||
# research award rides along with the signal
|
||||
# D. an enemy that dies after the planet has already fallen changes nothing
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)`. A top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which can
|
||||
# happen before autoloads are registered - and these scripts reference
|
||||
# GameState in function bodies, which GDScript resolves at compile time.
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
var SPAWN_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var host: Node
|
||||
var planet_id: String = "test_garrison_planet"
|
||||
var conquered_count: int = 0
|
||||
var conquered_points: int = -1
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
SPAWN_SCRIPT = load("res://surface/PlanetSetupSpawn.gd")
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.planet_capture.clear()
|
||||
game_state.planet_id = planet_id
|
||||
game_state.planet_definition_id = "rocky"
|
||||
game_state.planet_conquered.connect(func(_id, points):
|
||||
conquered_count += 1
|
||||
conquered_points = points)
|
||||
|
||||
var setup: Resource = PLANET_SETUP_SCRIPT.new()
|
||||
setup.setup_id = "test_garrison"
|
||||
setup.surface_profile_id = "rocky"
|
||||
# Worth something, so the award travelling with the signal is checked on a
|
||||
# planet that actually pays.
|
||||
setup.research_point_reward = 3
|
||||
for i in range(3):
|
||||
var spawn: Resource = SPAWN_SCRIPT.new()
|
||||
spawn.enemy_id = "chaser"
|
||||
spawn.position = Vector2(0.0, 300.0 * float(i + 1))
|
||||
setup.enemy_spawns.append(spawn)
|
||||
|
||||
host = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
root.add_child(host)
|
||||
host.apply_setup(setup)
|
||||
|
||||
|
||||
func _defenders() -> Array:
|
||||
var out: Array = []
|
||||
for child in host.get_children():
|
||||
if is_instance_valid(child) and child.is_in_group("enemies") \
|
||||
and not child.is_queued_for_deletion():
|
||||
out.append(child)
|
||||
return out
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
if elapsed < 0.2:
|
||||
return false
|
||||
match stage:
|
||||
0:
|
||||
# --- A. the garrison registered itself -----------------------
|
||||
var defenders: Array = _defenders()
|
||||
if defenders.size() != 3:
|
||||
return _fail("the surface placed %d defenders for 3 authored" % defenders.size())
|
||||
if game_state.garrison_total(planet_id) != 3 \
|
||||
or game_state.garrison_remaining(planet_id) != 3:
|
||||
return _fail("a 3-strong garrison registered as %d of %d" % [
|
||||
game_state.garrison_remaining(planet_id),
|
||||
game_state.garrison_total(planet_id)])
|
||||
print("3 defenders placed, and the planet knows it")
|
||||
|
||||
# --- B. one down is not conquest -----------------------------
|
||||
var points_before: int = game_state.research_points
|
||||
defenders[0].take_damage(defenders[0].hp)
|
||||
if game_state.garrison_remaining(planet_id) != 2:
|
||||
return _fail("one kill left %d of the garrison standing, expected 2" \
|
||||
% game_state.garrison_remaining(planet_id))
|
||||
if game_state.is_planet_conquered(planet_id):
|
||||
return _fail("the planet fell to the first kill")
|
||||
if game_state.research_points != points_before:
|
||||
return _fail("an unfinished conquest paid out")
|
||||
print("one defender down: 2 left, planet still theirs")
|
||||
|
||||
# --- C. the last one takes it --------------------------------
|
||||
defenders[1].take_damage(defenders[1].hp)
|
||||
defenders[2].take_damage(defenders[2].hp)
|
||||
if not game_state.is_planet_conquered(planet_id):
|
||||
return _fail("the whole garrison is dead and the planet is not ours")
|
||||
if conquered_count != 1:
|
||||
return _fail("planet_conquered fired %d times, expected once" % conquered_count)
|
||||
if conquered_points != 3:
|
||||
return _fail("the conquest reported %d research points, expected the authored 3" \
|
||||
% conquered_points)
|
||||
if game_state.research_points != points_before + 3:
|
||||
return _fail("the reported award was not the one banked")
|
||||
print("last defender down: planet taken, 3 research points paid once")
|
||||
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# --- D. a straggler after the fact changes nothing -----------
|
||||
# A shot already in flight, or a bomb still blooming, can kill
|
||||
# something a frame after the planet has fallen.
|
||||
var points_after: int = game_state.research_points
|
||||
game_state.mark_garrison_enemy_destroyed(planet_id)
|
||||
if conquered_count != 1:
|
||||
return _fail("a kill after conquest fired planet_conquered again")
|
||||
if game_state.research_points != points_after:
|
||||
return _fail("a kill after conquest paid the reward a second time")
|
||||
print("a late kill is absorbed: no second conquest, no second payment")
|
||||
|
||||
print("TEST_PLANET_GARRISON: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_PLANET_GARRISON: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -145,7 +145,6 @@ func _plant_pool() -> bool:
|
|||
setup.setup_id = pool_id
|
||||
setup.display_name = pool_id
|
||||
setup.surface_profile_id = "rocky"
|
||||
setup.capture_points = PackedVector2Array([Vector2(100.0, 100.0)])
|
||||
if ResourceSaver.save(setup, "res://PlanetSetups/%s.tres" % pool_id) != OK:
|
||||
_remove_scratch()
|
||||
_fail("could not plant the scratch map pool")
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ extends SceneTree
|
|||
# 2. dropping a new .tres into res://PlanetSetups/ makes it appear via
|
||||
# PlanetSetups.rescan() with no other code change
|
||||
# 3. landing on a rocky/gas planet composes a PlanetSetupHost carrying the
|
||||
# resolved setup, with one marker per capture point
|
||||
# resolved setup, placing one enemy per authored spawn
|
||||
|
||||
const SCRATCH_SETUP_PATH: String = "res://PlanetSetups/_test_scratch_setup.tres"
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered, and CapturePoint.gd
|
||||
# references GameState in a function body (resolved at compile time). See
|
||||
# test_capture_points.gd for the full explanation.
|
||||
# can happen before autoloads are registered, and the scripts loaded here
|
||||
# reference GameState in function bodies (resolved at compile time). See
|
||||
# test_planet_garrison.gd for the full explanation.
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
var CAPTURE_POINT_SCRIPT: Script
|
||||
var ENEMY_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var rocky_setup: Resource
|
||||
|
|
@ -30,7 +30,7 @@ var pending_surface: Node
|
|||
|
||||
func _initialize() -> void:
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
CAPTURE_POINT_SCRIPT = load("res://surface/CapturePoint.gd")
|
||||
ENEMY_SCRIPT = load("res://enemies/Enemy.gd")
|
||||
# --- 1. determinism and profile filtering ---
|
||||
rocky_setup = PlanetSetups.setup_for_planet("chunk:3:7", "rocky")
|
||||
if rocky_setup == null:
|
||||
|
|
@ -65,7 +65,6 @@ func _initialize() -> void:
|
|||
scratch.setup_id = "_test_scratch_setup"
|
||||
scratch.display_name = "Scratch"
|
||||
scratch.surface_profile_id = "rocky"
|
||||
scratch.capture_points = PackedVector2Array([Vector2(100, 100)])
|
||||
var save_err: int = ResourceSaver.save(scratch, SCRATCH_SETUP_PATH)
|
||||
if save_err != OK:
|
||||
return _fail("could not save the scratch setup for the pickup check (err %d)" % save_err)
|
||||
|
|
@ -108,14 +107,14 @@ func _process(delta: float) -> bool:
|
|||
if host.setup != rocky_setup:
|
||||
pending_surface.free()
|
||||
return _fail("host did not receive the deterministically resolved setup")
|
||||
var point_count: int = 0
|
||||
var enemy_count: int = 0
|
||||
for child in host.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT:
|
||||
point_count += 1
|
||||
if point_count != rocky_setup.capture_points.size():
|
||||
if child.get_script() == ENEMY_SCRIPT:
|
||||
enemy_count += 1
|
||||
if enemy_count != rocky_setup.enemy_spawns.size():
|
||||
pending_surface.free()
|
||||
return _fail("host placed %d capture points for %d authored ones" % [
|
||||
point_count, rocky_setup.capture_points.size()
|
||||
return _fail("host placed %d enemies for %d authored spawns" % [
|
||||
enemy_count, rocky_setup.enemy_spawns.size()
|
||||
])
|
||||
pending_surface.free()
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ func _initialize() -> void:
|
|||
target_planet_id = "chunk:%d:%d" % [target_chunk.x, target_chunk.y]
|
||||
# This test is about the resource-id gate, not conquest itself (see
|
||||
# test_planet_conquest_gate) - pre-conquer the target directly.
|
||||
game_state.register_capture_points(target_planet_id, 1, "research_lab")
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_planet_garrison(target_planet_id, 1, "research_lab")
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
game_state.reset_progression()
|
||||
for node in GameState.SKILL_TREE:
|
||||
game_state.unlocked.append(node.id)
|
||||
|
|
|
|||
|
|
@ -98,34 +98,34 @@ func _initialize() -> void:
|
|||
if int(lab_definition.conquest_research_points) != 0:
|
||||
return _fail("the research_lab planet type still grants %d point(s) of its own - the reward belongs to the authored map now" \
|
||||
% int(lab_definition.conquest_research_points))
|
||||
game_state.register_capture_points("test_lab_no_reward", 1, "research_lab")
|
||||
game_state.mark_point_captured("test_lab_no_reward", 0)
|
||||
game_state.register_planet_garrison("test_lab_no_reward", 1, "research_lab")
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_no_reward")
|
||||
if game_state.research_points != 0:
|
||||
return _fail("a lab whose map states no reward still awarded %d" % game_state.research_points)
|
||||
print("the planet type grants nothing by itself; an unpriced map awards nothing")
|
||||
|
||||
# What an authored map is worth is passed through as the override, exactly
|
||||
# as PlanetSetupHost does from the setup's research_point_reward.
|
||||
game_state.register_capture_points("test_lab_planet", 2, "research_lab", 1)
|
||||
game_state.register_planet_garrison("test_lab_planet", 2, "research_lab", 1)
|
||||
if game_state.is_planet_conquered("test_lab_planet"):
|
||||
return _fail("a lab with 2 real capture points started conquered")
|
||||
game_state.mark_point_captured("test_lab_planet", 0)
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
|
||||
if game_state.research_points != 0:
|
||||
return _fail("a research point was awarded before every point was captured")
|
||||
game_state.mark_point_captured("test_lab_planet", 1)
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
|
||||
if game_state.research_points != 1:
|
||||
return _fail("conquering a research_lab did not award the 1 point its map is worth: %d" % game_state.research_points)
|
||||
if totals_seen != [1]:
|
||||
return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen])
|
||||
print("conquering a research_lab awarded exactly what its map is worth, once")
|
||||
|
||||
game_state.mark_point_captured("test_lab_planet", 0) # already captured - redundant re-mark
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet") # already beaten - redundant kill
|
||||
if game_state.research_points != 1:
|
||||
return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points)
|
||||
print("double-award guarded against")
|
||||
|
||||
game_state.register_capture_points("test_rocky_planet", 1, "rocky")
|
||||
game_state.mark_point_captured("test_rocky_planet", 0)
|
||||
game_state.register_planet_garrison("test_rocky_planet", 1, "rocky")
|
||||
game_state.mark_garrison_enemy_destroyed("test_rocky_planet")
|
||||
if game_state.research_points != 1:
|
||||
return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points)
|
||||
print("a non-research_lab planet's conquest never awards a point")
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ extends SceneTree
|
|||
# Drives PlanetSetupEditor's model directly (place/move/delete/save/reload)
|
||||
# rather than through touch input - see the ticket's own note that the
|
||||
# touch layer can stay out of the headless test. Verifies:
|
||||
# 1. placing capture points and enemies updates current_setup
|
||||
# 1. placing enemies updates current_setup
|
||||
# 2. moving and deleting update it correctly, including index shifting
|
||||
# 3. saving writes a real res://PlanetSetups/<id>.tres and the registry
|
||||
# picks it up
|
||||
# 4. reopening that file in a fresh editor instance round-trips the
|
||||
# exact points/enemies, and they can be moved/deleted again
|
||||
# 5. the palette has one entry per EnemyDefinitions entry plus the
|
||||
# capture point, generated from the registry rather than hardcoded
|
||||
# enemy kind, generated from the registry rather than hardcoded
|
||||
|
||||
const TEST_SETUP_ID: String = "test_editor_setup"
|
||||
const TEST_SETUP_PATH: String = "res://PlanetSetups/test_editor_setup.tres"
|
||||
|
|
@ -37,32 +37,29 @@ func _process(delta: float) -> bool:
|
|||
if elapsed < 0.1:
|
||||
return false
|
||||
|
||||
var expected_palette_size: int = 1 + EnemyDefinitions.all().size()
|
||||
if editor._palette_buttons.size() != expected_palette_size:
|
||||
return _fail("palette has %d entries, expected 1 + %d enemy definitions" % [
|
||||
# #211 took the capture-point tile out: the palette is exactly the
|
||||
# enemy registry now.
|
||||
if editor._palette_buttons.size() != EnemyDefinitions.all().size():
|
||||
return _fail("palette has %d entries, expected %d enemy definitions" % [
|
||||
editor._palette_buttons.size(), EnemyDefinitions.all().size()
|
||||
])
|
||||
|
||||
editor.new_setup(TEST_SETUP_ID, "Test Editor Setup", "rocky")
|
||||
editor.add_capture_point(Vector2(100, 200))
|
||||
editor.add_capture_point(Vector2(-300, 50))
|
||||
editor.add_capture_point(Vector2(0, -400))
|
||||
editor.add_enemy("chaser", Vector2(100, 200))
|
||||
editor.add_enemy("chaser", Vector2(500, 500))
|
||||
editor.add_enemy("sniper", Vector2(-500, -500))
|
||||
|
||||
if editor.current_setup.capture_points.size() != 3:
|
||||
return _fail("placing 3 capture points did not land 3")
|
||||
if editor.current_setup.enemy_spawns.size() != 3:
|
||||
return _fail("placing 3 enemies did not land 3")
|
||||
|
||||
editor.move_enemy(0, Vector2(150, 250))
|
||||
if editor.current_setup.enemy_spawns[0].position != Vector2(150, 250):
|
||||
return _fail("move_enemy did not update the position")
|
||||
|
||||
editor.remove_enemy(1) # removes the second chaser
|
||||
if editor.current_setup.enemy_spawns.size() != 2:
|
||||
return _fail("placing 2 enemies did not land 2")
|
||||
|
||||
editor.move_capture_point(0, Vector2(150, 250))
|
||||
if editor.current_setup.capture_points[0] != Vector2(150, 250):
|
||||
return _fail("move_capture_point did not update the position")
|
||||
|
||||
editor.remove_enemy(0) # removes the chaser spawn
|
||||
if editor.current_setup.enemy_spawns.size() != 1:
|
||||
return _fail("remove_enemy did not shrink enemy_spawns")
|
||||
if editor.current_setup.enemy_spawns[0].enemy_id != "sniper":
|
||||
if editor.current_setup.enemy_spawns[1].enemy_id != "sniper":
|
||||
return _fail("remove_enemy removed the wrong entry (index did not shift)")
|
||||
|
||||
var result: Dictionary = editor.save_setup()
|
||||
|
|
@ -97,20 +94,19 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
if not editor.load_setup(TEST_SETUP_ID):
|
||||
return _fail("reopening the saved setup failed")
|
||||
var points: PackedVector2Array = editor.current_setup.capture_points
|
||||
var spawns: Array = editor.current_setup.enemy_spawns
|
||||
if points.size() != 3 or points[0] != Vector2(150, 250) \
|
||||
or points[1] != Vector2(-300, 50) or points[2] != Vector2(0, -400):
|
||||
return _fail("reopened capture points did not match what was saved: %s" % [points])
|
||||
if spawns.size() != 1 or spawns[0].enemy_id != "sniper" or spawns[0].position != Vector2(-500, -500):
|
||||
if spawns.size() != 2 or spawns[0].enemy_id != "chaser" \
|
||||
or spawns[0].position != Vector2(150, 250) \
|
||||
or spawns[1].enemy_id != "sniper" \
|
||||
or spawns[1].position != Vector2(-500, -500):
|
||||
return _fail("reopened enemy spawns did not match what was saved")
|
||||
|
||||
# The reopened items can still be moved and deleted.
|
||||
editor.move_enemy(0, Vector2(-600, -600))
|
||||
if editor.current_setup.enemy_spawns[0].position != Vector2(-600, -600):
|
||||
return _fail("could not move an item after reopening")
|
||||
editor.remove_capture_point(1)
|
||||
if editor.current_setup.capture_points.size() != 2:
|
||||
editor.remove_enemy(1)
|
||||
if editor.current_setup.enemy_spawns.size() != 1:
|
||||
return _fail("could not delete an item after reopening")
|
||||
|
||||
print("TEST_SETUP_EDITOR: PASS")
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ func _process(delta: float) -> bool:
|
|||
# Two setups, so deleting one still leaves a populated list - a panel that
|
||||
# empties would hide itself and mask a wrong-row deletion.
|
||||
editor.new_setup(KEEP_ID, "Delete Keeper", "rocky")
|
||||
editor.add_capture_point(Vector2(10, 20))
|
||||
editor.add_enemy("chaser", Vector2(10, 20))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("could not save the keeper setup")
|
||||
editor.new_setup(SCRATCH_ID, "Delete Scratch", "rocky")
|
||||
editor.add_capture_point(Vector2(30, 40))
|
||||
editor.add_enemy("chaser", Vector2(30, 40))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("could not save the scratch setup")
|
||||
|
||||
|
|
@ -123,8 +123,8 @@ func _process(delta: float) -> bool:
|
|||
return _fail("deleting the open setup failed: %s" % outcome.message)
|
||||
if String(editor.current_setup.setup_id) == KEEP_ID:
|
||||
return _fail("the editor is still holding '%s' after deleting it - the next save would put the file straight back" % KEEP_ID)
|
||||
if not editor.current_setup.capture_points.is_empty():
|
||||
return _fail("the replacement setup carried the deleted one's capture points over")
|
||||
if not editor.current_setup.enemy_spawns.is_empty():
|
||||
return _fail("the replacement setup carried the deleted one's enemies over")
|
||||
print("deleting the open setup left a fresh '%s' being edited" % editor.current_setup.setup_id)
|
||||
|
||||
# Deleting something that is not there is refused, not silently "ok".
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ func _process(delta: float) -> bool:
|
|||
scratch.setup_id = SCRATCH_ID
|
||||
scratch.display_name = "Identity Scratch"
|
||||
scratch.surface_profile_id = "rocky"
|
||||
scratch.capture_points = PackedVector2Array([Vector2(10.0, 20.0)])
|
||||
scratch.research_point_reward = 1
|
||||
if ResourceSaver.save(scratch, SCRATCH_FILE) != OK:
|
||||
return _fail("could not write the scratch setup")
|
||||
|
|
@ -116,7 +115,7 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- E. a NEW setup still lands under its id --------------------------
|
||||
editor.new_setup(NEW_ID, "Identity New", "rocky")
|
||||
editor.add_capture_point(Vector2(20.0, 30.0))
|
||||
editor.add_enemy("chaser", Vector2(20.0, 30.0))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("saving a brand-new setup failed")
|
||||
if not FileAccess.file_exists(ProjectSettings.globalize_path(NEW_PATH)):
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ extends SceneTree
|
|||
#
|
||||
# Covers:
|
||||
# A. the gas giant's vent field draws behind the ships (the #91 report)
|
||||
# B. the same for every other drawing feature on every profile, rocks and
|
||||
# capture rings included
|
||||
# B. the same for every other drawing feature on every profile
|
||||
# C. the fleet's own ships are where the comparison says they are, so a
|
||||
# future change that pushed the ships back instead cannot make A and B
|
||||
# pass by moving the goalposts
|
||||
|
|
@ -106,8 +105,14 @@ func _process(delta: float) -> bool:
|
|||
return _fail("ships are at z=%d, not 0 - this test's premise moved" % ship_z)
|
||||
|
||||
var features: Array = _drawing_descendants(surface.get_node("SurfaceFeatures"), [])
|
||||
# A profile is allowed to have no scenery of its own - the research lab
|
||||
# had none but the capture rings, and #211 took those away. The rule is
|
||||
# about features that EXIST, so an empty profile is skipped rather than
|
||||
# failed; the run-wide check below is what stops this test quietly
|
||||
# checking nothing at all.
|
||||
if features.is_empty():
|
||||
return _fail("%s has no drawing surface features to check" % game_state.planet_definition_id)
|
||||
print("%s: no drawing surface features of its own" \
|
||||
% game_state.planet_definition_id)
|
||||
for feature in features:
|
||||
var z: int = _effective_z(feature)
|
||||
if z >= ship_z:
|
||||
|
|
@ -120,6 +125,8 @@ func _process(delta: float) -> bool:
|
|||
checked += features.size()
|
||||
|
||||
if planets.is_empty():
|
||||
if checked == 0:
|
||||
return _fail("no profile had a single drawing feature - this test proved nothing")
|
||||
print("TEST_SURFACE_FEATURE_DEPTH: PASS (%d features checked)" % checked)
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -118,6 +118,38 @@ func _state_cases() -> bool:
|
|||
return _fail("a wave of %d exceeds the %d cap" % [large.size(), GameState.WAVE_MAX_ENEMIES])
|
||||
print("fleet 1 -> %d enemies, fleet 9 -> %d enemies of %d kinds" \
|
||||
% [small.size(), large.size(), heavy_kinds.size()])
|
||||
|
||||
# --- #218: a starting fleet meets one kind of enemy, however long it
|
||||
# takes. Strength alone could not promise this - it counts the wave
|
||||
# NUMBER too, so simply surviving used to unlock lancers and snipers
|
||||
# against a fleet that had grown by nothing.
|
||||
var patient: int = game_state.wave_number
|
||||
for wave in [1, 5, 20, 60]:
|
||||
game_state.wave_number = wave
|
||||
for size in range(1, GameState.BASE_FLEET_SIZE + 1):
|
||||
for id in game_state.wave_composition(size):
|
||||
if id != "chaser":
|
||||
game_state.wave_number = patient
|
||||
return _fail("wave %d against a fleet of %d fields a %s" \
|
||||
% [wave, size, id])
|
||||
game_state.wave_number = patient
|
||||
print("a fleet of %d or fewer meets chasers only, at every wave number" \
|
||||
% GameState.BASE_FLEET_SIZE)
|
||||
|
||||
# And each vessel past the starting three opens exactly one more kind, so
|
||||
# the ramp is progressive rather than a cliff.
|
||||
var previous_kinds: int = 1
|
||||
for size in range(GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 4):
|
||||
var kinds_here: Dictionary = {}
|
||||
for id in game_state.wave_composition(size):
|
||||
kinds_here[id] = true
|
||||
if kinds_here.size() < previous_kinds:
|
||||
return _fail("a fleet of %d meets %d kinds, fewer than the %d before it" \
|
||||
% [size, kinds_here.size(), previous_kinds])
|
||||
previous_kinds = kinds_here.size()
|
||||
if previous_kinds < 3:
|
||||
return _fail("a grown fleet still only meets %d kinds of enemy" % previous_kinds)
|
||||
print("enemy variety climbs with the fleet, reaching %d kinds" % previous_kinds)
|
||||
game_state.reset_waves()
|
||||
return false
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
class_name PaletteEntryButton
|
||||
extends Control
|
||||
|
||||
# One selectable tile in the setup editor's palette: either the generic
|
||||
# capture-point marker, or one enemy definition's own hull/colour. Drawn
|
||||
# directly from the definition rather than a fixed icon set, so a new
|
||||
# EnemyDefinitions entry shows up automatically with no editor code change.
|
||||
# One selectable tile in the setup editor's palette: one enemy definition's
|
||||
# own hull and colour. Drawn directly from the definition rather than a fixed
|
||||
# icon set, so a new EnemyDefinitions entry shows up automatically with no
|
||||
# editor code change. #211 removed the capture-point tile that used to sit
|
||||
# alongside them.
|
||||
|
||||
signal pressed
|
||||
|
||||
@export var is_capture_point: bool = false
|
||||
const ENEMY_DEFINITIONS = preload("res://enemies/EnemyDefinitions.gd")
|
||||
|
||||
@export var enemy_definition: Resource
|
||||
@export var selected: bool = false: set = set_selected
|
||||
|
||||
const RING_COLOR: Color = Color(0.55, 0.85, 1.0)
|
||||
|
||||
|
||||
func set_selected(value: bool) -> void:
|
||||
selected = value
|
||||
queue_redraw()
|
||||
|
|
@ -44,13 +41,7 @@ func _draw() -> void:
|
|||
var frame_color: Color = Color(1.0, 0.95, 0.9) if selected else Color(0.4, 0.45, 0.5)
|
||||
NeonDraw.draw_glowing_polygon(self, frame, frame_color, 0.85 if selected else 0.5)
|
||||
|
||||
if is_capture_point:
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(16):
|
||||
var angle: float = float(i) / 16.0 * TAU
|
||||
points.append(center + Vector2(cos(angle), sin(angle)) * (size.x * 0.28))
|
||||
NeonDraw.draw_glowing_polygon(self, points, RING_COLOR, 0.8)
|
||||
elif enemy_definition != null:
|
||||
if enemy_definition != null:
|
||||
# Same routine as the marker and the enemy, so the button previews
|
||||
# what will actually be placed.
|
||||
var scale_factor: float = (size.x * 0.5) / 28.0 * 0.6
|
||||
|
|
|
|||
|
|
@ -498,6 +498,13 @@ func _complete_whiteout() -> void:
|
|||
return
|
||||
whiteout_teleported = true
|
||||
fleet.teleport_to(DEFAULT_SPAWN)
|
||||
# #209: the convoy charted its way north for the whole prologue, and none
|
||||
# of that map is anywhere near where the breach drops the survivors. Left
|
||||
# standing it is a map of somewhere else, and it also handed the player
|
||||
# "chart 30 sectors" already finished the moment the quest was offered.
|
||||
# After the teleport, so a chunk recorded at the old position on this same
|
||||
# frame goes with it.
|
||||
GameState.explored_chunks.clear()
|
||||
camera.global_position = fleet.get_centroid()
|
||||
camera.reset_physics_interpolation()
|
||||
camera.offset = Vector2.ZERO
|
||||
|
|
|
|||
1
world/OctopusField.gd.uid
Normal file
1
world/OctopusField.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dmnl1gfg50tn7
|
||||
Loading…
Reference in a new issue