Files
Genarrative/server-rs/crates/module-runtime-story-compat/src/npc_support.rs
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

217 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde_json::{Value, json};
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,
};
pub fn resolve_npc_gift_affinity_gain(item: &Value) -> i32 {
let rarity_score = match item_rarity_key(item).as_str() {
"legendary" => 5,
"epic" => 4,
"rare" => 3,
"uncommon" => 2,
_ => 1,
};
let tags = read_array_field(item, "tags")
.into_iter()
.filter_map(|tag| tag.as_str().map(|value| value.to_string()))
.collect::<Vec<_>>();
let mana_bonus = if tags.iter().any(|tag| tag == "mana") {
3
} else {
0
};
let healing_bonus = if tags.iter().any(|tag| tag == "healing") {
3
} else {
0
};
(4 + rarity_score * 3 + mana_bonus + healing_bonus).min(24)
}
pub fn build_npc_gift_result_text(
npc_name: &str,
item: &Value,
affinity_gain: i32,
next_affinity: i32,
) -> String {
let shift_text = if affinity_gain >= 12 {
"态度一下子软化了许多"
} else if affinity_gain >= 8 {
"态度明显和缓下来"
} else if affinity_gain >= 5 {
"态度比先前亲近了一些"
} else {
"态度略微放松了些"
};
let affinity_text = if next_affinity >= 90 {
"对你高度信赖,言谈间明显亲近,几乎已经把你当成自己人。"
} else if next_affinity >= 60 {
"对你已经建立起稳固信任,愿意进一步合作。"
} else if next_affinity >= 30 {
"对你的态度明显友善了许多,也更愿意正常交流。"
} else if next_affinity >= 15 {
"戒备开始松动,愿意试探性地配合你的节奏。"
} else if next_affinity >= 0 {
"仍保持明显距离,只会给出谨慎而有限的回应。"
} else {
"关系已经降到冰点,对你几乎不再保留善意。"
};
format!(
"{}收下了{}{}{}",
npc_name,
read_inventory_item_name(item),
shift_text,
affinity_text
)
}
fn inventory_item_value(item: &Value) -> i32 {
if let Some(explicit_value) = read_i32_field(item, "value") {
return explicit_value.max(8);
}
let rarity_base = match item_rarity_key(item).as_str() {
"legendary" => 168,
"epic" => 92,
"rare" => 48,
"uncommon" => 24,
_ => 12,
};
let category = read_optional_string_field(item, "category").unwrap_or_default();
let tags = read_array_field(item, "tags")
.into_iter()
.filter_map(|tag| tag.as_str().map(|value| value.to_string()))
.collect::<Vec<_>>();
let mut value = rarity_base;
if tags.iter().any(|tag| tag == "weapon") {
value += 14;
}
if tags.iter().any(|tag| tag == "armor") {
value += 12;
}
if tags.iter().any(|tag| tag == "relic") {
value += 16;
}
if tags.iter().any(|tag| tag == "mana") {
value += 8;
}
if tags.iter().any(|tag| tag == "healing") {
value += 8;
}
if tags.iter().any(|tag| tag == "material") {
value += 4;
}
if category.contains("专属") {
value += 10;
}
value.max(8)
}
fn discount_tier_for_affinity(affinity: i32) -> i32 {
if affinity >= 90 {
3
} else if affinity >= 60 {
2
} else if affinity >= 30 {
1
} else {
0
}
}
pub fn npc_purchase_price(item: &Value, affinity: i32) -> i32 {
let discount_multiplier = 1.0 - f64::from(discount_tier_for_affinity(affinity)) * 0.08;
(f64::from(inventory_item_value(item)) * discount_multiplier)
.round()
.max(6.0) as i32
}
pub fn npc_buyback_price(item: &Value, affinity: i32) -> i32 {
let buyback_multiplier = 0.4 + f64::from(discount_tier_for_affinity(affinity)) * 0.06;
(f64::from(inventory_item_value(item)) * buyback_multiplier)
.round()
.max(4.0) as i32
}
pub fn trade_quantity_suffix(quantity: i32) -> String {
if quantity > 1 {
format!(" x{quantity}")
} else {
String::new()
}
}
fn add_companion_if_absent(
game_state: &mut Value,
npc_id: &str,
character_id: Option<String>,
joined_at_affinity: i32,
) {
let root = ensure_json_object(game_state);
let companions = root
.entry("companions".to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if !companions.is_array() {
*companions = Value::Array(Vec::new());
}
let items = companions
.as_array_mut()
.expect("companions should be array");
if items
.iter()
.any(|item| read_optional_string_field(item, "npcId").is_some_and(|value| value == npc_id))
{
return;
}
items.push(json!({
"npcId": npc_id,
"characterId": character_id,
"joinedAtAffinity": joined_at_affinity,
}));
}
fn remove_companion_by_npc_id(game_state: &mut Value, npc_id: &str) -> Option<Value> {
let root = ensure_json_object(game_state);
let companions = root
.entry("companions".to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if !companions.is_array() {
*companions = Value::Array(Vec::new());
}
let items = companions
.as_array_mut()
.expect("companions should be array");
let index = items.iter().position(|item| {
read_optional_string_field(item, "npcId").is_some_and(|value| value == npc_id)
})?;
Some(items.remove(index))
}
/// compat bridge 先只维护一个轻量队伍名单,继续复用旧前端的满员换队语义。
pub fn recruit_companion_to_party(
game_state: &mut Value,
npc_id: &str,
joined_at_affinity: i32,
release_npc_id: Option<&str>,
) -> Result<Option<String>, String> {
let companion_count = read_array_field(game_state, "companions").len();
if companion_count < MAX_TASK5_COMPANIONS {
add_companion_if_absent(game_state, npc_id, None, joined_at_affinity);
return Ok(None);
}
let Some(release_npc_id) = release_npc_id.and_then(normalize_required_string) else {
return Err("队伍已满时必须明确指定一名离队同伴".to_string());
};
let released_companion = remove_companion_by_npc_id(game_state, release_npc_id.as_str())
.ok_or_else(|| "指定的离队同伴不存在,无法完成换队招募".to_string())?;
let released_name = read_optional_string_field(&released_companion, "displayName")
.or_else(|| read_optional_string_field(&released_companion, "name"))
.or_else(|| read_optional_string_field(&released_companion, "npcName"))
.unwrap_or(release_npc_id);
add_companion_if_absent(game_state, npc_id, None, joined_at_affinity);
Ok(Some(released_name))
}