This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -22,6 +22,27 @@ pub struct RuntimeStoryStateResolveRequest {
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 {
@@ -66,7 +87,13 @@ impl Default for RuntimeStoryAiRequestOptions {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryAiRequest {
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub client_version: Option<u32>,
#[serde(default)]
pub world_type: String,
#[serde(default)]
pub character: Value,
#[serde(default)]
pub monsters: Vec<Value>,
@@ -74,9 +101,16 @@ pub struct RuntimeStoryAiRequest {
pub history: Vec<Value>,
#[serde(default)]
pub choice: String,
#[serde(default)]
pub context: Value,
#[serde(default)]
pub request_options: RuntimeStoryAiRequestOptions,
#[serde(default)]
pub last_function_id: Option<String>,
#[serde(default)]
pub observe_signs_requested: bool,
#[serde(default)]
pub recent_action_result: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -163,6 +197,130 @@ pub struct RuntimeStoryStatusViewModel {
pub current_npc_battle_outcome: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryInventoryActionView {
pub function_id: String,
pub action_text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryInventoryItemActionsView {
#[serde(rename = "use")]
pub use_item: RuntimeStoryInventoryActionView,
pub equip: RuntimeStoryInventoryActionView,
pub dismantle: RuntimeStoryInventoryActionView,
pub reforge: RuntimeStoryInventoryActionView,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryInventoryItemView {
pub item: Value,
pub actions: RuntimeStoryInventoryItemActionsView,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryEquipmentSlotView {
pub slot_id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item: Option<Value>,
pub unequip: RuntimeStoryInventoryActionView,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryForgeRequirementView {
pub id: String,
pub label: String,
pub quantity: i32,
pub owned: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryForgeRecipeView {
pub id: String,
pub name: String,
pub kind: String,
pub description: String,
pub result_label: String,
pub currency_cost: i32,
pub currency_text: String,
pub requirements: Vec<RuntimeStoryForgeRequirementView>,
pub can_craft: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled_reason: Option<String>,
pub action: RuntimeStoryInventoryActionView,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryInventoryViewModel {
pub player_currency: i32,
pub currency_text: String,
pub in_battle: bool,
pub backpack_items: Vec<RuntimeStoryInventoryItemView>,
pub equipment_slots: Vec<RuntimeStoryEquipmentSlotView>,
pub forge_recipes: Vec<RuntimeStoryForgeRecipeView>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeNpcTradeItemView {
pub item_id: String,
pub item: Value,
pub mode: String,
pub unit_price: i32,
pub max_quantity: i32,
pub can_submit: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeNpcGiftItemView {
pub item_id: String,
pub item: Value,
pub affinity_gain: i32,
pub can_submit: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeNpcTradeView {
pub buy_items: Vec<RuntimeNpcTradeItemView>,
pub sell_items: Vec<RuntimeNpcTradeItemView>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeNpcGiftView {
pub items: Vec<RuntimeNpcGiftItemView>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeNpcInteractionView {
pub npc_id: String,
pub npc_name: String,
pub player_currency: i32,
pub currency_name: String,
pub trade: RuntimeNpcTradeView,
pub gift: RuntimeNpcGiftView,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeBattlePresentation {
@@ -185,8 +343,11 @@ pub struct RuntimeStoryViewModel {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encounter: Option<RuntimeStoryEncounterViewModel>,
pub companions: Vec<RuntimeStoryCompanionViewModel>,
pub inventory: RuntimeStoryInventoryViewModel,
pub available_options: Vec<RuntimeStoryOptionView>,
pub status: RuntimeStoryStatusViewModel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub npc_interaction: Option<RuntimeNpcInteractionView>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -311,6 +472,23 @@ mod tests {
);
}
#[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]
fn runtime_story_ai_request_defaults_optional_arrays() {
let payload: RuntimeStoryAiRequest = serde_json::from_value(json!({
@@ -326,6 +504,33 @@ mod tests {
assert!(payload.request_options.available_options.is_empty());
}
#[test]
fn runtime_story_ai_request_accepts_session_only_payload() {
let payload: RuntimeStoryAiRequest = serde_json::from_value(json!({
"sessionId": "runtime-main",
"clientVersion": 3,
"choice": "继续向前",
"lastFunctionId": "idle_explore_forward",
"requestOptions": {
"optionCatalog": [{
"functionId": "idle_observe_signs",
"actionText": "观察周围迹象"
}]
}
}))
.expect("payload should deserialize");
assert_eq!(payload.session_id.as_deref(), Some("runtime-main"));
assert_eq!(payload.client_version, Some(3));
assert_eq!(payload.world_type, "");
assert_eq!(payload.context, Value::Null);
assert_eq!(
payload.last_function_id.as_deref(),
Some("idle_explore_forward")
);
assert_eq!(payload.request_options.option_catalog.len(), 1);
}
#[test]
fn runtime_story_action_response_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryActionResponse {
@@ -353,6 +558,87 @@ mod tests {
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()),
},
},
}],
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(),
@@ -373,6 +659,47 @@ mod tests {
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": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
affinity_gain: 10,
can_submit: true,
reason: None,
}],
},
}),
},
presentation: RuntimeStoryPresentation {
action_text: "".to_string(),
@@ -419,6 +746,14 @@ mod tests {
payload["viewModel"]["availableOptions"][0]["interaction"]["npcId"],
json!("npc_camp_firekeeper")
);
assert_eq!(
payload["viewModel"]["inventory"]["backpackItems"][0]["actions"]["use"]["functionId"],
json!("inventory_use")
);
assert_eq!(
payload["viewModel"]["inventory"]["forgeRecipes"][0]["canCraft"],
json!(false)
);
assert_eq!(
payload["presentation"]["storyText"],
json!("守火人抬眼看了你一瞬,示意你把想问的话继续说完。")