use crate::{ models::{ item::{Item, SkillType, StatType, BuffImpact}, user::User }, db::DBTrans, DResult, }; use rand::{self, Rng}; use chrono::Utc; 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 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)) .or_insert((*magnitude).max(0) as f64); } _ => {} } } } // 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(brn * 0.5); target_item.total_skills.entry(SkillType::Blades) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Blades) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 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(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Climb) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Clubs) .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Clubs) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Dodge) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fish) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fish) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fists) .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fists) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Focus) .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Focus) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fuck) .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Fuck) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 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(brn * 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(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Pistols) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Pistols) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Quickdraw) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Quickdraw) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Rifles) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Scavenge) .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Scavenge) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Sneak) .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Spears) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Spears) .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Throw) .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 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(brn * 0.5); target_item.total_skills.entry(SkillType::Whips) .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); // 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)) .or_insert((*magnitude).max(0) as f64); } _ => {} } } } } 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.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::() < 0.5 * (LAMBDA * (gap 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) }