blastmud/blastmud_game/src/services/skills.rs

469 lines
14 KiB
Rust
Raw Normal View History

#[double]
use crate::db::DBTrans;
2023-01-20 23:08:40 +11:00
use crate::{
models::{
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;
use mockall_double::double;
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() {
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 } => {
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() {
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
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);
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 } => {
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-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 {
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.
// 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.
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
// 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
if who.item_type == "player"
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp()
{
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? {
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
}
user.raw_skills
.entry(skill.clone())
.and_modify(|raw| *raw += 0.01)
.or_insert(0.01);
user.last_skill_improve.insert(skill.clone(), Utc::now());
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)
}
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(())
}