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::>(); 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::>(); 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, 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 { 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, 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)) }