#[double] use crate::db::DBTrans; use crate::{ models::{ item::{BuffImpact, Item, SkillType, StatType}, user::User, }, services::combat::{change_health, soak_damage}, static_content::{ possession_type::{DamageDistribution, DamageType}, species::BodyPart, }, DResult, }; use chrono::Utc; use mockall_double::double; use rand::{self, Rng}; use rand_distr::{Distribution, Normal}; use std::collections::BTreeMap; 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), ); } // 2: Apply stat (de)buffs... for impact in target_item .temporary_buffs .iter() .flat_map(|buff| &buff.impacts) .chain(user.wristpad_hack_buffs()) { match impact { BuffImpact::ChangeStat { ref stat, ref magnitude, } => { target_item .total_stats .entry(stat.clone()) .and_modify(|old_value| { *old_value = (*old_value + magnitude.clone() as f64).max(0.0) }) .or_insert((*magnitude).max(0.0)); } _ => {} } } // 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), ); } // 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); 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); // 5: Apply skill (de)buffs... for impact in target_item .temporary_buffs .iter() .flat_map(|buff| &buff.impacts) .chain(user.wristpad_hack_buffs()) { 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) }) .or_insert((*magnitude).max(0.0)); } _ => {} } } } 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 rand::thread_rng().gen::() - 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% 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 { 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.75 // If the skill gap is 1, probability of learning is 0.6 (20% less), and so on (exponential decrease). const LAMBDA: f64 = -0.2231435513142097; // log 0.8 if who.item_type == "player" && rand::thread_rng().gen::() < 0.75 * (LAMBDA * (gap.abs() as f64)).exp() { 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); } 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?; 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::::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(()) }