Files
Genarrative/server-rs/crates/module-runtime-story-compat/src/forge_actions.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

221 lines
8.3 KiB
Rust

use serde_json::Value;
use shared_contracts::runtime_story::RuntimeStoryActionRequest;
use crate::{
StoryResolution, add_inventory_items_to_list, build_current_build_toast, current_world_type,
ensure_inventory_action_available, find_player_inventory_entry, read_i32_field,
read_inventory_item_name, read_optional_string_field, read_player_inventory_values,
remove_inventory_item_from_list, resolve_action_text, resolve_equipment_slot_for_item,
write_i32_field, write_player_inventory_values,
};
use super::forge::{
apply_forge_requirements_if_possible, build_dismantle_outputs, build_forge_recipe_result_item,
build_forge_success_text, build_reforged_item, forge_recipe_definition, format_currency_text,
reforge_cost_definition,
};
/// 锻造动作编排已经不再依赖 `api-server` 的 HTTP 边界。
///
/// 这里继续沿用 compat 快照态结算,后续可直接被 `api-server` 外壳或真相态桥接层复用。
pub fn resolve_forge_craft_action(
game_state: &mut Value,
request: &RuntimeStoryActionRequest,
) -> Result<StoryResolution, String> {
ensure_inventory_action_available(
game_state,
"缺少玩家角色,无法执行锻造配方。",
"战斗中无法使用工坊。",
)?;
let recipe_id = request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "recipeId"))
.or_else(|| request.action.target_id.clone())
.ok_or_else(|| "forge_craft 缺少 recipeId".to_string())?;
let recipe = forge_recipe_definition(recipe_id.as_str())
.ok_or_else(|| "未找到目标锻造配方。".to_string())?;
let player_currency = read_i32_field(game_state, "playerCurrency").unwrap_or(0);
if player_currency < recipe.currency_cost {
return Err(format!("{} 当前材料或货币不足。", recipe.name));
}
let current_inventory = read_player_inventory_values(game_state);
let consumed_inventory = apply_forge_requirements_if_possible(
current_inventory.as_slice(),
recipe.requirements.as_slice(),
)
.ok_or_else(|| format!("{} 当前材料或货币不足。", recipe.name))?;
let created_item = build_forge_recipe_result_item(
game_state,
recipe.id,
current_world_type(game_state).as_deref(),
);
let next_inventory =
add_inventory_items_to_list(consumed_inventory, vec![created_item.clone()]);
write_i32_field(
game_state,
"playerCurrency",
player_currency.saturating_sub(recipe.currency_cost),
);
write_player_inventory_values(game_state, next_inventory);
Ok(StoryResolution {
action_text: resolve_action_text(
&format!("制作{}", read_inventory_item_name(&created_item)),
request,
),
result_text: build_forge_success_text(
"craft",
Some(recipe.name),
None,
Some(read_inventory_item_name(&created_item).as_str()),
&[],
Some(format_currency_text(
recipe.currency_cost,
current_world_type(game_state).as_deref(),
)),
),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: Vec::new(),
battle: None,
toast: Some(build_current_build_toast(game_state)),
})
}
pub fn resolve_forge_dismantle_action(
game_state: &mut Value,
request: &RuntimeStoryActionRequest,
) -> Result<StoryResolution, String> {
ensure_inventory_action_available(
game_state,
"缺少玩家角色,无法执行拆解。",
"战斗中无法执行拆解。",
)?;
let item_id = request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "itemId"))
.or_else(|| request.action.target_id.clone())
.ok_or_else(|| "forge_dismantle 缺少 itemId".to_string())?;
let item = find_player_inventory_entry(game_state, item_id.as_str())
.cloned()
.ok_or_else(|| "未找到可拆解的物品。".to_string())?;
if read_i32_field(&item, "quantity").unwrap_or(0) <= 0 {
return Err("未找到可拆解的物品。".to_string());
}
let outputs = build_dismantle_outputs(game_state, &item)
.ok_or_else(|| format!("{} 当前不支持拆解。", read_inventory_item_name(&item)))?;
let mut next_inventory = read_player_inventory_values(game_state);
next_inventory = remove_inventory_item_from_list(next_inventory, item_id.as_str(), 1);
next_inventory = add_inventory_items_to_list(next_inventory, outputs.clone());
write_player_inventory_values(game_state, next_inventory);
let output_names = outputs
.iter()
.map(read_inventory_item_name)
.collect::<Vec<_>>();
Ok(StoryResolution {
action_text: resolve_action_text(
&format!("拆解{}", read_inventory_item_name(&item)),
request,
),
result_text: build_forge_success_text(
"dismantle",
None,
Some(read_inventory_item_name(&item).as_str()),
None,
output_names.as_slice(),
None,
),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: Vec::new(),
battle: None,
toast: Some(build_current_build_toast(game_state)),
})
}
pub fn resolve_forge_reforge_action(
game_state: &mut Value,
request: &RuntimeStoryActionRequest,
) -> Result<StoryResolution, String> {
ensure_inventory_action_available(
game_state,
"缺少玩家角色,无法执行重铸。",
"战斗中无法执行重铸。",
)?;
let item_id = request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "itemId"))
.or_else(|| request.action.target_id.clone())
.ok_or_else(|| "forge_reforge 缺少 itemId".to_string())?;
let item = find_player_inventory_entry(game_state, item_id.as_str())
.cloned()
.ok_or_else(|| "未找到可重铸的物品。".to_string())?;
if read_i32_field(&item, "quantity").unwrap_or(0) <= 0 {
return Err("未找到可重铸的物品。".to_string());
}
let slot_id = resolve_equipment_slot_for_item(&item);
let reforge_cost = reforge_cost_definition(slot_id);
let player_currency = read_i32_field(game_state, "playerCurrency").unwrap_or(0);
if player_currency < reforge_cost.currency_cost {
return Err(format!(
"{} 当前不满足重铸条件。",
read_inventory_item_name(&item)
));
}
let reforged_item = build_reforged_item(game_state, &item)
.ok_or_else(|| format!("{} 当前不满足重铸条件。", read_inventory_item_name(&item)))?;
let base_inventory = remove_inventory_item_from_list(
read_player_inventory_values(game_state),
item_id.as_str(),
1,
);
let consumed_inventory = apply_forge_requirements_if_possible(
base_inventory.as_slice(),
reforge_cost.requirements.as_slice(),
)
.ok_or_else(|| format!("{} 当前不满足重铸条件。", read_inventory_item_name(&item)))?;
let next_inventory =
add_inventory_items_to_list(consumed_inventory, vec![reforged_item.clone()]);
write_player_inventory_values(game_state, next_inventory);
write_i32_field(
game_state,
"playerCurrency",
player_currency.saturating_sub(reforge_cost.currency_cost),
);
Ok(StoryResolution {
action_text: resolve_action_text(
&format!("重铸{}", read_inventory_item_name(&item)),
request,
),
result_text: build_forge_success_text(
"reforge",
None,
Some(read_inventory_item_name(&item).as_str()),
Some(read_inventory_item_name(&reforged_item).as_str()),
&[],
Some(format_currency_text(
reforge_cost.currency_cost,
current_world_type(game_state).as_deref(),
)),
),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: Vec::new(),
battle: None,
toast: Some(build_current_build_toast(game_state)),
})
}