2023-06-12 00:36:55 +10:00
|
|
|
#[double]
|
|
|
|
use crate::db::DBTrans;
|
2023-01-20 23:08:40 +11:00
|
|
|
use crate::{
|
|
|
|
models::{
|
2023-06-12 00:36:55 +10:00
|
|
|
item::{BuffImpact, Item, SkillType, StatType},
|
|
|
|
user::User,
|
|
|
|
},
|
|
|
|
services::combat::{change_health, soak_damage},
|
|
|
|
static_content::{
|
|
|
|
possession_type::{DamageDistribution, DamageType},
|
|
|
|
species::BodyPart,
|
2023-01-20 23:08:40 +11:00
|
|
|
},
|
|
|
|
DResult,
|
|
|
|
};
|
|
|
|
use chrono::Utc;
|
2023-02-19 14:03:15 +11:00
|
|
|
use mockall_double::double;
|
2023-06-12 00:36:55 +10:00
|
|
|
use rand::{self, Rng};
|
|
|
|
use rand_distr::{Distribution, Normal};
|
|
|
|
use std::collections::BTreeMap;
|
2023-01-20 23:08:40 +11:00
|
|
|
|
|
|
|
pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
|
|
|
|
target_item.total_stats = BTreeMap::new();
|
|
|
|
// 1: Start with total stats = raw stats
|
|
|
|
for stat_type in StatType::values() {
|
2023-06-12 00:36:55 +10:00
|
|
|
target_item.total_stats.insert(
|
|
|
|
stat_type.clone(),
|
|
|
|
*user.raw_stats.get(&stat_type).unwrap_or(&0.0),
|
|
|
|
);
|
2023-01-20 23:08:40 +11:00
|
|
|
}
|
|
|
|
// 2: Apply stat (de)buffs...
|
|
|
|
for buff in &target_item.temporary_buffs {
|
|
|
|
for impact in &buff.impacts {
|
|
|
|
match impact {
|
|
|
|
BuffImpact::ChangeStat { stat, magnitude } => {
|
2023-06-12 00:36:55 +10:00
|
|
|
target_item
|
|
|
|
.total_stats
|
|
|
|
.entry(stat.clone())
|
|
|
|
.and_modify(|old_value| {
|
|
|
|
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
|
|
|
})
|
2023-05-28 21:59:09 +10:00
|
|
|
.or_insert((*magnitude).max(0.0));
|
2023-01-20 23:08:40 +11:00
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 3: Total skills = raw skills
|
|
|
|
target_item.total_skills = BTreeMap::new();
|
|
|
|
for skill_type in SkillType::values() {
|
2023-06-12 00:36:55 +10:00
|
|
|
target_item.total_skills.insert(
|
|
|
|
skill_type.clone(),
|
|
|
|
*user.raw_skills.get(&skill_type).unwrap_or(&0.0),
|
|
|
|
);
|
2023-01-20 23:08:40 +11:00
|
|
|
}
|
|
|
|
// 4: Adjust skills by stats
|
2023-06-12 00:36:55 +10:00
|
|
|
let brn = *target_item
|
|
|
|
.total_stats
|
|
|
|
.get(&StatType::Brains)
|
|
|
|
.unwrap_or(&0.0);
|
|
|
|
let sen = *target_item
|
|
|
|
.total_stats
|
|
|
|
.get(&StatType::Senses)
|
|
|
|
.unwrap_or(&0.0);
|
|
|
|
let brw = *target_item
|
|
|
|
.total_stats
|
|
|
|
.get(&StatType::Brawn)
|
|
|
|
.unwrap_or(&0.0);
|
|
|
|
let refl = *target_item
|
|
|
|
.total_stats
|
|
|
|
.get(&StatType::Reflexes)
|
|
|
|
.unwrap_or(&0.0);
|
|
|
|
let end = *target_item
|
|
|
|
.total_stats
|
|
|
|
.get(&StatType::Endurance)
|
|
|
|
.unwrap_or(&0.0);
|
2023-01-20 23:08:40 +11:00
|
|
|
let col = *target_item.total_stats.get(&StatType::Cool).unwrap_or(&0.0);
|
2023-06-12 00:36:55 +10:00
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Appraise)
|
|
|
|
.and_modify(|sk| *sk += brn * 0.5)
|
|
|
|
.or_insert(brn * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Appraise)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Blades)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Blades)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Bombs)
|
|
|
|
.and_modify(|sk| *sk += brn * 0.5)
|
|
|
|
.or_insert(brn * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Bombs)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Chemistry)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Climb)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Climb)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Clubs)
|
|
|
|
.and_modify(|sk| *sk += brw * 0.5)
|
|
|
|
.or_insert(brw * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Clubs)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Craft)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Dodge)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Dodge)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fish)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fish)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fists)
|
|
|
|
.and_modify(|sk| *sk += brw * 0.5)
|
|
|
|
.or_insert(brw * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fists)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Focus)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Focus)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fuck)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Fuck)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Hack)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Locksmith)
|
|
|
|
.and_modify(|sk| *sk += brn * 0.5)
|
|
|
|
.or_insert(brn * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Locksmith)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Medic)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Persuade)
|
|
|
|
.and_modify(|sk| *sk += brn * 0.5)
|
|
|
|
.or_insert(brn * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Persuade)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Pilot)
|
|
|
|
.and_modify(|sk| *sk += brn * 0.5)
|
|
|
|
.or_insert(brn * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Pilot)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Pistols)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Pistols)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Quickdraw)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Quickdraw)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Repair)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Rifles)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Rifles)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Scavenge)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Scavenge)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Science)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Sneak)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Sneak)
|
|
|
|
.and_modify(|sk| *sk += col * 0.5)
|
|
|
|
.or_insert(col * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Spears)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Spears)
|
|
|
|
.and_modify(|sk| *sk += end * 0.5)
|
|
|
|
.or_insert(end * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Swim)
|
|
|
|
.and_modify(|sk| *sk += end)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Teach)
|
|
|
|
.and_modify(|sk| *sk += brn)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Throw)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Throw)
|
|
|
|
.and_modify(|sk| *sk += brw * 0.5)
|
|
|
|
.or_insert(brw * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Track)
|
|
|
|
.and_modify(|sk| *sk += sen)
|
|
|
|
.or_insert(brn);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Whips)
|
|
|
|
.and_modify(|sk| *sk += sen * 0.5)
|
|
|
|
.or_insert(sen * 0.5);
|
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(SkillType::Whips)
|
|
|
|
.and_modify(|sk| *sk += refl * 0.5)
|
|
|
|
.or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
// 5: Apply skill (de)buffs...
|
|
|
|
for buff in &target_item.temporary_buffs {
|
|
|
|
for impact in &buff.impacts {
|
|
|
|
match impact {
|
|
|
|
BuffImpact::ChangeSkill { skill, magnitude } => {
|
2023-06-12 00:36:55 +10:00
|
|
|
target_item
|
|
|
|
.total_skills
|
|
|
|
.entry(skill.clone())
|
|
|
|
.and_modify(|old_value| {
|
|
|
|
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
|
|
|
})
|
2023-05-28 21:59:09 +10:00
|
|
|
.or_insert((*magnitude).max(0.0));
|
2023-01-20 23:08:40 +11:00
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-12 00:36:55 +10:00
|
|
|
|
2023-01-20 23:08:40 +11:00
|
|
|
pub fn calc_level_gap(who: &Item, skill: &SkillType, diff_level: f64) -> f64 {
|
|
|
|
let user_level = who.total_skills.get(skill).unwrap_or(&0.0);
|
|
|
|
diff_level - user_level.clone()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn skill_check_fn(level_gap: f64) -> f64 {
|
2023-01-23 21:40:36 +11:00
|
|
|
const K: f64 = 0.3662040962227033; // log 3 / 3
|
2023-01-20 23:08:40 +11:00
|
|
|
rand::thread_rng().gen::<f64>() - 1.0 / (1.0 + (-K * (level_gap as f64)).exp())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rolls the die to determine if a player pulls off something that requires a skill.
|
|
|
|
// It is a number between -1 and 1.
|
|
|
|
// Non-negative numbers mean they pulled it off, positive mean they didn't, with
|
|
|
|
// more positive numbers meaning they did a better job, and more negative numbers
|
|
|
|
// meaning it went really badly.
|
|
|
|
// If level = raw skill, there is a 50% chance of succeeding.
|
2023-01-23 21:40:36 +11:00
|
|
|
// level = raw skill + 3, there is a 75% chance of succeeding.
|
|
|
|
// level = raw skill - 3, there is a 25% chance of succeeding.
|
|
|
|
// Outside those differences, it follows the logistic function:
|
|
|
|
// Difference: -5 -4 -3 -2 -1 0 1 2 3 4 5
|
|
|
|
// Probability: 13% 19% 25% 32% 41% 50% 59% 68% 75% 81% 86%
|
2023-01-20 23:08:40 +11:00
|
|
|
pub fn skill_check_only(who: &Item, skill: &SkillType, diff_level: f64) -> f64 {
|
|
|
|
skill_check_fn(calc_level_gap(who, skill, diff_level))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: Caller must save who because skills might update.
|
|
|
|
// Don't return error if skillcheck fails, it can fail but still grind.
|
2023-06-12 00:36:55 +10:00
|
|
|
pub async fn skill_check_and_grind(
|
|
|
|
trans: &DBTrans,
|
|
|
|
who: &mut Item,
|
|
|
|
skill: &SkillType,
|
|
|
|
diff_level: f64,
|
|
|
|
) -> DResult<f64> {
|
2023-01-20 23:08:40 +11:00
|
|
|
let gap = calc_level_gap(who, skill, diff_level);
|
|
|
|
let result = skill_check_fn(gap);
|
|
|
|
|
|
|
|
// If the skill gap is 0, probability of learning is 0.5
|
2023-01-23 21:40:36 +11:00
|
|
|
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
|
|
|
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
2023-06-12 00:36:55 +10:00
|
|
|
if who.item_type == "player"
|
2023-07-24 22:46:50 +10:00
|
|
|
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap.abs() as f64)).exp()
|
2023-06-12 00:36:55 +10:00
|
|
|
{
|
2023-01-20 23:08:40 +11:00
|
|
|
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
|
|
|
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
2023-06-12 00:36:55 +10:00
|
|
|
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0
|
|
|
|
|| !user
|
|
|
|
.last_skill_improve
|
|
|
|
.get(skill)
|
|
|
|
.map(|t| (Utc::now() - *t).num_seconds() > 60)
|
|
|
|
.unwrap_or(true)
|
|
|
|
{
|
|
|
|
return Ok(result);
|
2023-01-20 23:08:40 +11:00
|
|
|
}
|
2023-06-12 00:36:55 +10:00
|
|
|
user.raw_skills
|
|
|
|
.entry(skill.clone())
|
|
|
|
.and_modify(|raw| *raw += 0.01)
|
|
|
|
.or_insert(0.01);
|
2023-01-25 23:59:19 +11:00
|
|
|
user.last_skill_improve.insert(skill.clone(), Utc::now());
|
2023-06-12 00:36:55 +10:00
|
|
|
trans
|
|
|
|
.queue_for_session(
|
|
|
|
&sess,
|
|
|
|
Some(&format!(
|
|
|
|
"Your raw {} is now {:.2}\n",
|
|
|
|
skill.display(),
|
|
|
|
user.raw_skills.get(skill).unwrap_or(&0.0)
|
|
|
|
)),
|
|
|
|
)
|
|
|
|
.await?;
|
2023-01-20 23:08:40 +11:00
|
|
|
trans.save_user_model(&user).await?;
|
|
|
|
calculate_total_stats_skills_for_user(who, &user);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
2023-06-12 00:36:55 +10:00
|
|
|
|
|
|
|
struct SkillFailDamage {
|
|
|
|
pub damage_type: DamageType,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DamageDistribution for SkillFailDamage {
|
|
|
|
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)> {
|
|
|
|
vec![(&self.damage_type, total)]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn crit_fail_penalty_for_skill(
|
|
|
|
trans: &DBTrans,
|
|
|
|
who: &mut Item,
|
|
|
|
skill: &SkillType,
|
|
|
|
) -> DResult<()> {
|
|
|
|
use SkillType::*;
|
|
|
|
let (msg, part, dist) = match skill {
|
|
|
|
Bombs | Chemistry => (
|
|
|
|
"Ow! You burn your hand.",
|
|
|
|
BodyPart::Hands,
|
|
|
|
SkillFailDamage {
|
|
|
|
damage_type: DamageType::Shock,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
Craft | Repair => (
|
|
|
|
"Ow! You catch your hand on something sharp.",
|
|
|
|
BodyPart::Hands,
|
|
|
|
SkillFailDamage {
|
|
|
|
damage_type: DamageType::Slash,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
_ => return Ok(()),
|
|
|
|
};
|
|
|
|
let actual_damage_presoak = Normal::<f64>::new(5.0, 1.0)?
|
|
|
|
.sample(&mut rand::thread_rng())
|
|
|
|
.floor()
|
|
|
|
.max(1.0);
|
|
|
|
let final_damage = soak_damage(trans, &dist, who, actual_damage_presoak, &part).await?;
|
|
|
|
change_health(trans, -final_damage as i64, who, &msg, &msg).await?;
|
|
|
|
Ok(())
|
|
|
|
}
|