use serde_json::Value; use shared_contracts::runtime_story::{ RuntimeStoryCompanionViewModel, RuntimeStoryEncounterViewModel, RuntimeStoryOptionView, RuntimeStoryPlayerViewModel, RuntimeStoryStatusViewModel, RuntimeStoryViewModel, }; use crate::{ read_array_field, read_bool_field, read_i32_field, read_object_field, read_optional_string_field, read_required_string_field, }; /// 运行时故事 view-model 只依赖快照 JSON 与共享 contract,可脱离 HTTP 层独立编译。 pub fn build_runtime_story_view_model( game_state: &Value, options: &[RuntimeStoryOptionView], ) -> RuntimeStoryViewModel { RuntimeStoryViewModel { player: RuntimeStoryPlayerViewModel { hp: read_i32_field(game_state, "playerHp").unwrap_or(0), max_hp: read_i32_field(game_state, "playerMaxHp").unwrap_or(1), mana: read_i32_field(game_state, "playerMana").unwrap_or(0), max_mana: read_i32_field(game_state, "playerMaxMana").unwrap_or(1), }, encounter: build_runtime_story_encounter(game_state), companions: build_runtime_story_companions(game_state), available_options: options.to_vec(), status: RuntimeStoryStatusViewModel { in_battle: read_bool_field(game_state, "inBattle").unwrap_or(false), npc_interaction_active: read_bool_field(game_state, "npcInteractionActive") .unwrap_or(false), current_npc_battle_mode: read_optional_string_field(game_state, "currentNpcBattleMode"), current_npc_battle_outcome: read_optional_string_field( game_state, "currentNpcBattleOutcome", ), }, } } pub fn build_runtime_story_companions(game_state: &Value) -> Vec { read_array_field(game_state, "companions") .into_iter() .filter_map(|entry| { let npc_id = read_required_string_field(entry, "npcId")?; Some(RuntimeStoryCompanionViewModel { npc_id, character_id: read_optional_string_field(entry, "characterId"), joined_at_affinity: read_i32_field(entry, "joinedAtAffinity").unwrap_or(0), }) }) .collect() } pub fn build_runtime_story_encounter(game_state: &Value) -> Option { let encounter = read_object_field(game_state, "currentEncounter")?; let npc_name = read_required_string_field(encounter, "npcName") .or_else(|| read_required_string_field(encounter, "name")) .unwrap_or_else(|| "当前遭遇".to_string()); let encounter_id = read_required_string_field(encounter, "id").unwrap_or_else(|| npc_name.clone()); let npc_state = resolve_current_encounter_npc_state(game_state, &encounter_id, &npc_name); Some(RuntimeStoryEncounterViewModel { id: encounter_id, kind: read_required_string_field(encounter, "kind").unwrap_or_else(|| "npc".to_string()), npc_name, hostile: read_bool_field(encounter, "hostile").unwrap_or(false), affinity: npc_state.and_then(|state| read_i32_field(state, "affinity")), recruited: npc_state.and_then(|state| read_bool_field(state, "recruited")), interaction_active: read_bool_field(game_state, "npcInteractionActive").unwrap_or(false), battle_mode: read_optional_string_field(game_state, "currentNpcBattleMode"), }) } pub fn resolve_current_encounter_npc_state<'a>( game_state: &'a Value, encounter_id: &str, npc_name: &str, ) -> Option<&'a Value> { let npc_states = read_object_field(game_state, "npcStates")?; npc_states .get(encounter_id) .or_else(|| npc_states.get(npc_name)) }