use serde_json::Value; use shared_contracts::runtime_story::{RuntimeStoryOptionInteraction, RuntimeStoryOptionView}; use crate::{read_bool_field, read_field, read_optional_string_field, read_required_string_field}; /// 这批 helper 只负责 runtime story option 的纯 DTO 编译,不触碰 HTTP / AppState。 pub fn infer_option_scope(function_id: &str) -> &'static str { if function_id.starts_with("battle_") || function_id == "inventory_use" { "combat" } else if function_id.starts_with("npc_") { "npc" } else { "story" } } pub fn build_static_runtime_story_option( function_id: &str, action_text: &str, scope: &str, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { function_id: function_id.to_string(), action_text: action_text.to_string(), detail_text: None, scope: scope.to_string(), interaction: None, payload: None, disabled: None, reason: None, } } pub fn build_runtime_story_option_with_payload( function_id: &str, action_text: &str, scope: &str, detail_text: Option, payload: Value, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { detail_text, payload: Some(payload), ..build_static_runtime_story_option(function_id, action_text, scope) } } pub fn build_disabled_runtime_story_option( function_id: &str, action_text: &str, scope: &str, detail_text: Option, reason: &str, payload: Option, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { detail_text, payload, disabled: Some(true), reason: Some(reason.to_string()), ..build_static_runtime_story_option(function_id, action_text, scope) } } pub fn build_runtime_story_option_from_story_option( value: &Value, ) -> Option { let function_id = read_required_string_field(value, "functionId")?; let action_text = read_required_string_field(value, "actionText") .or_else(|| read_required_string_field(value, "text")) .unwrap_or_else(|| function_id.clone()); Some(RuntimeStoryOptionView { scope: infer_option_scope(function_id.as_str()).to_string(), detail_text: read_optional_string_field(value, "detailText"), interaction: build_runtime_story_option_interaction(read_field(value, "interaction")), payload: read_field(value, "runtimePayload") .or_else(|| read_field(value, "payload")) .cloned(), disabled: read_bool_field(value, "disabled"), reason: read_optional_string_field(value, "disabledReason") .or_else(|| read_optional_string_field(value, "reason")), function_id, action_text, }) } pub fn build_runtime_story_option_interaction( value: Option<&Value>, ) -> Option { let interaction = value?; match read_required_string_field(interaction, "kind")?.as_str() { "npc" => Some(RuntimeStoryOptionInteraction::Npc { npc_id: read_required_string_field(interaction, "npcId")?, action: read_required_string_field(interaction, "action")?, quest_id: read_optional_string_field(interaction, "questId"), }), _ => None, } } pub fn build_story_option_from_runtime_option(option: &RuntimeStoryOptionView) -> Value { serde_json::json!({ "functionId": option.function_id, "actionText": option.action_text, "text": option.action_text, "detailText": option.detail_text, "visuals": { "playerAnimation": "idle", "playerMoveMeters": 0, "playerOffsetY": 0, "playerFacing": "right", "scrollWorld": false, "monsterChanges": [] }, "interaction": option.interaction, "runtimePayload": option.payload, "disabled": option.disabled, "disabledReason": option.reason, }) }