2023-01-20 23:08:40 +11:00
|
|
|
use crate::{
|
|
|
|
models::{
|
|
|
|
item::{Item, SkillType, StatType, BuffImpact},
|
|
|
|
user::User
|
|
|
|
},
|
|
|
|
DResult,
|
|
|
|
};
|
|
|
|
use rand::{self, Rng};
|
|
|
|
use chrono::Utc;
|
|
|
|
use std::collections::BTreeMap;
|
2023-02-19 14:03:15 +11:00
|
|
|
use mockall_double::double;
|
|
|
|
#[double] use crate::db::DBTrans;
|
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));
|
|
|
|
}
|
|
|
|
// 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));
|
|
|
|
}
|
|
|
|
// 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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Blades)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Blades)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Chemistry)
|
|
|
|
.and_modify(|sk| *sk += brn).or_insert(brn);
|
|
|
|
target_item.total_skills.entry(SkillType::Climb)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Climb)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Clubs)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Clubs)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Craft)
|
|
|
|
.and_modify(|sk| *sk += brn).or_insert(brn);
|
|
|
|
target_item.total_skills.entry(SkillType::Dodge)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Dodge)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fish)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fish)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fists)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fists)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Focus)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Focus)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fuck)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Fuck)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Pistols)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Pistols)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Quickdraw)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Quickdraw)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Repair)
|
|
|
|
.and_modify(|sk| *sk += brn).or_insert(brn);
|
|
|
|
target_item.total_skills.entry(SkillType::Rifles)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Rifles)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Scavenge)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Scavenge)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Science)
|
|
|
|
.and_modify(|sk| *sk += brn).or_insert(brn);
|
|
|
|
target_item.total_skills.entry(SkillType::Sneak)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Sneak)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Spears)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Spears)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Throw)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Track)
|
|
|
|
.and_modify(|sk| *sk += sen).or_insert(brn);
|
|
|
|
target_item.total_skills.entry(SkillType::Whips)
|
2023-02-19 01:18:08 +11:00
|
|
|
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
2023-01-20 23:08:40 +11:00
|
|
|
target_item.total_skills.entry(SkillType::Whips)
|
2023-02-19 01:18:08 +11:00
|
|
|
.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
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &SkillType, diff_level: f64) -> DResult<f64> {
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
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-01-20 23:08:40 +11:00
|
|
|
trans.queue_for_session(&sess,
|
2023-01-22 22:43:44 +11:00
|
|
|
Some(&format!("Your raw {} is now {:.2}\n",
|
2023-01-20 23:08:40 +11:00
|
|
|
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)
|
|
|
|
}
|