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:
Loïc Wisniewski 2026-09-05 13:31:25 +02:00
commit 6e13b6397c
42 changed files with 540 additions and 749 deletions

View file

@ -1,7 +1,7 @@
extends Node2D
# Authoring tool for PlanetSetup resources (#40/#48): place/move/delete
# capture points and enemy spawns for a chosen surface profile, then save
# enemy spawns for a chosen surface profile, then save
# to res://PlanetSetups/<id>.tres so the game picks it up the same way it
# picks up every other authored setup. Deliberately does not touch
# GameState - this tool only edits committed resources, never gameplay
@ -38,7 +38,6 @@ const ORIGIN_COLOR: Color = Color(1.0, 0.9, 0.4)
# cross-class - a global class_name lookup at parse time needs the class
# cache already warm (see #42/#44's load-order notes), which a plain
# local const sidesteps entirely.
const CAPTURE_MARKER_COLOR: Color = Color(0.55, 0.85, 1.0)
const PALETTE_STEP: float = 92.0
@onready var camera: Camera2D = $Camera2D
@ -68,7 +67,11 @@ var current_setup: Resource
# saved. Saving writes HERE rather than to a path derived from setup_id - see
# PlanetSetups.path_for_id for why deriving it forks a duplicate.
var current_setup_path: String = ""
var selected_palette: String = "capture_point"
# #211 took the capture points out of the palette, so the opening tool is the
# first enemy kind. Resolved in _build_palette() rather than hardcoded here,
# so removing an enemy definition cannot leave the editor pointing at a tool
# that no longer exists.
var selected_palette: String = ""
var selected_item: Dictionary = {}
var _profile_ids: Array = []
var _profile_cursor: int = 0
@ -130,7 +133,7 @@ func new_setup(setup_id: String, display_name: String, profile_id: String) -> vo
current_setup.setup_id = setup_id
current_setup.display_name = display_name
current_setup.surface_profile_id = profile_id
# capture_points/enemy_spawns are left at PlanetSetup's own declared
# enemy_spawns is left at PlanetSetup's own declared
# defaults (already empty) rather than reassigned here - assigning a
# plain `[]` to enemy_spawns from outside PlanetSetup.gd's own file
# fails at runtime, since the compiler here only sees current_setup as
@ -220,34 +223,6 @@ func delete_setup(setup_id: String) -> Dictionary:
return {"ok": true, "message": "Deleted '%s'." % setup_id}
# --- model: capture points ---------------------------------------------
func add_capture_point(at: Vector2) -> int:
if current_setup == null:
return -1
var points: PackedVector2Array = current_setup.capture_points
points.append(at)
current_setup.capture_points = points
_rebuild_markers()
return points.size() - 1
func move_capture_point(index: int, at: Vector2) -> void:
if current_setup == null or index < 0 or index >= current_setup.capture_points.size():
return
current_setup.capture_points[index] = at
_rebuild_markers()
func remove_capture_point(index: int) -> void:
if current_setup == null or index < 0 or index >= current_setup.capture_points.size():
return
var points: PackedVector2Array = current_setup.capture_points
points.remove_at(index)
current_setup.capture_points = points
_rebuild_markers()
# --- model: enemy spawns ------------------------------------------------
func add_enemy(enemy_id: String, at: Vector2) -> int:
@ -300,24 +275,10 @@ func _rebuild_markers() -> void:
child.queue_free()
if current_setup == null:
return
for i in range(current_setup.capture_points.size()):
_add_capture_marker(i, current_setup.capture_points[i])
for i in range(current_setup.enemy_spawns.size()):
_add_enemy_marker(i, current_setup.enemy_spawns[i])
func _add_capture_marker(index: int, at: Vector2) -> void:
var is_selected: bool = selected_item.get("kind", "") == "capture" and selected_item.get("index", -1) == index
var color: Color = Color(1.0, 0.95, 0.9) if is_selected else CAPTURE_MARKER_COLOR
var marker: NeonPiece = NeonPiece.create(item_host, at, color)
var points: PackedVector2Array = PackedVector2Array()
for i in range(16):
var angle: float = float(i) / 16.0 * TAU
points.append(Vector2(cos(angle), sin(angle)) * 60.0)
marker.add_polygon(points, 0.6)
marker.queue_redraw()
func _add_enemy_marker(index: int, spawn: Resource) -> void:
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
var is_selected: bool = selected_item.get("kind", "") == "enemy" and selected_item.get("index", -1) == index
@ -335,16 +296,22 @@ func _add_enemy_marker(index: int, spawn: Resource) -> void:
# --- palette ---------------------------------------------------------------
func _build_palette() -> void:
var entries: Array = [{"id": "capture_point", "is_point": true, "definition": null}]
var entries: Array = []
for definition in ENEMY_DEFINITIONS.all():
entries.append({"id": definition.enemy_id, "is_point": false, "definition": definition})
entries.append({"id": definition.enemy_id, "definition": definition})
if entries.is_empty():
return
# The palette opens on its first tool. Before #211 that was the capture
# point, which was always present; an enemy list could in principle be
# reordered, so the default is read off the list itself.
if selected_palette == "" or not entries.any(func(e): return e.id == selected_palette):
selected_palette = String(entries[0].id)
var x: float = 0.0
for entry in entries:
var button: Control = PALETTE_ENTRY_BUTTON_SCRIPT.new()
button.position = Vector2(x, 0)
button.size = Vector2(84, 84)
button.is_capture_point = entry.is_point
button.enemy_definition = entry.definition
button.selected = entry.id == selected_palette
button.pressed.connect(_on_palette_pressed.bind(entry.id))
@ -360,8 +327,7 @@ func _on_palette_pressed(id: String) -> void:
func _refresh_palette_selection() -> void:
for button in _palette_buttons:
var entry_id: String = "capture_point" if button.is_capture_point else button.enemy_definition.enemy_id
button.selected = entry_id == selected_palette
button.selected = String(button.enemy_definition.enemy_id) == selected_palette
# --- UI handlers -------------------------------------------------------
@ -468,10 +434,7 @@ func _on_delete_pressed() -> void:
if selected_item.is_empty():
_set_status("Nothing selected to delete.")
return
if selected_item.kind == "capture":
remove_capture_point(selected_item.index)
else:
remove_enemy(selected_item.index)
remove_enemy(selected_item.index)
selected_item = {}
_refresh_selected_label()
@ -506,21 +469,15 @@ func _refresh_selected_label() -> void:
selected_label.text = ""
return
var index: int = int(selected_item.index)
if selected_item.kind == "capture":
selected_label.text = "Selected: Capture Point #%d" % (index + 1)
else:
var spawn: Resource = current_setup.enemy_spawns[index]
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
selected_label.text = "Selected: %s #%d" % [definition.display_name, index + 1]
var spawn: Resource = current_setup.enemy_spawns[index]
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
selected_label.text = "Selected: %s #%d" % [definition.display_name, index + 1]
# --- placement/selection helpers ----------------------------------------
func _place_selected(world_pos: Vector2) -> void:
if selected_palette == "capture_point":
add_capture_point(world_pos)
else:
add_enemy(selected_palette, world_pos)
add_enemy(selected_palette, world_pos)
func _find_marker_near(world_pos: Vector2) -> Dictionary:
@ -528,11 +485,6 @@ func _find_marker_near(world_pos: Vector2) -> Dictionary:
return {}
var best: Dictionary = {}
var best_dist: float = HIT_RADIUS
for i in range(current_setup.capture_points.size()):
var d: float = world_pos.distance_to(current_setup.capture_points[i])
if d <= best_dist:
best_dist = d
best = {"kind": "capture", "index": i}
for i in range(current_setup.enemy_spawns.size()):
var d: float = world_pos.distance_to(current_setup.enemy_spawns[i].position)
if d <= best_dist:
@ -544,10 +496,7 @@ func _find_marker_near(world_pos: Vector2) -> Dictionary:
func _move_selected(world_pos: Vector2) -> void:
if dragging_item.is_empty():
return
if dragging_item.kind == "capture":
move_capture_point(dragging_item.index, world_pos)
else:
move_enemy(dragging_item.index, world_pos)
move_enemy(dragging_item.index, world_pos)
func _screen_to_world(screen_pos: Vector2) -> Vector2: