use serde_json::{Value, json}; use crate::{ equipment_slot_label, item_rarity_key, read_array_field, read_field, read_i32_field, read_inventory_item_name, read_optional_string_field, read_u32_field, remove_inventory_item_from_list, resolve_equipment_slot_for_item, }; /// 这批定义只服务 runtime story compat 的确定性锻造链。 /// /// 当前仍然保持旧快照态结算口径,不引入 HTTP / AppState / 持久化边界。 pub(crate) struct ForgeRequirementDefinition { pub(crate) id: &'static str, pub(crate) label: &'static str, pub(crate) quantity: i32, pub(crate) matcher: ForgeRequirementMatcher, } #[derive(Clone, Copy)] pub(crate) enum ForgeRequirementMatcher { Named(&'static str), TaggedMaterial(&'static str), AnyMaterial, } pub(crate) struct ForgeRecipeDefinition { pub(crate) id: &'static str, pub(crate) name: &'static str, pub(crate) kind: &'static str, pub(crate) description: &'static str, pub(crate) result_label: &'static str, pub(crate) currency_cost: i32, pub(crate) requirements: Vec, } pub(crate) struct ReforgeCostDefinition { pub(crate) currency_cost: i32, pub(crate) requirements: Vec, } pub(crate) fn forge_recipe_definition(recipe_id: &str) -> Option { forge_recipe_definitions() .into_iter() .find(|recipe| recipe.id == recipe_id) } pub(crate) fn forge_recipe_definitions() -> Vec { vec![ ForgeRecipeDefinition { id: "synthesis-refined-ingot", name: "压炼锭材", kind: "synthesis", description: "把零散残片和基础材料压成稳定可用的金属锭材。", result_label: "精炼锭材", currency_cost: 18, requirements: vec![ForgeRequirementDefinition { id: "material:any", label: "任意材料", quantity: 3, matcher: ForgeRequirementMatcher::AnyMaterial, }], }, ForgeRecipeDefinition { id: "synthesis-condensed-silk", name: "凝光纺丝", kind: "synthesis", description: "用灵性残材与粉末纺出适合饰品锻造的凝光纱。", result_label: "凝光纱", currency_cost: 24, requirements: vec![ ForgeRequirementDefinition { id: "material:any", label: "任意材料", quantity: 2, matcher: ForgeRequirementMatcher::AnyMaterial, }, ForgeRequirementDefinition { id: "tag:mana", label: "含法力标签材料", quantity: 1, matcher: ForgeRequirementMatcher::TaggedMaterial("mana"), }, ], }, ForgeRecipeDefinition { id: "forge-duelist-blade", name: "锻造 百炼追风剑", kind: "forge", description: "围绕快剑、突进、追击构筑的轻灵主武器。", result_label: "百炼追风剑", currency_cost: 72, requirements: vec![ ForgeRequirementDefinition { id: "name:精炼锭材", label: "精炼锭材", quantity: 2, matcher: ForgeRequirementMatcher::Named("精炼锭材"), }, ForgeRequirementDefinition { id: "name:快剑精粹", label: "快剑精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("快剑精粹"), }, ForgeRequirementDefinition { id: "name:突进精粹", label: "突进精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("突进精粹"), }, ], }, ForgeRecipeDefinition { id: "forge-ward-armor", name: "锻造 镇岳护甲", kind: "forge", description: "面向前排承压的护甲,适合守御与护体构筑。", result_label: "镇岳护甲", currency_cost: 78, requirements: vec![ ForgeRequirementDefinition { id: "name:精炼锭材", label: "精炼锭材", quantity: 2, matcher: ForgeRequirementMatcher::Named("精炼锭材"), }, ForgeRequirementDefinition { id: "name:守御精粹", label: "守御精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("守御精粹"), }, ForgeRequirementDefinition { id: "name:护体精粹", label: "护体精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("护体精粹"), }, ], }, ForgeRecipeDefinition { id: "forge-thunder-relic", name: "锻造 雷纹灵坠", kind: "forge", description: "为法修、雷法、过载 build 提供资源与爆发补强。", result_label: "雷纹灵坠", currency_cost: 88, requirements: vec![ ForgeRequirementDefinition { id: "name:凝光纱", label: "凝光纱", quantity: 2, matcher: ForgeRequirementMatcher::Named("凝光纱"), }, ForgeRequirementDefinition { id: "name:法力精粹", label: "法力精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("法力精粹"), }, ForgeRequirementDefinition { id: "name:雷法精粹", label: "雷法精粹", quantity: 1, matcher: ForgeRequirementMatcher::Named("雷法精粹"), }, ], }, ] } pub(crate) fn reforge_cost_definition(slot_id: Option<&str>) -> ReforgeCostDefinition { if slot_id == Some("relic") { return ReforgeCostDefinition { currency_cost: 52, requirements: vec![ForgeRequirementDefinition { id: "name:凝光纱", label: "凝光纱", quantity: 1, matcher: ForgeRequirementMatcher::Named("凝光纱"), }], }; } ReforgeCostDefinition { currency_cost: 46, requirements: vec![ForgeRequirementDefinition { id: "name:精炼锭材", label: "精炼锭材", quantity: 1, matcher: ForgeRequirementMatcher::Named("精炼锭材"), }], } } fn forge_requirement_matches(item: &Value, requirement: &ForgeRequirementDefinition) -> bool { match requirement.matcher { ForgeRequirementMatcher::Named(name) => { read_optional_string_field(item, "name").as_deref() == Some(name) } ForgeRequirementMatcher::TaggedMaterial(tag) => { is_material_item(item) && read_array_field(item, "tags") .into_iter() .filter_map(Value::as_str) .any(|item_tag| forge_tag_matches(item_tag, tag)) } ForgeRequirementMatcher::AnyMaterial => is_material_item(item), } } pub(crate) fn count_matching_forge_requirement( inventory: &[Value], requirement: &ForgeRequirementDefinition, ) -> i32 { inventory .iter() .filter(|item| forge_requirement_matches(item, requirement)) .map(|item| read_i32_field(item, "quantity").unwrap_or(0).max(0)) .sum() } pub(crate) fn apply_forge_requirements_if_possible( inventory: &[Value], requirements: &[ForgeRequirementDefinition], ) -> Option> { let mut next_inventory = inventory.to_vec(); for requirement in requirements { let mut remaining = requirement.quantity.max(0); let snapshot = next_inventory.clone(); for item in snapshot { if remaining <= 0 { break; } if !forge_requirement_matches(&item, requirement) { continue; } let item_id = read_optional_string_field(&item, "id")?; let item_quantity = read_i32_field(&item, "quantity").unwrap_or(0).max(0); let consumed = remaining.min(item_quantity); next_inventory = remove_inventory_item_from_list(next_inventory, item_id.as_str(), consumed); remaining -= consumed; } if remaining > 0 { return None; } } Some(next_inventory) } fn is_material_item(item: &Value) -> bool { read_array_field(item, "tags") .into_iter() .filter_map(Value::as_str) .any(|tag| tag == "material") || read_optional_string_field(item, "category") .is_some_and(|category| category.contains("材料")) } fn forge_tag_matches(item_tag: &str, expected_tag: &str) -> bool { item_tag == expected_tag || (expected_tag == "mana" && item_tag == "法力") } pub fn build_runtime_material_item( game_state: &Value, name: &str, quantity: i32, tags: &[&str], rarity: &str, ) -> Value { let mut all_tags = vec!["material".to_string()]; all_tags.extend(tags.iter().map(|tag| (*tag).to_string())); json!({ "id": generate_runtime_item_id(game_state, format!("forge-material:{name}").as_str()), "category": "材料", "name": name, "quantity": quantity.max(1), "rarity": rarity, "tags": all_tags, "buildProfile": { "role": "工巧", "tags": tags, "synergy": tags, "forgeRank": 0 } }) } pub fn build_runtime_equipment_item( game_state: &Value, name: &str, slot_id: &str, rarity: &str, description: &str, role: &str, tags: &[&str], synergy: &[&str], stat_profile: Value, ) -> Value { let slot_tag = match slot_id { "weapon" => "weapon", "armor" => "armor", _ => "relic", }; let mut next_tags = vec![slot_tag.to_string()]; next_tags.extend(tags.iter().map(|tag| (*tag).to_string())); json!({ "id": generate_runtime_item_id(game_state, format!("forge-equip:{name}").as_str()), "category": equipment_slot_label(slot_id), "name": name, "description": description, "quantity": 1, "rarity": rarity, "tags": next_tags, "equipmentSlotId": slot_id, "statProfile": stat_profile, "buildProfile": { "role": role, "tags": tags, "synergy": synergy, "forgeRank": 1 } }) } pub(crate) fn build_forge_recipe_result_item( game_state: &Value, recipe_id: &str, _world_type: Option<&str>, ) -> Value { match recipe_id { "synthesis-refined-ingot" => { build_runtime_material_item(game_state, "精炼锭材", 1, &["工巧", "守御"], "rare") } "synthesis-condensed-silk" => { build_runtime_material_item(game_state, "凝光纱", 1, &["工巧", "法力"], "rare") } "forge-duelist-blade" => build_runtime_equipment_item( game_state, "百炼追风剑", "weapon", "epic", "为快剑与追身构筑准备的锻造兵刃。", "快剑", &["快剑", "突进", "追击"], &["快剑", "突进", "追击"], json!({ "maxManaBonus": 10, "outgoingDamageBonus": 0.20 }), ), "forge-ward-armor" => build_runtime_equipment_item( game_state, "镇岳护甲", "armor", "epic", "厚重但稳定的护甲套件,适合顶住正面压力后再伺机反打。", "守御", &["守御", "护体", "先锋"], &["守御", "护体", "先锋"], json!({ "maxHpBonus": 56, "maxManaBonus": 8, "outgoingDamageBonus": 0.08, "incomingDamageMultiplier": 0.84 }), ), "forge-thunder-relic" => build_runtime_equipment_item( game_state, "雷纹灵坠", "relic", "epic", "内封雷纹与灵引回路的饰品,能在短窗口内快速放大法术节奏。", "法修", &["法修", "雷法", "过载"], &["法修", "雷法", "过载"], json!({ "maxHpBonus": 8, "maxManaBonus": 42, "outgoingDamageBonus": 0.14, "incomingDamageMultiplier": 0.92 }), ), _ => build_runtime_material_item(game_state, "临时锻造产物", 1, &["工巧"], "common"), } } fn build_tag_essence_item(game_state: &Value, tag: &str) -> Value { build_runtime_material_item( game_state, format!("{tag}精粹").as_str(), 1, &[tag, "工巧"], "rare", ) } pub(crate) fn build_dismantle_outputs(game_state: &Value, item: &Value) -> Option> { let slot_id = resolve_equipment_slot_for_item(item); if slot_id.is_none() && read_field(item, "buildProfile").is_none() { return None; } let rarity_scale = match item_rarity_key(item).as_str() { "legendary" => 5, "epic" => 4, "rare" => 3, "uncommon" => 2, _ => 1, }; let mut outputs = Vec::new(); match slot_id { Some("weapon") => outputs.push(build_runtime_material_item( game_state, "武器残片", rarity_scale, &["工巧", "重击"], "uncommon", )), Some("armor") => outputs.push(build_runtime_material_item( game_state, "甲片", rarity_scale, &["工巧", "守御"], "uncommon", )), Some("relic") => outputs.push(build_runtime_material_item( game_state, "灵饰碎片", rarity_scale, &["工巧", "法力"], "uncommon", )), _ => outputs.push(build_runtime_material_item( game_state, "零散材料", ((rarity_scale + 1) / 2).max(1), &["工巧"], "uncommon", )), } let mut build_tags = read_field(item, "buildProfile") .map(|profile| { let mut tags = read_array_field(profile, "tags") .into_iter() .filter_map(Value::as_str) .map(str::to_string) .collect::>(); if let Some(role) = read_optional_string_field(profile, "role") { tags.push(role); } tags }) .unwrap_or_default(); build_tags.sort(); build_tags.dedup(); let tag_limit = if item_rarity_key(item) == "legendary" { 3 } else { 2 }; for tag in build_tags.into_iter().take(tag_limit) { outputs.push(build_tag_essence_item(game_state, tag.as_str())); } Some(outputs) } pub(crate) fn build_reforged_item(game_state: &Value, item: &Value) -> Option { let slot_id = resolve_equipment_slot_for_item(item)?; let build_profile = read_field(item, "buildProfile")?; let mut next_tags = read_array_field(build_profile, "tags") .into_iter() .filter_map(Value::as_str) .map(str::to_string) .collect::>(); let extra_tag = match slot_id { "weapon" => "追击", "armor" => "护体", _ => "法力", }; next_tags.push(extra_tag.to_string()); next_tags.sort(); next_tags.dedup(); next_tags.truncate(3); let source_name = read_inventory_item_name(item); let next_name = if source_name.contains('·') && source_name.contains("重铸") { source_name.clone() } else { format!("{source_name}·重铸") }; let stat_profile = read_field(item, "statProfile"); let outgoing_damage_bonus = stat_profile .and_then(|profile| read_field(profile, "outgoingDamageBonus")) .and_then(Value::as_f64) .unwrap_or(0.0); let incoming_damage_multiplier = stat_profile .and_then(|profile| read_field(profile, "incomingDamageMultiplier")) .and_then(Value::as_f64); let current_forge_rank = read_i32_field(build_profile, "forgeRank").unwrap_or(0); let mut tags = read_array_field(item, "tags") .into_iter() .filter_map(Value::as_str) .map(str::to_string) .collect::>(); tags.sort(); tags.dedup(); Some(json!({ "id": generate_runtime_item_id(game_state, format!("reforge:{source_name}").as_str()), "category": read_optional_string_field(item, "category") .unwrap_or_else(|| equipment_slot_label(slot_id).to_string()), "name": next_name, "description": read_optional_string_field(item, "description"), "quantity": 1, "rarity": item_rarity_key(item), "tags": tags, "equipmentSlotId": slot_id, "statProfile": { "maxHpBonus": stat_profile .and_then(|profile| read_i32_field(profile, "maxHpBonus")) .unwrap_or(0) + if slot_id == "armor" { 10 } else { 4 }, "maxManaBonus": stat_profile .and_then(|profile| read_i32_field(profile, "maxManaBonus")) .unwrap_or(0) + if slot_id == "relic" { 10 } else { 4 }, "outgoingDamageBonus": ((outgoing_damage_bonus + 0.03) * 1000.0).round() / 1000.0, "incomingDamageMultiplier": if let Some(multiplier) = incoming_damage_multiplier { (((multiplier - 0.03).max(0.72)) * 1000.0).round() / 1000.0 } else if slot_id == "armor" { 0.94 } else { 0.97 } }, "buildProfile": { "role": read_optional_string_field(build_profile, "role"), "tags": next_tags, "synergy": read_array_field(build_profile, "tags") .into_iter() .filter_map(Value::as_str) .map(str::to_string) .chain(std::iter::once(extra_tag.to_string())) .collect::>() .into_iter() .collect::>(), "forgeRank": current_forge_rank + 1 } })) } pub(crate) fn build_forge_success_text( action: &str, recipe_name: Option<&str>, source_item_name: Option<&str>, created_item_name: Option<&str>, output_names: &[String], currency_text: Option, ) -> String { match action { "craft" => format!( "你在工坊中完成了{},获得了{}{}。", recipe_name.unwrap_or("目标配方"), created_item_name.unwrap_or("目标物品"), currency_text .map(|text| format!(",并支付了{text}")) .unwrap_or_default() ), "reforge" => format!( "你消耗材料重新淬炼了{},最终得到{}{}。", source_item_name.unwrap_or("目标物品"), created_item_name.unwrap_or("重铸产物"), currency_text .map(|text| format!(",并支付了{text}")) .unwrap_or_default() ), _ => format!( "你拆解了{},回收出{}。", source_item_name.unwrap_or("目标物品"), output_names.join("、") ), } } pub fn format_currency_text(value: i32, world_type: Option<&str>) -> String { let currency_name = match world_type { Some("XIANXIA") => "灵石", Some("WUXIA") => "铜钱", _ => "钱币", }; format!("{value} {currency_name}") } fn generate_runtime_item_id(game_state: &Value, prefix: &str) -> String { let version = read_u32_field(game_state, "runtimeActionVersion").unwrap_or(0); let inventory_len = read_array_field(game_state, "playerInventory").len(); format!("{prefix}:{version}:{inventory_len}") }