use serde::Serialize; use std::collections::BTreeMap; #[derive(Debug, Serialize)] pub struct AdventureRunResponse { pub combat_log: serde_json::Value, pub enemy: String, pub loot: Vec, pub zone: String, } #[derive(Debug, Serialize)] pub struct HealResponse { pub gold: i32, pub healed: bool, pub hp: i32, pub max_hp: i32, } #[derive(Debug, Serialize)] pub struct SpellEntryResponse { pub code: String, pub combat_damage_max: i32, pub combat_damage_min: i32, pub combat_effect: String, pub description: String, pub heal_max: i32, pub heal_min: i32, pub id: String, pub known: bool, pub name: String, pub range: String, pub spell_level: i32, pub spell_points: i32, } #[derive(Debug, Serialize)] pub struct SpellbookResponse { pub caster_class: String, pub learn_costs: BTreeMap, pub max_spell_points: i32, pub spell_level: i32, pub spell_points: i32, pub spells: Vec, } #[derive(Debug, Serialize)] pub struct ActivateSpellResponse { pub active_spell: Option, pub spell_points: i32, } #[derive(Debug, Serialize)] pub struct LearnSpellsResponse { pub gold: i32, pub learned_count: usize, pub max_spell_points: i32, pub spell_points: i32, } #[derive(Debug, Serialize)] #[serde(untagged)] pub enum LearnSpellResponse { Activate(ActivateSpellResponse), Learn(LearnSpellsResponse), } impl From for AdventureRunResponse { fn from(outcome: sb_core::adventure::AdventureOutcome) -> Self { AdventureRunResponse { // to_value mirrors exactly what json! does to &log today: struct → // Value::Object with BTreeMap ordering — nested keys sorted, byte-identical wire. combat_log: serde_json::to_value(&outcome.combat_log) .expect("CombatLog graph contains no map types; to_value is infallible"), enemy: outcome.enemy_name, loot: outcome.loot_item_names, zone: outcome.zone_id, } } } #[cfg(test)] mod tests { use super::*; use sb_shared::{CombatLog, CombatRound, CritEntry, CritResult}; fn representative_log() -> CombatLog { CombatLog { rounds: vec![ CombatRound { attacker: "Test".into(), defender: "Giant Rat".into(), raw_roll: 67, ob: 15, db: 0, result: 82, damage: 12, crit: None, defender_hp: 0, }, CombatRound { attacker: "Giant Rat".into(), defender: "Test".into(), raw_roll: 99, ob: 3, db: 5, result: 97, damage: 18, crit: Some(CritResult { severity: "A".into(), crit: CritEntry { name: "critical".into(), damage_multiplier: 2.0, stun_rounds: 1, bleed_per_round: 2, bleed_rounds: 3, effects: vec!["stun".into()], }, }), defender_hp: 62, }, ], winner: "Test".into(), loser: "Giant Rat".into(), xp_gained: 5, gold_gained: 2, } } fn representative_outcome() -> sb_core::adventure::AdventureOutcome { sb_core::adventure::AdventureOutcome { zone_id: "skara_brae_streets".into(), enemy_name: "Giant Rat".into(), combat_log: representative_log(), loot_item_names: vec!["Iron Sword".into()], } } const GOLDEN_W1: &str = r#"{"combat_log":{"gold_gained":2,"loser":"Giant Rat","rounds":[{"attacker":"Test","crit":null,"damage":12,"db":0,"defender":"Giant Rat","defender_hp":0,"ob":15,"raw_roll":67,"result":82},{"attacker":"Giant Rat","crit":{"crit":{"bleed_per_round":2,"bleed_rounds":3,"damage_multiplier":2.0,"effects":["stun"],"name":"critical","stun_rounds":1},"severity":"A"},"damage":18,"db":5,"defender":"Test","defender_hp":62,"ob":3,"raw_roll":99,"result":97}],"winner":"Test","xp_gained":5},"enemy":"Giant Rat","loot":["Iron Sword"],"zone":"skara_brae_streets"}"#; const GOLDEN_W2: &str = r#"{"gold":123,"healed":true,"hp":45,"max_hp":80}"#; const GOLDEN_W3: &str = r#"{"code":"fire_bolt","combat_damage_max":6,"combat_damage_min":2,"combat_effect":"damage","description":"Hurls a mote of fire at the target.","heal_max":0,"heal_min":0,"id":"spell_fire_bolt","known":true,"name":"Fire Bolt","range":"short","spell_level":1,"spell_points":10}"#; const GOLDEN_W4: &str = r#"{"caster_class":"mage","learn_costs":{"2":1000,"3":2000,"4":4000,"5":7000,"6":10000,"7":20000},"max_spell_points":40,"spell_level":4,"spell_points":12,"spells":[{"code":"fire_bolt","combat_damage_max":6,"combat_damage_min":2,"combat_effect":"damage","description":"Hurls a mote of fire at the target.","heal_max":0,"heal_min":0,"id":"spell_fire_bolt","known":true,"name":"Fire Bolt","range":"short","spell_level":1,"spell_points":10}]}"#; const GOLDEN_W5: &str = r#"{"active_spell":"mage_armor","spell_points":12}"#; const GOLDEN_W5_NULL: &str = r#"{"active_spell":null,"spell_points":12}"#; const GOLDEN_W6: &str = r#"{"gold":19700,"learned_count":4,"max_spell_points":40,"spell_points":40}"#; #[test] fn wire_identity_adventure_response() { let dto: AdventureRunResponse = representative_outcome().into(); assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W1); } #[test] fn wire_identity_heal_response() { let dto = HealResponse { gold: 123, healed: true, hp: 45, max_hp: 80, }; assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W2); } fn representative_spell_entry() -> SpellEntryResponse { SpellEntryResponse { code: "fire_bolt".into(), combat_damage_max: 6, combat_damage_min: 2, combat_effect: "damage".into(), description: "Hurls a mote of fire at the target.".into(), heal_max: 0, heal_min: 0, id: "spell_fire_bolt".into(), known: true, name: "Fire Bolt".into(), range: "short".into(), spell_level: 1, spell_points: 10, } } #[test] fn wire_identity_spell_entry_response() { let dto = representative_spell_entry(); assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W3); } #[test] fn wire_identity_spellbook_response() { let dto = SpellbookResponse { caster_class: "mage".into(), learn_costs: BTreeMap::from([ ("2".into(), 1000), ("3".into(), 2000), ("4".into(), 4000), ("5".into(), 7000), ("6".into(), 10000), ("7".into(), 20000), ]), max_spell_points: 40, spell_level: 4, spell_points: 12, spells: vec![representative_spell_entry()], }; assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W4); } #[test] fn wire_identity_activate_spell_response() { let dto = ActivateSpellResponse { active_spell: Some("mage_armor".into()), spell_points: 12, }; assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W5); let dto = ActivateSpellResponse { active_spell: None, spell_points: 12, }; assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W5_NULL); } #[test] fn wire_identity_learn_response() { let dto = LearnSpellResponse::Learn(LearnSpellsResponse { gold: 19700, learned_count: 4, max_spell_points: 40, spell_points: 40, }); assert_eq!(serde_json::to_string(&dto).unwrap(), GOLDEN_W6); } }