feat(objectives): ✨ update priority counts and wireguard tasks
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8828528cc4
commit
f4dacc216d
9 changed files with 187 additions and 42 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue