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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue