blastmud/blastmud_game/src/services/skills.rs

220 lines
11 KiB
Rust
Raw Normal View History

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;
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))
.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)
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))
.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
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> {
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)
}
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());
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)
}