feat(objectives): update priority counts and wireguard tasks

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 04:53:38 -07:00
parent 8828528cc4
commit f4dacc216d
9 changed files with 187 additions and 42 deletions

View file

@ -15,10 +15,10 @@
| Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total |
|---|---|---|---|---|---|---|
| **P0** | 27 | 5 | 3 | 0 | 0 | 35 |
| **P1** | 14 | 3 | 3 | 0 | 1 | 21 |
| **P1** | 14 | 3 | 4 | 0 | 1 | 22 |
| **P2** | 9 | 6 | 0 | 12 | 0 | 27 |
| **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 17 |
| **total** | **50** | **14** | **6** | **12** | **18** | **100** |
| **total** | **50** | **14** | **7** | **12** | **18** | **101** |
</td><td valign='top' style='padding-left:2em'>
@ -28,8 +28,8 @@
|---|---|
| [asset-sprite](../team-leads/asset-sprite.md) | 7 |
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [wireguard](../team-leads/wireguard.md) | 6 |
| [tourguide](../team-leads/tourguide.md) | 6 |
| [wireguard](../team-leads/wireguard.md) | 5 |
| [shipwright](../team-leads/shipwright.md) | 2 |
| [testwright](../team-leads/testwright.md) | 2 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
@ -100,7 +100,7 @@
| [p1-18](p1-18-village-discovery-feedback.md) | 🔴 stub | Village discovery — world-map feedback (notification, reward popup, minimap ping) | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
| [p1-19](p1-19-tutorial-opt-in.md) | 🔴 stub | Tutorial opt-in — HUD button, disappears after turn 5, starts from Step 1 | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
| [p1-20](p1-20-unit-action-capability-registry.md) | 🔴 stub | Unit action capability registry — one source of truth for "what can this unit do right now?" | [wireguard](../team-leads/wireguard.md) | 2026-04-18 |
| [p1-21](p1-21-unit-patrol-orders.md) | 🔴 stub | Unit patrol orders — standing order to loop between waypoint tiles (depends on p1-20) | [wireguard](../team-leads/wireguard.md) | 2026-04-18 |
| [p1-21](p1-21-unit-patrol-orders.md) | 🔴 stub | Unit patrol orders — standing order to loop between waypoint tiles | [wireguard](../team-leads/wireguard.md) | 2026-04-18 |
## P2 — Polish

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-04-18T09:15:28Z",
"generated_at": "2026-04-18T11:51:34Z",
"totals": {
"stub": 6,
"done": 50,
"oos": 18,
"missing": 12,
"stub": 7,
"oos": 18,
"partial": 14,
"total": 100
"done": 50,
"total": 101
},
"objectives": [
{
@ -561,13 +561,23 @@
},
{
"id": "p1-20",
"title": "Unit patrol orders — assign a unit to loop between waypoint tiles",
"title": "Unit action capability registry — one source of truth for \"what can this unit do right now?\"",
"priority": "p1",
"status": "stub",
"scope": "game1",
"owner": "wireguard",
"updated_at": "2026-04-18",
"summary": "Both the human player and the AI clans need a *standing order* that keeps a\nunit moving along a fixed route turn after turn without per-turn micro-management.\nCanonical use cases: escorting a worker loop, covering a chokepoint, sweeping\nscout fog between two outposts.\n\nToday a unit has two durable states: idle-on-tile, or fortified (via\n`unit.gd:250`). `Skip` ends the turn but does not persist. A player who wants\na scout to pace between two tiles must hand-move it every single turn — which\nbreaks down entirely once the empire has more than a few units, and which the\nAI cannot express at all because `mc-ai/tactical/movement.rs` re-plans from\nscratch each turn.\n\nThis objective adds a third durable state — **patrol** — with a small\nwaypoint list and a direction cursor. While patrolling, the unit auto-advances\nalong its route during the turn processor before the player's input phase, so\nturn N+1 opens with the unit already at the next step on its loop."
"summary": "The game has no unified answer to *\"what actions can unit U take on turn T in\nstate S?\"* Today the unit panel (`unit_panel.gd:19-40`) hardcodes three\nbuttons — Fortify, Skip, Found City — and decides visibility with bespoke\nper-unit booleans scattered across the JSON (`can_found_city`,\n`can_build_improvements`, `flags: [\"ranged\"]`) and ad-hoc GDScript predicates\n(`is_civilian()`). Meanwhile `mc-ai/src/tactical/movement.rs` enumerates\nmoves and attacks but has no registry for non-motion actions. UI and AI have\nno shared truth.\n\nEvery future action — patrol (p1-21), siege pack/deploy, pillage, embark,\nbuild-road, heal, upgrade — compounds that debt by adding another hardcoded\nbutton plus its own scattered check. A siege engine in `packed` state can\nmove but not bombard; in `deployed` state can bombard but not move. Patrol\nhas the same shape (idle ↔ patrolling, with auto-cancel). Fortify has the\nsame shape. Without a registry, each state gate becomes a new bespoke flag.\n\nThis objective lands the foundation: a JSON-driven capability declaration,\na Rust `ActionKind` enum with a single `legal_actions(unit, state)` query,\nand a unit-panel refactor that renders buttons from that list. **Behavior\ndoes not change** — the three existing actions are folded in with no\nsemantic change. The payoff is every subsequent action objective (patrol,\nsiege, pillage, embark, ...) ships as one enum variant + one JSON keyword\nmapping + one handler, with no UI or AI scaffolding to re-invent."
},
{
"id": "p1-21",
"title": "Unit patrol orders — standing order to loop between waypoint tiles",
"priority": "p1",
"status": "stub",
"scope": "game1",
"owner": "wireguard",
"updated_at": "2026-04-18",
"summary": "Both the human player and the AI clans need a *standing order* that keeps a\nunit moving along a fixed route turn after turn without per-turn micro-\nmanagement. Canonical use cases: escorting a worker loop, covering a\nchokepoint, sweeping scout fog between two outposts.\n\nToday a unit has two durable states: idle-on-tile, or fortified. `Skip`\nends the turn but does not persist. A player who wants a scout to pace\nbetween two tiles must hand-move it every single turn — which breaks down\nonce the empire has more than a few units, and which the AI cannot express\nat all because `mc-ai/tactical/movement.rs` re-plans from scratch each turn.\n\nThis objective adds a third durable state — **patrol** — with a small\nwaypoint list, a direction cursor, and a loop mode. While patrolling, the\nunit auto-advances along its route during the turn processor before the\nplayer's input phase, so turn N+1 opens with the unit already at the next\nstep on its loop.\n\n**This objective assumes p1-20 (unit action capability registry) has\nshipped.** Patrol plugs into the registry as one new `ActionKind` variant\nplus its handlers — no bespoke unit-panel buttons, no scattered\n`is_patrolling` checks in GDScript. If p1-20 slips, reassess whether to\nland a narrower patrol-only version first."
},
{
"id": "p2-01",

View file

@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Episode systems manifest",
"type": "object",
"required": ["systems"],
"additionalProperties": false,
"properties": {
"systems": {
"type": "array",
"minItems": 1,
"items": { "type": "string", "minLength": 1 }
}
}
}

View file

@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Homepage feature card",
"type": "object",
"required": ["title", "desc"],
"additionalProperties": false,
"properties": {
"title": { "type": "string", "minLength": 1 },
"desc": { "type": "string", "minLength": 1 },
"min_episode": { "type": "integer", "minimum": 1 }
}
}

View file

@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Map topology mode",
"type": "object",
"required": ["name", "desc"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "minLength": 1 },
"desc": { "type": "string", "minLength": 1 },
"math": { "type": "string" },
"is_default": { "type": "boolean" }
}
}

View file

@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Shipping roadmap",
"type": "object",
"required": ["coming_in_v1", "after_full_release"],
"additionalProperties": false,
"properties": {
"coming_in_v1": {
"type": "array",
"items": {
"type": "object",
"required": ["priority", "system", "description"],
"additionalProperties": false,
"properties": {
"priority": { "type": "integer", "minimum": 0 },
"system": { "type": "string", "minLength": 1 },
"description": { "type": "string", "minLength": 1 }
}
}
},
"after_full_release": {
"type": "array",
"items": {
"type": "object",
"required": ["version", "systems"],
"additionalProperties": false,
"properties": {
"version": { "type": "string", "minLength": 1 },
"systems": { "type": "string", "minLength": 1 }
}
}
}
}
}

View file

@ -7,8 +7,6 @@ extends PanelContainer
## to the actual handlers — the panel itself never mutates the simulation
## directly. Disabled buttons keep a tooltip explaining WHY they cannot fire.
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
## Action signals consumed by world_map.gd. Disconnected from the slim HUD
## panel that p0-33 retired.
signal move_pressed
@ -17,6 +15,8 @@ signal skip_pressed
signal found_city_pressed
signal build_improvement_pressed
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
## Disabled-button outline color (matches the action-required tutorial badge).
const DISABLED_OUTLINE_COLOR: Color = Color(0.95, 0.35, 0.35, 0.85)
@ -109,9 +109,11 @@ func _refresh_display() -> void:
var move_remaining: int = _get_movement_remaining(_selected_unit)
var move_total: int = _get_movement_total(_selected_unit)
_attack_label.text = ThemeVocabulary.lookup("fmt_key_int") % [ThemeVocabulary.lookup("attack"), attack]
_defense_label.text = ThemeVocabulary.lookup("fmt_key_int") % [ThemeVocabulary.lookup("defense"), defense]
_hp_label.text = ThemeVocabulary.lookup("fmt_current_of_max") % [ThemeVocabulary.lookup("hit_points"), hp, max_hp]
var key_fmt: String = ThemeVocabulary.lookup("fmt_key_int")
var pair_fmt: String = ThemeVocabulary.lookup("fmt_current_of_max")
_attack_label.text = key_fmt % [ThemeVocabulary.lookup("attack"), attack]
_defense_label.text = key_fmt % [ThemeVocabulary.lookup("defense"), defense]
_hp_label.text = pair_fmt % [ThemeVocabulary.lookup("hit_points"), hp, max_hp]
_movement_label.text = ThemeVocabulary.lookup("fmt_current_of_max") % [
ThemeVocabulary.lookup("movement"), move_remaining, move_total,
]

View file

@ -75,8 +75,12 @@ var _last_hover_axial: Vector2i = Vector2i(-9999, -9999)
@onready var _viewport_manager: Control = $ViewportWindowManager
@onready var _hud: CanvasLayer = $WorldMapHud
@onready var _tech_tree: CanvasLayer = $TechTree
@onready var _minimap: Control = $MinimapLayer/Minimap if has_node("MinimapLayer/Minimap") else null
@onready var _unit_panel: Node = $UnitPanelLayer/UnitPanel if has_node("UnitPanelLayer/UnitPanel") else null
@onready var _minimap: Control = (
$MinimapLayer/Minimap if has_node("MinimapLayer/Minimap") else null
)
@onready var _unit_panel: Node = (
$UnitPanelLayer/UnitPanel if has_node("UnitPanelLayer/UnitPanel") else null
)
func _ready() -> void:
@ -529,42 +533,44 @@ func _is_prologue_active() -> bool:
func _handle_hotkeys(key_event: InputEventKey) -> bool:
var handled: bool = false
match key_event.keycode:
KEY_T:
_toggle_tech_tree()
get_viewport().set_input_as_handled()
return true
handled = true
KEY_C:
_toggle_chronicle()
get_viewport().set_input_as_handled()
return true
handled = true
KEY_B:
if _selected_unit != null:
_on_build_improvement_pressed()
get_viewport().set_input_as_handled()
return true
handled = true
KEY_M:
## p0-35 enter movement mode when a unit is selected and has MP.
if _selected_unit != null and _selected_unit_has_movement():
_enter_movement_mode()
get_viewport().set_input_as_handled()
return true
handled = true
KEY_ESCAPE:
## p0-35: ESC cancels movement mode FIRST (before bubbling to
## panels or the in-game menu owned by main.gd).
if _movement_mode:
_exit_movement_mode()
get_viewport().set_input_as_handled()
return true
if _chronicle_panel != null and _chronicle_panel.visible:
_chronicle_panel.hide()
EventBus.chronicle_closed.emit(GameState.current_player_index)
get_viewport().set_input_as_handled()
return true
if _tech_tree.visible:
_tech_tree.close()
get_viewport().set_input_as_handled()
return true
handled = _handle_escape_key()
if handled:
get_viewport().set_input_as_handled()
return handled
## p0-35: ESC dispatch — cancels movement mode FIRST, then closes the
## topmost open panel (chronicle, tech tree). Falls through to false so
## main.gd can open the in-game menu when nothing here consumed it.
func _handle_escape_key() -> bool:
if _movement_mode:
_exit_movement_mode()
return true
if _chronicle_panel != null and _chronicle_panel.visible:
_chronicle_panel.hide()
EventBus.chronicle_closed.emit(GameState.current_player_index)
return true
if _tech_tree.visible:
_tech_tree.close()
return true
return false
@ -958,7 +964,9 @@ func update_path_preview(hovered_axial: Vector2i) -> void:
var scouted: Dictionary = _build_scouted_dict()
## Use a generous budget so multi-turn previews can be displayed; the
## turn count is computed below from the per-tile cost dictionary.
var movement_total: int = int(_selected_unit.get_movement()) if _selected_unit.has_method("get_movement") else 2
var movement_total: int = 2
if _selected_unit.has_method("get_movement"):
movement_total = int(_selected_unit.get_movement())
var preview_budget: int = maxi(movement_total * 8, 16)
var path: Array[Vector2i] = PathfinderScript.find_path_with_fog(
game_map,

View file

@ -291,6 +291,57 @@ class GameDataValidator:
else:
self._ok(label)
def validate_guide_data(self):
"""Validate the four guide-consumed JSON files extracted from hardcoded
page enums (p2-32). Each has a minimal schema in data/schemas/."""
print("\n guide-data enums")
# homepage-features.json: {"features": [card, ...]}
schema = self._load_schema("homepage-features")
if schema is not None:
path = self.game_data / "homepage-features.json"
data, err = load_json_safe(path)
if err:
self._fail("homepage-features.json", f"parse error: {err}")
else:
rel = path.relative_to(self.root)
for i, card in enumerate(data.get("features", [])):
self._validate_entry(schema, card, f"{rel}[features][{i}]")
# map-topologies.json: {"topologies": [topology, ...]}
schema = self._load_schema("map-topology")
if schema is not None:
path = self.game_data / "map-topologies.json"
data, err = load_json_safe(path)
if err:
self._fail("map-topologies.json", f"parse error: {err}")
else:
rel = path.relative_to(self.root)
for i, topo in enumerate(data.get("topologies", [])):
self._validate_entry(schema, topo, f"{rel}[topologies][{i}]")
# episodes/ep1-systems.json: whole-file wrapper validation
schema = self._load_schema("episode-systems")
if schema is not None:
path = self.game_data / "episodes" / "ep1-systems.json"
data, err = load_json_safe(path)
if err:
self._fail("episodes/ep1-systems.json", f"parse error: {err}")
else:
rel = path.relative_to(self.root)
self._validate_entry(schema, data, str(rel))
# shipping-roadmap.json: whole-file wrapper validation
schema = self._load_schema("shipping-roadmap")
if schema is not None:
path = self.game_data / "shipping-roadmap.json"
data, err = load_json_safe(path)
if err:
self._fail("shipping-roadmap.json", f"parse error: {err}")
else:
rel = path.relative_to(self.root)
self._validate_entry(schema, data, str(rel))
def validate_cross_refs(self):
"""Cross-reference checks: collectibles → resources, gates_* → units/buildings."""
resources = self._load_resources()
@ -354,6 +405,7 @@ class GameDataValidator:
self.validate_improvements()
self.validate_biomes()
self.validate_deposit_concept_refs()
self.validate_guide_data()
self.validate_cross_refs()
def report(self) -> int: