diff --git a/.project/designs/app/src/pages/UnitActions.tsx b/.project/designs/app/src/pages/UnitActions.tsx index cff73682..61cfbcd7 100644 --- a/.project/designs/app/src/pages/UnitActions.tsx +++ b/.project/designs/app/src/pages/UnitActions.tsx @@ -133,37 +133,37 @@ const STATUS: Readonly> = { "scout:trail-mark": "shipped", // p2-53i: TrailMark tag + 5-turn fog-piercing visibility // ── Buildings · Barracks (p2-53d) ─────────────────────────────── - "barracks:drill": "stubbed-rust", // p2-53d: handler wired; building_panel GDScript signal in-flight (ui-wiring2) - "barracks:auto-promote": "stubbed-rust", // p2-53d: handler wired; building_panel GDScript signal in-flight + "barracks:drill": "shipped", // p2-53d: handler + building_panel.gd _KIND_TO_SIGNAL_BUILDING wired + "barracks:auto-promote": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · Watchtower (p2-53d) ───────────────────────────── - "watchtower:ranged-fire": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "watchtower:alarm": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "watchtower:fire-arrows": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "watchtower:ranged-fire": "shipped", // p2-53d: handler + building_panel.gd wired + "watchtower:alarm": "shipped", // p2-53d: handler + building_panel.gd wired + "watchtower:fire-arrows": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · Wall (p2-53d) ─────────────────────────────────── - "city-wall:repair": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "city-wall:murder-holes": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "city-wall:gate": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "city-wall:repair": "shipped", // p2-53d: handler + building_panel.gd wired + "city-wall:murder-holes": "shipped", // p2-53d: handler + building_panel.gd wired + "city-wall:gate": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · City Centre (p2-53d) ──────────────────────────── - "city-center:raze": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "city-center:annex": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "city-center:raze": "shipped", // p2-53d: handler + building_panel.gd wired + "city-center:annex": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · Workshop (p2-53d) ─────────────────────────────── - "alchemist-workshop:stockpile": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "alchemist-workshop:overdrive": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "alchemist-workshop:research-aid": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "alchemist-workshop:stockpile": "shipped", // p2-53d: handler + building_panel.gd wired + "alchemist-workshop:overdrive": "shipped", // p2-53d: handler + building_panel.gd wired + "alchemist-workshop:research-aid": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · Ancestor Hall (p2-53d) ────────────────────────── - "ancestor-hall:invoke": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "ancestor-hall:inscribe": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "ancestor-hall:invoke": "shipped", // p2-53d: handler + building_panel.gd wired + "ancestor-hall:inscribe": "shipped", // p2-53d: handler + building_panel.gd wired // ── Buildings · Outpost (p2-53d) ──────────────────────────────── - "outpost:pack": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "outpost:supply": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "outpost:claim-tile": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight - "outpost:beacon": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight + "outpost:pack": "shipped", // p2-53d: handler + building_panel.gd wired + "outpost:supply": "shipped", // p2-53d: handler + building_panel.gd wired + "outpost:claim-tile": "shipped", // p2-53d: handler + building_panel.gd wired + "outpost:beacon": "shipped", // p2-53d: handler + building_panel.gd wired // ── Out-of-Game-1 archetypes (caravan, aerial, heroic) ────────── "caravan:route": "design-only-g2", // Caravan trade routes — g6 oos / g2 design diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 323c0bf3..d0a39ff6 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -16,9 +16,9 @@ |---|---|---|---|---|---|---|---| | **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 | | **P1** | 43 | 1 | 7 | 0 | 14 | 1 | 66 | -| **P2** | 45 | 1 | 7 | 1 | 7 | 6 | 67 | +| **P2** | 46 | 1 | 6 | 1 | 7 | 6 | 67 | | **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 | -| **total** | **134** | **2** | **14** | **1** | **22** | **26** | **199** | +| **total** | **135** | **2** | **13** | **1** | **22** | **26** | **199** | @@ -27,8 +27,8 @@ | Team Lead | Remaining | |---|---| | [warcouncil](../team-leads/warcouncil.md) | 6 | -| [shipwright](../team-leads/shipwright.md) | 6 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | +| [shipwright](../team-leads/shipwright.md) | 5 | | [combat-dev](../team-leads/combat-dev.md) | 4 | | [terraformer](../team-leads/terraformer.md) | 2 | | [simulator-infra](../team-leads/simulator-infra.md) | 1 | @@ -210,7 +210,7 @@ | [p2-53a](p2-53a-sentry-guard-action-kind.md) | ✅ done | Sentry/Guard ActionKind — add Sentry/Unsentry to mc-core with wake-on-vision | [wireguard](../team-leads/wireguard.md) | 2026-05-01 | | [p2-53b](p2-53b-building-action-registry.md) | ✅ done | Building action registry — `BuildingActionKind`, `building_actions.json`, `GdBuildingActions` bridge | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-01 | | [p2-53c](p2-53c-rally-vocabulary-expansion.md) | ✅ done | Rally vocabulary expansion — Hold / Fortify / JoinFormation + two-waypoint Patrol | [shipwright](../team-leads/shipwright.md) | 2026-05-01 | -| [p2-53d](p2-53d-building-specifics.md) | 🟡 partial | Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | +| [p2-53d](p2-53d-building-specifics.md) | ✅ done | Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | | [p2-53e](p2-53e-siege-pillage-embark.md) | 🟡 partial | Siege handlers (Pack/Deploy/Bombard) + Pillage UI wiring + Embark/Disembark handlers | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 | | [p2-53f](p2-53f-infantry-specifics.md) | ✅ done | Infantry specifics — Shield Wall, Brace, Shove, Rage, Cleave, War Cry | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 | | [p2-53g](p2-53g-ranged-specifics.md) | 🟡 partial | Ranged specifics — Volley, Aimed Shot, Fire Arrows | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 | diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 7c7eeac6..d800d8f6 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-05-03T00:05:07Z", + "generated_at": "2026-05-03T00:07:00Z", "totals": { + "in_progress": 2, "missing": 22, "stub": 1, "oos": 26, - "done": 134, - "in_progress": 2, - "partial": 14, + "partial": 13, + "done": 135, "total": 199 }, "objectives": [ @@ -1664,7 +1664,7 @@ "id": "p2-53d", "title": "Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions", "priority": "p2", - "status": "partial", + "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-05-03", diff --git a/src/simulator/api-gdext/src/building_action.rs b/src/simulator/api-gdext/src/building_action.rs index 1f88cef8..1610cdf2 100644 --- a/src/simulator/api-gdext/src/building_action.rs +++ b/src/simulator/api-gdext/src/building_action.rs @@ -1,9 +1,11 @@ //! GDExtension bridge for building action capability queries. //! //! Exposes `GdBuildingActions` to GDScript: -//! - `legal_actions_for(building_type, keywords, current_hp, max_hp, is_active, -//! garrison_count, garrison_capacity, has_rally_target)` → `Array[Dictionary]` -//! - `invoke(state, player_idx, city_idx, building_id, kind)` — queued-request mutation +//! - `legal_actions_for(...)` — basic capability query (backwards-compat) +//! - `legal_actions_for_ext(...)` — extended query with archetype toggle state +//! - `get_building_state(state, player_idx, city_idx, building_id)` — Dictionary of runtime state +//! - `invoke(state, player_idx, city_idx, building_id, kind)` — no-unit action +//! - `invoke_with_unit(state, player_idx, city_idx, building_id, kind, unit_id)` — GarrisonIn/Out use godot::prelude::*; use mc_core::building_action::{ @@ -82,11 +84,127 @@ impl GdBuildingActions { availability_to_godot_array(&actions) } + /// Extended legal-actions query that passes archetype-specific toggle state. + /// Preferred over `legal_actions_for` when the caller has a full BuildingState. + /// + /// `state_dict` is a Dictionary with optional bool keys: + /// `"continuous_in_progress"`, `"gate_open"`, `"murder_holes_active"`, + /// `"ranged_fire_active"`, `"fire_arrows_active"`, `"auto_promote"`, + /// `"stockpile_locked"`, `"ancestor_invoked_this_era"`. + /// Missing keys default to false. Callers can pass `get_building_state()` + /// output directly here. + #[func] + pub fn legal_actions_for_ext( + &self, + building_type: GString, + keywords: GString, + current_hp: i64, + max_hp: i64, + is_active: bool, + garrison_count: i64, + garrison_capacity: i64, + has_rally_target: bool, + state_dict: Dictionary, + ) -> Array { + let bool_key = |k: &str| -> bool { + state_dict.get(k).and_then(|v| v.try_to::().ok()).unwrap_or(false) + }; + let cap = BuildingCapability { + building_type: building_type.to_string(), + keywords: keywords + .to_string() + .split_whitespace() + .filter(|s| !s.is_empty()) + .map(String::from) + .collect(), + current_hp: current_hp.max(0) as u32, + max_hp: max_hp.max(0) as u32, + is_active, + garrison_count: garrison_count.max(0) as u32, + garrison_capacity: garrison_capacity.max(0) as u32, + has_rally_target, + continuous_action_in_progress: bool_key("continuous_in_progress"), + gate_open: bool_key("gate_open"), + murder_holes_active: bool_key("murder_holes_active"), + ranged_fire_active: bool_key("ranged_fire_active"), + fire_arrows_active: bool_key("fire_arrows_active"), + auto_promote: bool_key("auto_promote"), + stockpile_locked: bool_key("stockpile_locked"), + ancestor_invoked_this_era: bool_key("ancestor_invoked_this_era"), + }; + let actions = legal_actions_for_building(&cap); + availability_to_godot_array(&actions) + } + + /// Return runtime BuildingState fields as a Dictionary for status rendering. + /// + /// Keys: `"garrisoned_unit_ids"` (Array[int]), `"is_active"` (bool), + /// `"gate_open"` (bool), `"murder_holes_active"` (bool), + /// `"ranged_fire_active"` (bool), `"fire_arrows_active"` (bool), + /// `"sound_alarm_expiry"` (int), `"continuous_turns_remaining"` (int, 0 = none), + /// `"auto_promote"` (bool), `"stockpile_locked"` (bool). + /// + /// Returns an empty Dictionary if no BuildingState exists for this building + /// (meaning all fields are at their defaults). + #[func] + pub fn get_building_state( + &self, + state: Gd, + player_idx: i64, + city_idx: i64, + building_id: GString, + ) -> Dictionary { + let gs = state.bind(); + let pi = player_idx.max(0) as usize; + let ci = city_idx.max(0) as usize; + let bid = building_id.to_string(); + + let mut d = Dictionary::new(); + + let Some(player) = gs.inner.players.get(pi) else { + return d; + }; + + let bs = player.building_states.get(&(ci, bid)); + + // garrisoned_unit_ids — always emit (empty array when no state or no garrison) + let mut ids: Array = Array::new(); + if let Some(bs) = bs { + for &uid in &bs.garrisoned_unit_ids { + ids.push(uid as i64); + } + } + d.set("garrisoned_unit_ids", ids); + + let bs = match bs { + Some(bs) => bs, + None => return d, // defaults are all false/0; caller can use those + }; + + d.set("is_active", bs.is_active); + d.set("gate_open", bs.gate_open); + d.set("murder_holes_active", bs.murder_holes_active); + d.set("ranged_fire_active", bs.ranged_fire_active); + d.set("fire_arrows_active", bs.fire_arrows_active); + d.set("sound_alarm_expiry", bs.sound_alarm_expiry as i64); + d.set("auto_promote", bs.auto_promote); + d.set("stockpile_locked", bs.stockpile_locked); + d.set( + "continuous_turns_remaining", + bs.current_continuous + .as_ref() + .map(|a| a.turns_remaining() as i64) + .unwrap_or(0), + ); + d + } + /// Queue a building action onto `state.pending_building_actions`. /// Drained at the start of the next turn by the turn processor. /// /// `kind` must be a valid `BuildingActionKind::as_str()` value, e.g. `"set_rally"`. /// Unknown kinds are silently dropped (programmer error — log and investigate). + /// For GarrisonIn/Out use `invoke_with_unit` instead. #[func] pub fn invoke( &self, @@ -107,7 +225,34 @@ impl GdBuildingActions { city_idx: city_idx.max(0) as usize, building_id: building_id.to_string(), kind: action_kind, - unit_id: None, // GarrisonIn/Out require unit_id via invoke_with_unit + unit_id: None, + }); + } + + /// Queue GarrisonIn or GarrisonOut with a specific unit ID. + /// `unit_id` is the MapUnit::id of the unit to garrison or extract. + #[func] + pub fn invoke_with_unit( + &self, + state: Gd, + player_idx: i64, + city_idx: i64, + building_id: GString, + kind: GString, + unit_id: i64, + ) { + let kind_str = kind.to_string(); + let Some(action_kind) = BuildingActionKind::from_str(&kind_str) else { + godot_error!("GdBuildingActions::invoke_with_unit: unknown BuildingActionKind {:?}", kind_str); + return; + }; + let mut gs = state.clone(); + gs.bind_mut().inner.pending_building_actions.push(BuildingActionRequest { + player_idx: player_idx.max(0) as usize, + city_idx: city_idx.max(0) as usize, + building_id: building_id.to_string(), + kind: action_kind, + unit_id: Some(unit_id.max(0) as u32), }); } }