feat(@projects/@magic-civilization): ⛵ p3-18 P4a — transport data model (keywords + carrier_id)
Foundation for the transport mechanic (owner: build it):
- catalog UnitStats gains `keywords: Vec<String>` (authored on units/<id>.json),
+ `is_transport()` (keywords.contains("transport")) and a TRANSPORT_CAPACITY=2
constant mirroring the combat.json transport keyword ("carry up to 2 land
units"). Data-driven — no unit id hardcoded.
- MapUnit gains `carrier_id: Option<u32>` — the hull carrying this land unit
(None = on map normally). A carried unit rides the carrier's hex, isn't
independently attackable, and is lost with the carrier.
Test: transport_keyword_detected_data_driven; mc-units lib green. Mechanic
(board/carry/unload + combat-loss) lands in P4b/P4c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
416c62da85
commit
ba81bcf476
3 changed files with 49 additions and 1 deletions
|
|
@ -1170,6 +1170,14 @@ pub struct MapUnit {
|
|||
/// embark action.
|
||||
#[serde(default)]
|
||||
pub is_embarked: bool,
|
||||
/// p3-18 transport — the `id` of the transport hull currently carrying this
|
||||
/// land unit, or `None` when it is on the map normally. A carried unit rides
|
||||
/// the transport's hex (its position mirrors the carrier), is not
|
||||
/// independently attackable, and is lost if the carrier is destroyed. Set by
|
||||
/// boarding a transport (move onto its hex), cleared by disembarking (move to
|
||||
/// adjacent land). See `mc-turn::process_one_move`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub carrier_id: Option<u32>,
|
||||
/// Rally command to execute when this unit first reaches its rally hex.
|
||||
/// Set at spawn; cleared by `apply_rally_arrival_actions` after firing once.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -79,8 +79,29 @@ pub struct UnitStats {
|
|||
/// come online.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub logistics: Option<UnitLogistics>,
|
||||
/// Gameplay keywords authored on `units/<id>.json` (e.g. `"transport"`,
|
||||
/// `"amphibious"`, `"ranged"`). Surfaced so the runtime can gate
|
||||
/// keyword-driven mechanics (p3-18 transport: `keywords.contains("transport")`
|
||||
/// marks a hull that can carry land units) without hardcoding unit ids.
|
||||
#[serde(default)]
|
||||
pub keywords: Vec<String>,
|
||||
}
|
||||
|
||||
impl UnitStats {
|
||||
/// p3-18 — whether this unit can carry land units across water (the
|
||||
/// `transport` keyword, e.g. `dwarf_fortress_ship`). Data-driven: no unit id
|
||||
/// is hardcoded.
|
||||
pub fn is_transport(&self) -> bool {
|
||||
self.keywords.iter().any(|k| k == "transport")
|
||||
}
|
||||
}
|
||||
|
||||
/// p3-18 — how many land units a `transport`-keyword hull can carry, per the
|
||||
/// `transport` keyword definition in `combat.json` ("Can carry up to 2 land
|
||||
/// units across water"). A named constant rather than a magic literal; if a
|
||||
/// per-unit capacity is ever authored it supersedes this.
|
||||
pub const TRANSPORT_CAPACITY: usize = 2;
|
||||
|
||||
/// Base combat stats authored directly on `units/<id>.json`. Mirrors the
|
||||
/// `mc_combat::resolver::UnitStats` channels that `MapUnit` carries
|
||||
/// (`hp`/`max_hp`/`attack`/`defense`/`ranged_attack`/`range`). `max_hp`
|
||||
|
|
@ -341,6 +362,7 @@ mod tests {
|
|||
ransom_multiplier: 2.0,
|
||||
build_cost: 0,
|
||||
logistics: None,
|
||||
keywords: Vec::new(),
|
||||
combat: CombatStats::default(),
|
||||
});
|
||||
assert_eq!(cat.len(), 1);
|
||||
|
|
@ -438,4 +460,22 @@ mod tests {
|
|||
let n = cat.load_json_str("42").expect("parse");
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_keyword_detected_data_driven() {
|
||||
let raw = r#"[
|
||||
{"id":"barge","movement":4,"domain":"naval","keywords":["transport"]},
|
||||
{"id":"warrior","movement":2,"domain":"land","keywords":["melee"]},
|
||||
{"id":"founder","movement":2,"domain":"land"}
|
||||
]"#;
|
||||
let mut cat = UnitsCatalog::new();
|
||||
cat.load_json_str(raw).expect("parse");
|
||||
assert!(cat.get("barge").unwrap().is_transport(), "transport keyword → is_transport");
|
||||
assert!(!cat.get("warrior").unwrap().is_transport());
|
||||
assert!(
|
||||
!cat.get("founder").unwrap().is_transport(),
|
||||
"missing keywords block → not a transport"
|
||||
);
|
||||
assert_eq!(TRANSPORT_CAPACITY, 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ pub mod catalog;
|
|||
|
||||
pub use action::UnitActionDef;
|
||||
pub use ap::{ActionCtx, ApCostError};
|
||||
pub use catalog::{CombatStats, UnitStats, UnitsCatalog};
|
||||
pub use catalog::{CombatStats, UnitStats, UnitsCatalog, TRANSPORT_CAPACITY};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue