Close DDD refactor and remove generated asset proxy
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user