91 lines
3.7 KiB
Rust
91 lines
3.7 KiB
Rust
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<RuntimeStoryCompanionViewModel> {
|
||
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<RuntimeStoryEncounterViewModel> {
|
||
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))
|
||
}
|