Close DDD cleanup and tests-support closure

This commit is contained in:
2026-04-30 16:15:05 +08:00
parent 7ab0933f6d
commit fd08262bf0
81 changed files with 8415 additions and 6662 deletions

View File

@@ -2,12 +2,15 @@
`module-runtime-story` 承接 RPG runtime story 的纯领域规则、应用用例、事件和错误模型,不依赖 HTTP / `AppState` / SpacetimeDB。
当前已经迁入的历史快照态纯逻辑会继续收口为 session scoped 新主链:
当前已经迁入的历史快照态纯逻辑会继续收口为 session scoped 新主链;顶层 DDD 物理拆分已经完成
1. action 结算结果结构
2. action response 组装参数结构
3. NPC 委托上下文结构
4. functionId / 队伍上限常量
5. 少量只依赖 `serde_json::Value``shared-contracts` 的纯 helper
1. `src/domain.rs` 承载 action 结算结果结构、NPC 委托上下文、functionId / 队伍上限常量
2. `src/commands.rs` 承载 action 文本解析 helper
3. `src/application.rs` 承载 action response 组装参数、status patch 和 world type helper
4. `src/events.rs` 承载 runtime story 领域事件
5. `src/errors.rs` 承载 runtime story 纯规则错误
6. `src/lib.rs` 只保留模块声明、公开导出和子模块 re-export。
后续 WP-RS 继续按 battle / forge / NPC / quest / presentation 的顺序,把旧 `/api/runtime/story/*` 写侧能力迁到 session scoped 新接口,并删除运行代码中的旧入口命名。
配套记录见 [../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md](../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md)。

View File

@@ -1,3 +1,58 @@
//! runtime story 应用编排落位。
//!
//! 这里组合纯领域规则并返回后端投影真实保存、SSE 和模型调用由外层完成。
use serde_json::Value;
use shared_contracts::runtime_story::{
RuntimeBattlePresentation, RuntimeStoryOptionView, RuntimeStoryPatch,
RuntimeStorySnapshotPayload,
};
use crate::{StoryResolution, read_bool_field, read_optional_string_field};
pub struct RuntimeStoryActionResponseParts {
pub requested_session_id: String,
pub server_version: u32,
pub snapshot: RuntimeStorySnapshotPayload,
pub action_text: String,
pub result_text: String,
pub story_text: String,
pub options: Vec<RuntimeStoryOptionView>,
pub patches: Vec<RuntimeStoryPatch>,
pub toast: Option<String>,
pub battle: Option<RuntimeBattlePresentation>,
}
pub fn simple_story_resolution(
game_state: &Value,
action_text: String,
result_text: &str,
) -> StoryResolution {
StoryResolution {
action_text,
result_text: result_text.to_string(),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: vec![build_status_patch(game_state)],
battle: None,
toast: None,
}
}
pub fn build_status_patch(game_state: &Value) -> RuntimeStoryPatch {
RuntimeStoryPatch::StatusChanged {
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 current_world_type(game_state: &Value) -> Option<String> {
read_optional_string_field(game_state, "worldType")
}

View File

@@ -1,3 +1,16 @@
//! runtime story 写入命令过渡落位
//! runtime story 写入命令。
//!
//! 用于表达剧情动作解析、战斗动作、锻造动作和 NPC 互动等输入。
use shared_contracts::runtime_story::RuntimeStoryActionRequest;
use crate::read_optional_string_field;
pub fn resolve_action_text(default_text: &str, request: &RuntimeStoryActionRequest) -> String {
request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "optionText"))
.unwrap_or_else(|| default_text.to_string())
}

View File

@@ -1,4 +1,44 @@
//! runtime story 领域模型过渡落位
//! runtime story 领域模型。
//!
//! 当前 crate 用于运行时剧情主链的纯规则收口。后续迁移时仍只能保留 JSON 规则、
//! 选项生成和视图模型转换,不引入 Axum、LLM 或 SpacetimeDB。
use serde_json::Value;
use shared_contracts::runtime_story::{
RuntimeBattlePresentation, RuntimeStoryOptionView, RuntimeStoryPatch,
};
pub const CONTINUE_ADVENTURE_FUNCTION_ID: &str = "story_continue_adventure";
pub const MAX_TASK5_COMPANIONS: usize = 2;
pub struct StoryResolution {
pub action_text: String,
pub result_text: String,
pub story_text: Option<String>,
pub presentation_options: Option<Vec<RuntimeStoryOptionView>>,
pub saved_current_story: Option<Value>,
pub patches: Vec<RuntimeStoryPatch>,
pub battle: Option<RuntimeBattlePresentation>,
pub toast: Option<String>,
}
pub struct GeneratedStoryPayload {
pub story_text: String,
pub history_result_text: String,
pub presentation_options: Vec<RuntimeStoryOptionView>,
pub saved_current_story: Value,
}
pub struct CurrentEncounterNpcQuestContext {
pub npc_id: String,
pub npc_name: String,
}
pub struct PendingQuestOfferContext {
pub dialogue: Vec<Value>,
pub turn_count: i32,
pub custom_input_placeholder: String,
pub quest: Value,
pub quest_id: String,
pub intro_text: Option<String>,
}

View File

@@ -1,3 +1,26 @@
//! runtime story 领域错误过渡落位
//! runtime story 领域错误。
//!
//! 错误只表达运行时剧情规则失败,不能直接绑定 HTTP 或数据库错误模型。
use std::{error::Error, fmt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RuntimeStoryRuleError {
MissingRuntimeSessionId,
MissingStoryAction,
UnsupportedStoryAction,
InvalidRuntimeSnapshot,
}
impl fmt::Display for RuntimeStoryRuleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingRuntimeSessionId => f.write_str("runtime_story.session_id 不能为空"),
Self::MissingStoryAction => f.write_str("runtime_story.action 不能为空"),
Self::UnsupportedStoryAction => f.write_str("runtime_story.action 当前不受支持"),
Self::InvalidRuntimeSnapshot => f.write_str("runtime_story.snapshot 非法"),
}
}
}
impl Error for RuntimeStoryRuleError {}

View File

@@ -1,3 +1,22 @@
//! runtime story 领域事件过渡落位
//! runtime story 领域事件。
//!
//! 用于表达剧情快照变化、战斗表现变化和物品/成长待同步等事实。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RuntimeStoryDomainEvent {
SnapshotChanged {
runtime_session_id: String,
story_session_id: Option<String>,
occurred_at_micros: i64,
},
BattlePresentationChanged {
runtime_session_id: String,
battle_state_id: Option<String>,
occurred_at_micros: i64,
},
CrossDomainSyncPending {
runtime_session_id: String,
reason: String,
occurred_at_micros: i64,
},
}

View File

@@ -4,12 +4,6 @@ mod domain;
mod errors;
mod events;
use serde_json::Value;
use shared_contracts::runtime_story::{
RuntimeBattlePresentation, RuntimeStoryActionRequest, RuntimeStoryOptionView,
RuntimeStoryPatch, RuntimeStorySnapshotPayload,
};
pub mod battle;
#[cfg(test)]
mod battle_tests;
@@ -25,10 +19,12 @@ pub mod prompt_context;
pub mod story_engine;
pub mod view_model;
pub use application::*;
pub use battle::{
build_battle_runtime_story_options, inventory_item_has_usable_effect, resolve_battle_action,
restore_player_resource,
};
pub use commands::*;
pub use core::{
MAX_PLAYER_LEVEL, add_player_currency, add_player_inventory_items, append_active_build_buffs,
append_story_history, clear_encounter_only, clear_encounter_state, cumulative_xp_required,
@@ -41,6 +37,9 @@ pub use core::{
write_first_hostile_npc_i32_field, write_i32_field, write_null_field, write_string_field,
write_u32_field, xp_to_next_level_for,
};
pub use domain::*;
pub use errors::*;
pub use events::*;
pub use forge::{build_runtime_equipment_item, build_runtime_material_item, format_currency_text};
pub use forge_actions::{
resolve_forge_craft_action, resolve_forge_dismantle_action, resolve_forge_reforge_action,
@@ -77,94 +76,3 @@ pub use view_model::{
build_runtime_story_companions, build_runtime_story_encounter, build_runtime_story_view_model,
resolve_current_encounter_npc_state,
};
pub const CONTINUE_ADVENTURE_FUNCTION_ID: &str = "story_continue_adventure";
pub const MAX_TASK5_COMPANIONS: usize = 2;
pub struct StoryResolution {
pub action_text: String,
pub result_text: String,
pub story_text: Option<String>,
pub presentation_options: Option<Vec<RuntimeStoryOptionView>>,
pub saved_current_story: Option<Value>,
pub patches: Vec<RuntimeStoryPatch>,
pub battle: Option<RuntimeBattlePresentation>,
pub toast: Option<String>,
}
pub struct GeneratedStoryPayload {
pub story_text: String,
pub history_result_text: String,
pub presentation_options: Vec<RuntimeStoryOptionView>,
pub saved_current_story: Value,
}
pub struct CurrentEncounterNpcQuestContext {
pub npc_id: String,
pub npc_name: String,
}
pub struct PendingQuestOfferContext {
pub dialogue: Vec<Value>,
pub turn_count: i32,
pub custom_input_placeholder: String,
pub quest: Value,
pub quest_id: String,
pub intro_text: Option<String>,
}
pub struct RuntimeStoryActionResponseParts {
pub requested_session_id: String,
pub server_version: u32,
pub snapshot: RuntimeStorySnapshotPayload,
pub action_text: String,
pub result_text: String,
pub story_text: String,
pub options: Vec<RuntimeStoryOptionView>,
pub patches: Vec<RuntimeStoryPatch>,
pub toast: Option<String>,
pub battle: Option<RuntimeBattlePresentation>,
}
pub fn simple_story_resolution(
game_state: &Value,
action_text: String,
result_text: &str,
) -> StoryResolution {
StoryResolution {
action_text,
result_text: result_text.to_string(),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: vec![build_status_patch(game_state)],
battle: None,
toast: None,
}
}
pub fn resolve_action_text(default_text: &str, request: &RuntimeStoryActionRequest) -> String {
request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "optionText"))
.unwrap_or_else(|| default_text.to_string())
}
pub fn build_status_patch(game_state: &Value) -> RuntimeStoryPatch {
RuntimeStoryPatch::StatusChanged {
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 current_world_type(game_state: &Value) -> Option<String> {
read_optional_string_field(game_state, "worldType")
}