feat(@projects/@magic-civilization): update p2 objectives statuses

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-02 21:02:24 -04:00
parent f0ddc1669b
commit ed77f92011
6 changed files with 226 additions and 19 deletions

View file

@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
| **P1** | 43 | 1 | 7 | 0 | 14 | 1 | 66 |
| **P2** | 46 | 1 | 6 | 1 | 7 | 6 | 67 |
| **P2** | 48 | 1 | 5 | 1 | 6 | 6 | 67 |
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
| **total** | **135** | **2** | **13** | **1** | **22** | **26** | **199** |
| **total** | **137** | **2** | **12** | **1** | **21** | **26** | **199** |
</td><td valign='top' style='padding-left:2em'>
@ -29,7 +29,7 @@
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [asset-sprite](../team-leads/asset-sprite.md) | 6 |
| [shipwright](../team-leads/shipwright.md) | 5 |
| [combat-dev](../team-leads/combat-dev.md) | 4 |
| [combat-dev](../team-leads/combat-dev.md) | 2 |
| [terraformer](../team-leads/terraformer.md) | 2 |
| [simulator-infra](../team-leads/simulator-infra.md) | 1 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
@ -206,15 +206,15 @@
| [p2-50](p2-50-rng-determinism-pin.md) | ✅ done | Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-51](p2-51-world-shape-knobs.md) | ✅ done | Player-facing world-shape parameters on new-game screen | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-52](p2-52-substrate-flora-cover-ontology-split.md) | ✅ done | Split terrain enum into substrate × flora-cover layers (resolve biome ontology) | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-53](p2-53-action-vocabulary-design-game-gap.md) | ❌ missing | Action vocabulary — gap analysis between design page and shipped Rust/Godot game | [wireguard](../team-leads/wireguard.md) | 2026-05-01 |
| [p2-53](p2-53-action-vocabulary-design-game-gap.md) | 🟡 partial | Action vocabulary — gap analysis between design page and shipped Rust/Godot game | [wireguard](../team-leads/wireguard.md) | 2026-05-03 |
| [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) | ✅ 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 |
| [p2-53h](p2-53h-cavalry-specifics.md) | 🟡 partial | Cavalry specifics — Charge, Pursue, Wheel | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53g](p2-53g-ranged-specifics.md) | ✅ done | Ranged specifics — Volley, Aimed Shot, Fire Arrows | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 |
| [p2-53h](p2-53h-cavalry-specifics.md) | ✅ done | Cavalry specifics — Charge, Pursue, Wheel | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 |
| [p2-53i](p2-53i-engineer-pioneer-medic-scout.md) | ✅ done | Support specifics — Engineer, Pioneer, Medic, Scout | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
| [p2-54](p2-54-resource-visibility-three-axis.md) | 🔵 in_progress | Resource visibility — three-axis (visibility/yield_gate/improvement_gate) refactor | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-54a](p2-54a-deposits-three-axis-migration.md) | ✅ done | Migrate deposits/*.json to three-axis visibility schema | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |

View file

@ -2,7 +2,7 @@
id: p2-54c
title: Renderer reads observations + indicator decorations for tech-gated resources
priority: p2
status: partial
status: done
scope: game1
owner: terraformer
updated_at: 2026-05-01

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-05-03T00:07:00Z",
"generated_at": "2026-05-03T01:01:16Z",
"totals": {
"in_progress": 2,
"missing": 22,
"stub": 1,
"partial": 12,
"oos": 26,
"partial": 13,
"done": 135,
"in_progress": 2,
"missing": 21,
"stub": 1,
"done": 137,
"total": 199
},
"objectives": [
@ -1624,10 +1624,10 @@
"id": "p2-53",
"title": "Action vocabulary — gap analysis between design page and shipped Rust/Godot game",
"priority": "p2",
"status": "missing",
"status": "partial",
"scope": "game1",
"owner": "wireguard",
"updated_at": "2026-05-01",
"updated_at": "2026-05-03",
"summary": "The design page at `/unit-actions` (`.project/designs/app/src/pages/UnitActions.tsx`) curates an exemplar per unit/building category and lists per-archetype action vocabularies. Cross-checking that vocabulary against the shipped Rust action registry (`mc-core/src/action.rs::ActionKind`), the JSON capability map (`unit_actions.json`), and the Godot panel (`scenes/hud/unit_panel.gd`) reveals four classes of gap. This objective is the gap analysis only — implementation work splits out into child objectives once the design vocabulary is ratified."
},
{
@ -1694,20 +1694,20 @@
"id": "p2-53g",
"title": "Ranged specifics — Volley, Aimed Shot, Fire Arrows",
"priority": "p2",
"status": "partial",
"status": "done",
"scope": "game1",
"owner": "combat-dev",
"updated_at": "2026-05-01",
"updated_at": "2026-05-03",
"summary": "Three new `ActionKind` variants gating on `keywords: [\"ranged\"]`:\n\n- **Volley** — area attack: hits target hex's centre + 2 random edge slots; lower per-target damage, more chances to hit\n- **Aimed Shot** — skip-turn that ignores 50% of target's defence on next attack; sets `aimed_shot_pending = true`\n- **Fire Arrows** — toggle posture; ranged attacks ignite tile (uses Fire system from `mc-ecology` if available); persistent damage, smoke obscures vision"
},
{
"id": "p2-53h",
"title": "Cavalry specifics — Charge, Pursue, Wheel",
"priority": "p2",
"status": "partial",
"status": "done",
"scope": "game1",
"owner": "combat-dev",
"updated_at": "2026-05-01",
"updated_at": "2026-05-03",
"summary": "Three new `ActionKind` variants gating on `keywords: [\"cavalry\"]`:\n\n- **Charge** — move 2+ hexes in straight line, then attack: +30% damage; may push target back; cancellable by Brace (p2-53f)\n- **Pursue** — if target dies/routs, advance into its hex without spending movement (passive trigger; manual confirm if multiple options)\n- **Wheel** — reorient to a new edge slot without leaving the hex; useful to dodge first-strike"
},
{

View file

@ -11,6 +11,8 @@ use mc_climate::{ClimatePhysics, EcologyPhysics, step_atmospheric_chemistry};
use mc_mapgen::{MapGenerator, seed::{derive as derive_seed, SeedDomain}};
use mc_save::{PlayerObservations, TileObservation};
pub mod resources;
/// WASM-exposed grid handle wrapping GridState.
#[wasm_bindgen]
pub struct WasmGrid {
@ -304,6 +306,15 @@ impl WasmGrid {
)
})
}
/// Return resource metadata for a single tile as a JSON string.
/// Includes visibility, yield_gate, and indicator_decorations from the
/// 3-axis schema (p2-54). Returns `null` if the tile has no resource or
/// the coordinates are out of range.
#[wasm_bindgen(js_name = "tileResourceJson")]
pub fn tile_resource_json(&self, col: i32, row: i32) -> Option<String> {
crate::resources::tile_resource_json_impl(&self.inner, col, row)
}
}
/// WASM-exposed climate physics engine.

View file

@ -0,0 +1,154 @@
//! Resource catalog WASM bridge — exposes per-tile resource metadata
//! including the 3-axis visibility schema landed in p2-54.
//!
//! The catalog is baked at compile time from `public/resources/resources.json`
//! and parsed once via `OnceLock` on first access.
use mc_core::grid::GridState;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug, Clone, Deserialize)]
struct CatalogEntry {
id: String,
name: String,
#[serde(default)]
visibility: mc_core::resources::Visibility,
#[serde(default)]
yield_gate: Option<String>,
#[serde(default)]
indicator_decorations: Vec<IndicatorDecorationRaw>,
}
#[derive(Debug, Clone, Deserialize)]
struct IndicatorDecorationRaw {
decoration_id: String,
#[allow(dead_code)]
name: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Deserialize)]
struct ResourcesJson {
#[serde(default)]
bonus: Vec<CatalogEntry>,
#[serde(default)]
luxury: Vec<CatalogEntry>,
#[serde(default)]
strategic: Vec<CatalogEntry>,
}
static CATALOG: OnceLock<HashMap<String, CatalogEntry>> = OnceLock::new();
fn catalog() -> &'static HashMap<String, CatalogEntry> {
CATALOG.get_or_init(|| {
const JSON: &str = include_str!("../../../../public/resources/resources.json");
let bundle: ResourcesJson = serde_json::from_str(JSON).unwrap_or(ResourcesJson {
bonus: vec![],
luxury: vec![],
strategic: vec![],
});
bundle
.bonus
.into_iter()
.chain(bundle.luxury)
.chain(bundle.strategic)
.map(|e| (e.id.clone(), e))
.collect()
})
}
#[derive(Serialize)]
pub struct TileResourceDto {
pub id: String,
pub name: String,
pub visibility: String,
pub yield_gate: Option<String>,
pub indicator_decorations: Vec<String>,
}
pub fn tile_resource_json_impl(grid: &GridState, col: i32, row: i32) -> Option<String> {
let tile = grid.tile(col, row)?;
if tile.resource_id.is_empty() {
return None;
}
let entry = catalog().get(&tile.resource_id)?;
let dto = TileResourceDto {
id: entry.id.clone(),
name: entry.name.clone(),
visibility: visibility_str(&entry.visibility).to_owned(),
yield_gate: entry.yield_gate.clone(),
indicator_decorations: entry
.indicator_decorations
.iter()
.map(|d| d.decoration_id.clone())
.collect(),
};
serde_json::to_string(&dto).ok()
}
fn visibility_str(v: &mc_core::resources::Visibility) -> &'static str {
match v {
mc_core::resources::Visibility::Always => "always",
mc_core::resources::Visibility::Scout => "scout",
mc_core::resources::Visibility::TechGated => "tech_gated",
}
}
#[cfg(test)]
mod tests {
use super::*;
use mc_core::grid::GridState;
fn make_grid_with_resource(resource_id: &str) -> GridState {
let mut g = GridState::new(2, 2);
g.tiles[0].resource_id = resource_id.to_owned();
g
}
#[test]
fn iron_deposit_returns_correct_dto() {
let grid = make_grid_with_resource("iron");
let json = tile_resource_json_impl(&grid, 0, 0).expect("should return Some for iron");
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["id"], "iron");
assert_eq!(val["visibility"], "tech_gated");
}
#[test]
fn always_visible_resource_has_no_decorations() {
let grid = make_grid_with_resource("deer");
let json = tile_resource_json_impl(&grid, 0, 0)
.expect("should return Some for deer");
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["visibility"], "always");
assert_eq!(val["indicator_decorations"].as_array().unwrap().len(), 0);
}
#[test]
fn empty_resource_id_returns_none() {
let grid = make_grid_with_resource("");
assert!(tile_resource_json_impl(&grid, 0, 0).is_none());
}
#[test]
fn out_of_bounds_returns_none() {
let grid = GridState::new(2, 2);
assert!(tile_resource_json_impl(&grid, 99, 99).is_none());
}
#[test]
fn unknown_resource_id_returns_none() {
let grid = make_grid_with_resource("dragon_bones");
assert!(tile_resource_json_impl(&grid, 0, 0).is_none());
}
#[test]
fn catalog_loads_known_entries() {
let cat = catalog();
assert!(cat.contains_key("iron"));
assert!(cat.contains_key("furs"));
}
}

View file

@ -369,6 +369,12 @@ impl TurnProcessor {
// their rally destination (Hold/Fortify/JoinFormation).
Self::apply_rally_arrival_actions(state);
// Phase 5a-pillage: drain GDScript-queued Pillage requests (worker units).
// Runs before bombard so the order is: pillage (worker), bombard (siege),
// volley (ranged AoE), charge (cavalry melee). Each phase has been
// confirmed to leave subsequent phases' inputs valid.
Self::process_pillage_requests(state, &mut result);
// Phase 5a-bombard: drain GDScript-queued Bombard requests (siege units).
Self::process_bombard_requests(state, &mut result);
@ -1975,6 +1981,42 @@ impl TurnProcessor {
///
/// Calls `mc_combat::siege::resolve_bombard` for each request; applies damage
/// to any unit or city on the target hex. The queue is cleared after drain.
/// Drain `GameState::pending_pillage_requests`.
///
/// Each request: validate the worker unit exists; call
/// `GameState::pillage_improvement(col, row)`. Severable improvements stay
/// with `pillaged = true`; non-severable get removed entirely. Negative or
/// out-of-`u16`-range coordinates skip silently. Bad player or unit indices
/// skip silently. The TurnResult counter `improvements_pillaged` records
/// each successful pillage for telemetry.
fn process_pillage_requests(state: &mut GameState, result: &mut TurnResult) {
let requests = std::mem::take(&mut state.pending_pillage_requests);
for req in requests {
let pi = req.player_index as usize;
if pi >= state.players.len() {
continue;
}
if state.players[pi].units.get(req.unit_index).is_none() {
continue;
}
// Reject negative or out-of-u16 coords (tile_improvements is keyed on u16).
let Ok(col) = u16::try_from(req.target_col) else {
continue;
};
let Ok(row) = u16::try_from(req.target_row) else {
continue;
};
// pillage_improvement returns true for severable (marked but kept),
// false for non-severable (removed) or no improvement. Either of the
// first two counts as a successful pillage event.
let had_improvement = state.tile_improvements.contains_key(&(col, row));
state.pillage_improvement(col, row);
if had_improvement {
result.improvements_pillaged = result.improvements_pillaged.saturating_add(1);
}
}
}
fn process_bombard_requests(state: &mut GameState, result: &mut TurnResult) {
use mc_combat::siege::{resolve_bombard, BombardTarget};