1
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use shared_contracts::runtime_story::{
|
||||
RuntimeNpcGiftItemView, RuntimeNpcGiftView, RuntimeNpcInteractionView, RuntimeNpcTradeItemView,
|
||||
RuntimeNpcTradeView,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
MAX_TASK5_COMPANIONS, ensure_json_object, item_rarity_key, normalize_required_string,
|
||||
read_array_field, read_i32_field, read_inventory_item_name, read_optional_string_field,
|
||||
read_array_field, read_bool_field, read_i32_field, read_inventory_item_name, read_object_field,
|
||||
read_optional_string_field, read_required_string_field,
|
||||
};
|
||||
|
||||
pub fn resolve_npc_gift_affinity_gain(item: &Value) -> i32 {
|
||||
@@ -142,6 +148,177 @@ pub fn trade_quantity_suffix(quantity: i32) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn currency_name_for_world(world_type: Option<&str>) -> String {
|
||||
match world_type {
|
||||
Some("XIANXIA") => "灵石",
|
||||
Some("WUXIA") => "铜钱",
|
||||
_ => "钱币",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn read_runtime_npc_state<'a>(
|
||||
game_state: &'a Value,
|
||||
encounter_id: &str,
|
||||
npc_name: &str,
|
||||
) -> Option<&'a Value> {
|
||||
let npc_states = read_object_field(game_state, "npcStates")?;
|
||||
|
||||
npc_states
|
||||
.get(encounter_id)
|
||||
.or_else(|| npc_states.get(npc_name))
|
||||
}
|
||||
|
||||
fn read_item_id(item: &Value) -> Option<String> {
|
||||
read_required_string_field(item, "id")
|
||||
}
|
||||
|
||||
fn sanitize_item_for_view(item: &Value) -> Value {
|
||||
let mut item = item.clone();
|
||||
if let Some(object) = item.as_object_mut() {
|
||||
object.retain(|key, _| key != "__internal");
|
||||
}
|
||||
item
|
||||
}
|
||||
|
||||
fn build_trade_item_view(params: BuildTradeItemViewParams<'_>) -> RuntimeNpcTradeItemView {
|
||||
let quantity = read_i32_field(params.item, "quantity").unwrap_or(0).max(0);
|
||||
let unit_price = match params.mode {
|
||||
"buy" => npc_purchase_price(params.item, params.affinity),
|
||||
_ => npc_buyback_price(params.item, params.affinity),
|
||||
};
|
||||
let mut reason = None;
|
||||
if quantity <= 0 {
|
||||
reason = Some(if params.mode == "buy" {
|
||||
"NPC 库存不足。".to_string()
|
||||
} else {
|
||||
"背包数量不足。".to_string()
|
||||
});
|
||||
} else if params.mode == "buy" && params.player_currency < unit_price {
|
||||
reason = Some("当前钱币不足。".to_string());
|
||||
}
|
||||
|
||||
RuntimeNpcTradeItemView {
|
||||
item_id: params.item_id.to_string(),
|
||||
item: sanitize_item_for_view(params.item),
|
||||
mode: params.mode.to_string(),
|
||||
unit_price,
|
||||
max_quantity: quantity,
|
||||
can_submit: reason.is_none(),
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
struct BuildTradeItemViewParams<'a> {
|
||||
item_id: &'a str,
|
||||
item: &'a Value,
|
||||
mode: &'a str,
|
||||
affinity: i32,
|
||||
player_currency: i32,
|
||||
}
|
||||
|
||||
/// 编译 NPC 交易 / 送礼展示用 view。
|
||||
///
|
||||
/// 中文注释:这份 view 只服务前端展示与按钮状态,正式结算仍会在
|
||||
/// `resolve_npc_trade_action` / `resolve_npc_gift_action` 中重新校验。
|
||||
pub fn build_runtime_npc_interaction_view(game_state: &Value) -> Option<RuntimeNpcInteractionView> {
|
||||
if read_bool_field(game_state, "inBattle").unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
if !read_bool_field(game_state, "npcInteractionActive").unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let encounter = read_object_field(game_state, "currentEncounter")?;
|
||||
if read_required_string_field(encounter, "kind").as_deref() != Some("npc") {
|
||||
return None;
|
||||
}
|
||||
let npc_name = read_optional_string_field(encounter, "npcName")
|
||||
.or_else(|| read_optional_string_field(encounter, "name"))
|
||||
.unwrap_or_else(|| "当前角色".to_string());
|
||||
let npc_id = read_optional_string_field(encounter, "id").unwrap_or_else(|| npc_name.clone());
|
||||
let npc_state = read_runtime_npc_state(game_state, npc_id.as_str(), npc_name.as_str())?;
|
||||
let affinity = read_i32_field(npc_state, "affinity").unwrap_or(0);
|
||||
let player_currency = read_i32_field(game_state, "playerCurrency")
|
||||
.unwrap_or(0)
|
||||
.max(0);
|
||||
let currency_name =
|
||||
currency_name_for_world(read_optional_string_field(game_state, "worldType").as_deref());
|
||||
|
||||
let buy_items = read_array_field(npc_state, "inventory")
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
let item_id = read_item_id(item)?;
|
||||
Some(build_trade_item_view(BuildTradeItemViewParams {
|
||||
item_id: item_id.as_str(),
|
||||
item,
|
||||
mode: "buy",
|
||||
affinity,
|
||||
player_currency,
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let sell_items = read_array_field(game_state, "playerInventory")
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
let item_id = read_item_id(item)?;
|
||||
Some(build_trade_item_view(BuildTradeItemViewParams {
|
||||
item_id: item_id.as_str(),
|
||||
item,
|
||||
mode: "sell",
|
||||
affinity,
|
||||
player_currency,
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let gift_items = read_array_field(game_state, "playerInventory")
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
let item_id = read_item_id(item)?;
|
||||
let quantity = read_i32_field(item, "quantity").unwrap_or(0).max(0);
|
||||
let reason = if quantity <= 0 {
|
||||
Some("背包里没有这件可赠送的物品。".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(RuntimeNpcGiftItemView {
|
||||
item_id,
|
||||
item: sanitize_item_for_view(item),
|
||||
affinity_gain: resolve_npc_gift_affinity_gain(item),
|
||||
can_submit: reason.is_none(),
|
||||
reason,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(RuntimeNpcInteractionView {
|
||||
npc_id,
|
||||
npc_name,
|
||||
player_currency,
|
||||
currency_name,
|
||||
trade: RuntimeNpcTradeView {
|
||||
buy_items,
|
||||
sell_items,
|
||||
},
|
||||
gift: RuntimeNpcGiftView { items: gift_items },
|
||||
})
|
||||
}
|
||||
|
||||
/// 将 NPC 交互 view 写入快照 JSON,方便旧前端在 hydrated snapshot 上直接读取。
|
||||
pub fn write_runtime_npc_interaction_view(game_state: &mut Value) {
|
||||
let view = build_runtime_npc_interaction_view(game_state);
|
||||
let root = ensure_json_object(game_state);
|
||||
match view {
|
||||
Some(view) => {
|
||||
let value = serde_json::to_value(view).unwrap_or_else(|_| Value::Object(Map::new()));
|
||||
root.insert("runtimeNpcInteraction".to_string(), value);
|
||||
}
|
||||
None => {
|
||||
root.remove("runtimeNpcInteraction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_companion_if_absent(
|
||||
game_state: &mut Value,
|
||||
npc_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user