feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-0 — project equipped items to UnitView
EquippedItemView (item_id, category, charges_remaining, triggers_in_combat — the exact mc_items::EquippedItem fields, cited) + UnitView.equipped, projected from MapUnit.equipped for OWN units only, omitted from the wire when empty. Surfaces the unit_panel.gd:789 entity read via view_json. happiness_breakdown DEFERRED (verified, not fabricated): the per-contributor breakdown is a transient calculate_happiness return (mc-happiness/pool.rs:170), not persisted PlayerState — only the scalar happiness pool is stored (game_state.rs:1295), already surfaced as ResourceView.happiness_pool. A Phase-1 SOT-flip widening, like XP/culture_stored. Dispatched simulator-infra; verify gate: mc-player-api green incl. new equipped round-trip/omit test. Additive (serde defaults). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
76b3e48ae3
commit
8c3e7b8a27
2 changed files with 90 additions and 1 deletions
|
|
@ -46,7 +46,7 @@ use mc_state::game_state::GameState;
|
|||
use mc_vision::{compute_vision, PlayerVision, VisionCatalog};
|
||||
|
||||
use crate::view::{
|
||||
CityView, CivicsView, CultureView, DiplomacyView, LegalActionEntry,
|
||||
CityView, CivicsView, CultureView, DiplomacyView, EquippedItemView, LegalActionEntry,
|
||||
PendingEventsView, PlayerView, ProductionQueueEntry, ResearchView, ResourceView, ScoreView,
|
||||
TileView, UnitPostureView, UnitView,
|
||||
};
|
||||
|
|
@ -392,6 +392,21 @@ fn project_units(
|
|||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
// Own units expose their equipped gear; enemies do not (no
|
||||
// loadout leak). Empty vecs are omitted on the wire.
|
||||
equipped: if is_own {
|
||||
unit.equipped
|
||||
.iter()
|
||||
.map(|it| EquippedItemView {
|
||||
item_id: it.item_id.clone(),
|
||||
category: it.category.clone(),
|
||||
charges_remaining: it.charges_remaining,
|
||||
triggers_in_combat: it.triggers_in_combat,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,22 @@ impl UnitPostureView {
|
|||
}
|
||||
}
|
||||
|
||||
/// One equipped item on a unit. Mirrors the subset of
|
||||
/// [`mc_items::EquippedItem`] the unit panel needs to render gear + remaining
|
||||
/// charges. Surfaced for **own units only** (no enemy-loadout leak).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EquippedItemView {
|
||||
/// Item definition id (e.g. `"iron_sword"`).
|
||||
pub item_id: String,
|
||||
/// Schema category: `"permanent"` | `"consumable"` | `"tool"`.
|
||||
pub category: String,
|
||||
/// Charges left before the item is spent (`0` for permanent gear).
|
||||
pub charges_remaining: i32,
|
||||
/// True if charges tick down once per combat resolved (combat-charge mode).
|
||||
#[serde(default, skip_serializing_if = "core::ops::Not::not")]
|
||||
pub triggers_in_combat: bool,
|
||||
}
|
||||
|
||||
/// One unit — own or visible enemy.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct UnitView {
|
||||
|
|
@ -243,6 +259,9 @@ pub struct UnitView {
|
|||
/// Per-unit legal actions. Empty on enemy units.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub legal_actions: Vec<LegalActionEntry>,
|
||||
/// Equipped gear (own units only). Omitted on the wire when empty.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub equipped: Vec<EquippedItemView>,
|
||||
}
|
||||
|
||||
/// One explored tile.
|
||||
|
|
@ -479,6 +498,7 @@ mod tests {
|
|||
formation_id: None,
|
||||
posture: UnitPostureView::default(),
|
||||
legal_actions: Vec::new(),
|
||||
equipped: Vec::new(),
|
||||
};
|
||||
let json = serde_json::to_string(&u).unwrap();
|
||||
assert!(json.contains("\"type\":\"dwarf_warrior\""), "json={}", json);
|
||||
|
|
@ -520,6 +540,7 @@ mod tests {
|
|||
formation_id: Some(7),
|
||||
posture: UnitPostureView { braced: true, ..Default::default() },
|
||||
legal_actions: Vec::new(),
|
||||
equipped: Vec::new(),
|
||||
};
|
||||
let json = serde_json::to_string(&u).unwrap();
|
||||
assert!(json.contains("\"braced\":true"), "active posture must serialize: {json}");
|
||||
|
|
@ -531,4 +552,57 @@ mod tests {
|
|||
let json2 = serde_json::to_string(&u).unwrap();
|
||||
assert!(!json2.contains("braced"), "resting posture omitted: {json2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_view_equipped_round_trips_and_omits_when_empty() {
|
||||
let mut u = UnitView {
|
||||
id: "u_3".into(),
|
||||
type_id: "dwarf_warrior".into(),
|
||||
position: [2, 2],
|
||||
owner: 0,
|
||||
hp: 12,
|
||||
max_hp: 12,
|
||||
movement_left: 2,
|
||||
movement_max: 2,
|
||||
experience: 0,
|
||||
promotion_available: false,
|
||||
fortified: false,
|
||||
sentry: false,
|
||||
formation_id: None,
|
||||
posture: UnitPostureView::default(),
|
||||
legal_actions: Vec::new(),
|
||||
equipped: Vec::new(),
|
||||
};
|
||||
// Empty loadout → field omitted entirely (wire economy).
|
||||
let json = serde_json::to_string(&u).unwrap();
|
||||
assert!(!json.contains("equipped"), "empty equipped omitted: {json}");
|
||||
|
||||
// Populate gear: a permanent weapon + a combat-charge charm.
|
||||
u.equipped = vec![
|
||||
EquippedItemView {
|
||||
item_id: "iron_sword".into(),
|
||||
category: "permanent".into(),
|
||||
charges_remaining: 0,
|
||||
triggers_in_combat: false,
|
||||
},
|
||||
EquippedItemView {
|
||||
item_id: "war_charm".into(),
|
||||
category: "consumable".into(),
|
||||
charges_remaining: 3,
|
||||
triggers_in_combat: true,
|
||||
},
|
||||
];
|
||||
let json = serde_json::to_string(&u).unwrap();
|
||||
assert!(json.contains("\"item_id\":\"iron_sword\""), "json={json}");
|
||||
assert!(json.contains("\"charges_remaining\":3"), "json={json}");
|
||||
assert!(json.contains("\"triggers_in_combat\":true"), "json={json}");
|
||||
// The permanent item's `triggers_in_combat:false` is omitted (default).
|
||||
assert_eq!(
|
||||
json.matches("triggers_in_combat").count(),
|
||||
1,
|
||||
"false triggers_in_combat omitted: {json}"
|
||||
);
|
||||
let back: UnitView = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(u, back, "equipped round-trips");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue