Close DDD refactor and remove generated asset proxy

This commit is contained in:
kdletters
2026-05-02 00:27:22 +08:00
parent fd08262bf0
commit 9d9913095d
605 changed files with 11811 additions and 10106 deletions

View File

@@ -6,14 +6,6 @@ pub struct StartPuzzleRunRequest {
pub profile_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdvanceLocalPuzzleNextLevelRequest {
pub run: PuzzleRunSnapshotResponse,
#[serde(default)]
pub source_session_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SwapPuzzlePiecesRequest {

View File

@@ -1,48 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStorySnapshotPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
pub bottom_tab: String,
pub game_state: Value,
#[serde(default)]
pub current_story: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryStateResolveRequest {
pub session_id: String,
#[serde(default)]
pub client_version: Option<u32>,
#[serde(default)]
pub snapshot: Option<RuntimeStorySnapshotPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryBootstrapRequest {
pub world_type: String,
#[serde(default)]
pub custom_world_profile: Option<Value>,
pub character: Value,
#[serde(default)]
pub runtime_mode: Option<String>,
#[serde(default)]
pub disable_persistence: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryBootstrapResponse {
pub session_id: String,
pub server_version: u32,
pub snapshot: RuntimeStorySnapshotPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryChoiceAction {
@@ -62,8 +20,6 @@ pub struct RuntimeStoryActionRequest {
#[serde(default)]
pub client_version: Option<u32>,
pub action: RuntimeStoryChoiceAction,
#[serde(default)]
pub snapshot: Option<RuntimeStorySnapshotPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -404,43 +360,11 @@ pub enum RuntimeStoryPatch {
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryActionResponse {
pub session_id: String,
pub server_version: u32,
pub view_model: RuntimeStoryViewModel,
pub presentation: RuntimeStoryPresentation,
pub patches: Vec<RuntimeStoryPatch>,
pub snapshot: RuntimeStorySnapshotPayload,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn runtime_story_state_resolve_request_accepts_missing_saved_at() {
let payload: RuntimeStoryStateResolveRequest = serde_json::from_value(json!({
"sessionId": "runtime-main",
"clientVersion": 7,
"snapshot": {
"bottomTab": "adventure",
"gameState": { "runtimeSessionId": "runtime-main" },
"currentStory": { "text": "营地里的火光还没有熄灭。" }
}
}))
.expect("payload should deserialize");
assert_eq!(payload.session_id, "runtime-main");
assert_eq!(payload.client_version, Some(7));
assert_eq!(
payload.snapshot.expect("snapshot should exist").saved_at,
None
);
}
#[test]
fn runtime_story_action_request_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryActionRequest {
@@ -452,12 +376,6 @@ mod tests {
target_id: Some("npc_camp_firekeeper".to_string()),
payload: Some(json!({ "optionText": "继续交谈" })),
},
snapshot: Some(RuntimeStorySnapshotPayload {
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()),
bottom_tab: "adventure".to_string(),
game_state: json!({ "runtimeSessionId": "runtime-main" }),
current_story: None,
}),
})
.expect("payload should serialize");
@@ -466,27 +384,6 @@ mod tests {
assert_eq!(payload["action"]["type"], json!("story_choice"));
assert_eq!(payload["action"]["functionId"], json!("npc_chat"));
assert_eq!(payload["action"]["targetId"], json!("npc_camp_firekeeper"));
assert_eq!(
payload["snapshot"]["savedAt"],
json!("2026-04-22T12:00:00.000Z")
);
}
#[test]
fn runtime_story_bootstrap_request_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryBootstrapRequest {
world_type: "CUSTOM".to_string(),
custom_world_profile: Some(json!({ "id": "profile-1" })),
character: json!({ "id": "role-1", "name": "沈砺" }),
runtime_mode: Some("play".to_string()),
disable_persistence: Some(false),
})
.expect("payload should serialize");
assert_eq!(payload["worldType"], json!("CUSTOM"));
assert_eq!(payload["customWorldProfile"]["id"], json!("profile-1"));
assert_eq!(payload["runtimeMode"], json!("play"));
assert_eq!(payload["disablePersistence"], json!(false));
}
#[test]
@@ -532,37 +429,157 @@ mod tests {
}
#[test]
fn runtime_story_action_response_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryActionResponse {
session_id: "runtime-main".to_string(),
server_version: 8,
view_model: RuntimeStoryViewModel {
player: RuntimeStoryPlayerViewModel {
hp: 32,
max_hp: 40,
mana: 18,
max_mana: 20,
},
encounter: Some(RuntimeStoryEncounterViewModel {
id: "npc_camp_firekeeper".to_string(),
kind: "npc".to_string(),
npc_name: "守火人".to_string(),
hostile: false,
affinity: Some(12),
recruited: Some(false),
interaction_active: true,
battle_mode: None,
}),
companions: vec![RuntimeStoryCompanionViewModel {
npc_id: "npc_companion_001".to_string(),
character_id: Some("char_companion_001".to_string()),
joined_at_affinity: 64,
fn runtime_story_presentation_uses_camel_case_fields() {
let view_model = RuntimeStoryViewModel {
player: RuntimeStoryPlayerViewModel {
hp: 32,
max_hp: 40,
mana: 18,
max_mana: 20,
},
encounter: Some(RuntimeStoryEncounterViewModel {
id: "npc_camp_firekeeper".to_string(),
kind: "npc".to_string(),
npc_name: "守火人".to_string(),
hostile: false,
affinity: Some(12),
recruited: Some(false),
interaction_active: true,
battle_mode: None,
}),
companions: vec![RuntimeStoryCompanionViewModel {
npc_id: "npc_companion_001".to_string(),
character_id: Some("char_companion_001".to_string()),
joined_at_affinity: 64,
}],
inventory: RuntimeStoryInventoryViewModel {
player_currency: 80,
currency_text: "80 铜钱".to_string(),
in_battle: false,
backpack_items: vec![RuntimeStoryInventoryItemView {
item: json!({
"id": "potion-1",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
actions: RuntimeStoryInventoryItemActionsView {
use_item: RuntimeStoryInventoryActionView {
function_id: "inventory_use".to_string(),
action_text: "使用疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: true,
reason: None,
},
equip: RuntimeStoryInventoryActionView {
function_id: "equipment_equip".to_string(),
action_text: "装备疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能装备。".to_string()),
},
dismantle: RuntimeStoryInventoryActionView {
function_id: "forge_dismantle".to_string(),
action_text: "拆解疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能拆解。".to_string()),
},
reforge: RuntimeStoryInventoryActionView {
function_id: "forge_reforge".to_string(),
action_text: "重铸疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能重铸。".to_string()),
},
},
}],
inventory: RuntimeStoryInventoryViewModel {
player_currency: 80,
currency_text: "80 铜钱".to_string(),
in_battle: false,
backpack_items: vec![RuntimeStoryInventoryItemView {
equipment_slots: vec![RuntimeStoryEquipmentSlotView {
slot_id: "weapon".to_string(),
label: "武器".to_string(),
item: None,
unequip: RuntimeStoryInventoryActionView {
function_id: "equipment_unequip".to_string(),
action_text: "卸下武器".to_string(),
payload: Some(json!({ "slotId": "weapon" })),
enabled: false,
reason: Some("武器位当前没有装备。".to_string()),
},
}],
forge_recipes: vec![RuntimeStoryForgeRecipeView {
id: "synthesis-refined-ingot".to_string(),
name: "压炼锭材".to_string(),
kind: "synthesis".to_string(),
description: "把零散残片和基础材料压成稳定可用的金属锭材。".to_string(),
result_label: "精炼锭材".to_string(),
currency_cost: 18,
currency_text: "18 铜钱".to_string(),
requirements: vec![RuntimeStoryForgeRequirementView {
id: "material:any".to_string(),
label: "任意材料".to_string(),
quantity: 3,
owned: 0,
}],
can_craft: false,
disabled_reason: Some("材料不足。".to_string()),
action: RuntimeStoryInventoryActionView {
function_id: "forge_craft".to_string(),
action_text: "制作精炼锭材".to_string(),
payload: Some(json!({ "recipeId": "synthesis-refined-ingot" })),
enabled: false,
reason: Some("材料不足。".to_string()),
},
}],
},
available_options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
action: "chat".to_string(),
quest_id: None,
}),
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
status: RuntimeStoryStatusViewModel {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
npc_interaction: Some(RuntimeNpcInteractionView {
npc_id: "npc_camp_firekeeper".to_string(),
npc_name: "守火人".to_string(),
player_currency: 80,
currency_name: "铜钱".to_string(),
trade: RuntimeNpcTradeView {
buy_items: vec![RuntimeNpcTradeItemView {
item_id: "npc-potion".to_string(),
item: json!({
"id": "npc-potion",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
mode: "buy".to_string(),
unit_price: 20,
max_quantity: 2,
can_submit: true,
reason: None,
}],
sell_items: Vec::new(),
},
gift: RuntimeNpcGiftView {
items: vec![RuntimeNpcGiftItemView {
item_id: "potion-1".to_string(),
item: json!({
"id": "potion-1",
"name": "疗伤药",
@@ -571,176 +588,47 @@ mod tests {
"rarity": "common",
"tags": ["healing"]
}),
actions: RuntimeStoryInventoryItemActionsView {
use_item: RuntimeStoryInventoryActionView {
function_id: "inventory_use".to_string(),
action_text: "使用疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: true,
reason: None,
},
equip: RuntimeStoryInventoryActionView {
function_id: "equipment_equip".to_string(),
action_text: "装备疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能装备。".to_string()),
},
dismantle: RuntimeStoryInventoryActionView {
function_id: "forge_dismantle".to_string(),
action_text: "拆解疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能拆解。".to_string()),
},
reforge: RuntimeStoryInventoryActionView {
function_id: "forge_reforge".to_string(),
action_text: "重铸疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能重铸。".to_string()),
},
},
}],
equipment_slots: vec![RuntimeStoryEquipmentSlotView {
slot_id: "weapon".to_string(),
label: "武器".to_string(),
item: None,
unequip: RuntimeStoryInventoryActionView {
function_id: "equipment_unequip".to_string(),
action_text: "卸下武器".to_string(),
payload: Some(json!({ "slotId": "weapon" })),
enabled: false,
reason: Some("武器位当前没有装备。".to_string()),
},
}],
forge_recipes: vec![RuntimeStoryForgeRecipeView {
id: "synthesis-refined-ingot".to_string(),
name: "压炼锭材".to_string(),
kind: "synthesis".to_string(),
description: "把零散残片和基础材料压成稳定可用的金属锭材。".to_string(),
result_label: "精炼锭材".to_string(),
currency_cost: 18,
currency_text: "18 铜钱".to_string(),
requirements: vec![RuntimeStoryForgeRequirementView {
id: "material:any".to_string(),
label: "任意材料".to_string(),
quantity: 3,
owned: 0,
}],
can_craft: false,
disabled_reason: Some("材料不足。".to_string()),
action: RuntimeStoryInventoryActionView {
function_id: "forge_craft".to_string(),
action_text: "制作精炼锭材".to_string(),
payload: Some(json!({ "recipeId": "synthesis-refined-ingot" })),
enabled: false,
reason: Some("材料不足。".to_string()),
},
affinity_gain: 10,
can_submit: true,
reason: None,
}],
},
available_options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
action: "chat".to_string(),
quest_id: None,
}),
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
status: RuntimeStoryStatusViewModel {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
npc_interaction: Some(RuntimeNpcInteractionView {
}),
};
let presentation = RuntimeStoryPresentation {
action_text: "".to_string(),
result_text: "".to_string(),
story_text: "守火人抬眼看了你一瞬,示意你把想问的话继续说完。".to_string(),
options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
npc_name: "守火人".to_string(),
player_currency: 80,
currency_name: "铜钱".to_string(),
trade: RuntimeNpcTradeView {
buy_items: vec![RuntimeNpcTradeItemView {
item_id: "npc-potion".to_string(),
item: json!({
"id": "npc-potion",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
mode: "buy".to_string(),
unit_price: 20,
max_quantity: 2,
can_submit: true,
reason: None,
}],
sell_items: Vec::new(),
},
gift: RuntimeNpcGiftView {
items: vec![RuntimeNpcGiftItemView {
item_id: "potion-1".to_string(),
item: json!({
"id": "potion-1",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
affinity_gain: 10,
can_submit: true,
reason: None,
}],
},
action: "chat".to_string(),
quest_id: None,
}),
},
presentation: RuntimeStoryPresentation {
action_text: "".to_string(),
result_text: "".to_string(),
story_text: "守火人抬眼看了你一瞬,示意你把想问的话继续说完。".to_string(),
options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
action: "chat".to_string(),
quest_id: None,
}),
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
toast: None,
battle: None,
},
patches: vec![RuntimeStoryPatch::StatusChanged {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
snapshot: RuntimeStorySnapshotPayload {
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()),
bottom_tab: "adventure".to_string(),
game_state: json!({ "runtimeSessionId": "runtime-main" }),
current_story: Some(json!({
"text": "守火人抬眼看了你一瞬,示意你把想问的话继续说完。"
})),
},
})
toast: None,
battle: None,
};
let patches = vec![RuntimeStoryPatch::StatusChanged {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
}];
let payload = serde_json::to_value(json!({
"viewModel": view_model,
"presentation": presentation,
"patches": patches
}))
.expect("payload should serialize");
assert_eq!(payload["sessionId"], json!("runtime-main"));
assert_eq!(payload["serverVersion"], json!(8));
assert_eq!(payload["viewModel"]["player"]["maxHp"], json!(40));
assert_eq!(
payload["viewModel"]["availableOptions"][0]["interaction"]["npcId"],
@@ -759,6 +647,5 @@ mod tests {
json!("守火人抬眼看了你一瞬,示意你把想问的话继续说完。")
);
assert_eq!(payload["patches"][0]["type"], json!("status_changed"));
assert_eq!(payload["snapshot"]["bottomTab"], json!("adventure"));
}
}

View File

@@ -11,6 +11,19 @@ pub struct BeginStorySessionRequest {
pub opening_summary: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BeginStoryRuntimeSessionRequest {
pub world_type: String,
#[serde(default)]
pub custom_world_profile: Option<Value>,
pub character: Value,
#[serde(default)]
pub runtime_mode: Option<String>,
#[serde(default)]
pub disable_persistence: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContinueStoryRequest {
@@ -20,6 +33,20 @@ pub struct ContinueStoryRequest {
pub choice_function_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResolveStoryRuntimeActionRequest {
pub story_session_id: String,
#[serde(default)]
pub client_version: Option<u32>,
pub function_id: String,
pub action_text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StorySessionPayload {
@@ -65,6 +92,17 @@ pub struct StorySessionStateResponse {
pub story_events: Vec<StoryEventPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StoryRuntimeSnapshotPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
pub bottom_tab: String,
pub game_state: Value,
#[serde(default)]
pub current_story: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StoryRuntimeProjectionRequest {
@@ -126,6 +164,7 @@ pub struct StoryRuntimeProjectionResponse {
pub story_session: StorySessionPayload,
pub story_events: Vec<StoryEventPayload>,
pub server_version: u32,
pub game_state: Value,
pub actor: StoryRuntimeActorProjection,
pub inventory: StoryRuntimeInventoryProjection,
pub options: Vec<StoryRuntimeOptionProjection>,
@@ -138,6 +177,12 @@ pub struct StoryRuntimeProjectionResponse {
pub toast: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StoryRuntimeMutationResponse {
pub projection: StoryRuntimeProjectionResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StoryBattleRewardItemRequest {
@@ -476,6 +521,11 @@ mod tests {
created_at: "2.000000Z".to_string(),
}],
server_version: 2,
game_state: json!({
"runtimeSessionId": "runtime_1",
"storySessionId": "storysess_1",
"currentScene": "Story"
}),
actor: StoryRuntimeActorProjection {
hp: 32,
max_hp: 40,
@@ -516,6 +566,7 @@ mod tests {
json!("storysess_1")
);
assert_eq!(payload["serverVersion"], json!(2));
assert_eq!(payload["gameState"]["storySessionId"], json!("storysess_1"));
assert_eq!(payload["actor"]["maxHp"], json!(40));
assert_eq!(
payload["inventory"]["backpackItems"][0]["name"],
@@ -527,6 +578,69 @@ mod tests {
assert!(payload.get("presentation").is_none());
}
#[test]
fn story_runtime_mutation_response_wraps_projection_only() {
let payload = serde_json::to_value(StoryRuntimeMutationResponse {
projection: StoryRuntimeProjectionResponse {
story_session: StorySessionPayload {
story_session_id: "storysess_1".to_string(),
runtime_session_id: "runtime_1".to_string(),
actor_user_id: "user_1".to_string(),
world_profile_id: "profile_1".to_string(),
initial_prompt: "进入营地".to_string(),
opening_summary: Some("营地开场".to_string()),
latest_narrative_text: "营火还亮着。".to_string(),
latest_choice_function_id: None,
status: "active".to_string(),
version: 1,
created_at: "1.000000Z".to_string(),
updated_at: "1.000000Z".to_string(),
},
story_events: Vec::new(),
server_version: 1,
game_state: json!({
"runtimeSessionId": "runtime_1",
"storySessionId": "storysess_1",
"currentScene": "Story"
}),
actor: StoryRuntimeActorProjection {
hp: 40,
max_hp: 40,
mana: 20,
max_mana: 20,
currency: 0,
currency_text: "0 铜钱".to_string(),
},
inventory: StoryRuntimeInventoryProjection {
backpack_items: Vec::new(),
equipment_slots: Vec::new(),
forge_recipes: Vec::new(),
},
options: Vec::new(),
status: StoryRuntimeStatusProjection {
in_battle: false,
npc_interaction_active: false,
current_encounter_id: None,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
current_narrative_text: Some("营火还亮着。".to_string()),
action_result_text: None,
toast: None,
},
})
.expect("payload should serialize");
assert_eq!(
payload["projection"]["storySession"]["storySessionId"],
json!("storysess_1")
);
assert_eq!(payload["projection"]["serverVersion"], json!(1));
assert!(payload.get("snapshot").is_none());
assert!(payload.get("viewModel").is_none());
assert!(payload.get("presentation").is_none());
}
#[test]
fn story_battle_responses_use_story_contract_shape() {
let battle_state = StoryBattleStatePayload {