598 lines
20 KiB
Rust
598 lines
20 KiB
Rust
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<ForgeRequirementDefinition>,
|
|
}
|
|
|
|
pub(crate) struct ReforgeCostDefinition {
|
|
pub(crate) currency_cost: i32,
|
|
pub(crate) requirements: Vec<ForgeRequirementDefinition>,
|
|
}
|
|
|
|
pub(crate) fn forge_recipe_definition(recipe_id: &str) -> Option<ForgeRecipeDefinition> {
|
|
forge_recipe_definitions()
|
|
.into_iter()
|
|
.find(|recipe| recipe.id == recipe_id)
|
|
}
|
|
|
|
pub(crate) fn forge_recipe_definitions() -> Vec<ForgeRecipeDefinition> {
|
|
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<Vec<Value>> {
|
|
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<Vec<Value>> {
|
|
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::<Vec<_>>();
|
|
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<Value> {
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<std::collections::BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>(),
|
|
"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>,
|
|
) -> 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}")
|
|
}
|