Implement grinding.
This commit is contained in:
		
							parent
							
								
									b2012d4d18
								
							
						
					
					
						commit
						0a5b9cc94e
					
				@ -2,11 +2,24 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error,
 | 
			
		||||
            get_player_item_or_fail, search_item_for_user};
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use ansi::ansi;
 | 
			
		||||
use crate::services::broadcast_to_room;
 | 
			
		||||
use crate::db::{DBTrans, ItemSearchParams};
 | 
			
		||||
use crate::models::{item::{Item, LocationActionType, Subattack}};
 | 
			
		||||
use std::time;
 | 
			
		||||
use crate::{
 | 
			
		||||
    services::broadcast_to_room,
 | 
			
		||||
    db::{DBTrans, ItemSearchParams},
 | 
			
		||||
    models::{item::{Item, LocationActionType, Subattack}},
 | 
			
		||||
    regular_tasks::{TaskRunContext, TaskHandler},
 | 
			
		||||
    DResult
 | 
			
		||||
};
 | 
			
		||||
use async_recursion::async_recursion;
 | 
			
		||||
 | 
			
		||||
pub struct AttackTaskHandler;
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl TaskHandler for AttackTaskHandler {
 | 
			
		||||
    async fn do_task(&self, _ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
 | 
			
		||||
        todo!("AttackTaskHandler");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
 | 
			
		||||
    let mut new_to_whom = (*to_whom).clone();
 | 
			
		||||
    if let Some(ac) = new_to_whom.active_combat.as_mut() {
 | 
			
		||||
@ -62,7 +75,7 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR
 | 
			
		||||
        verb,
 | 
			
		||||
        &to_whom.display_for_sentence(false, 1, false))
 | 
			
		||||
    );
 | 
			
		||||
    broadcast_to_room(trans, &by_whom.location, None, &msg_exp, Some(msg_nonexp.as_str())).await?;
 | 
			
		||||
    broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?;
 | 
			
		||||
 | 
			
		||||
    let mut by_whom_for_update = by_whom.clone();
 | 
			
		||||
    by_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacking =
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ use crate::{
 | 
			
		||||
    },
 | 
			
		||||
    services::{
 | 
			
		||||
        broadcast_to_room,
 | 
			
		||||
        skill_check
 | 
			
		||||
        skills::skill_check_and_grind
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
use std::time;
 | 
			
		||||
@ -54,11 +54,11 @@ pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, ar
 | 
			
		||||
 | 
			
		||||
pub async fn attempt_move_immediate(
 | 
			
		||||
    trans: &DBTrans,
 | 
			
		||||
    mover: &Item,
 | 
			
		||||
    orig_mover: &Item,
 | 
			
		||||
    direction: &Direction,
 | 
			
		||||
    mut player_ctx: Option<&mut VerbContext<'_>>
 | 
			
		||||
) -> UResult<()> {
 | 
			
		||||
    let (heretype, herecode) = mover.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
 | 
			
		||||
    let (heretype, herecode) = orig_mover.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
 | 
			
		||||
    if heretype != "room" {
 | 
			
		||||
        // Fix this when we have planes / boats / roomkits.
 | 
			
		||||
        user_error("Navigating outside rooms not yet supported.".to_owned())?
 | 
			
		||||
@ -68,11 +68,12 @@ pub async fn attempt_move_immediate(
 | 
			
		||||
    let exit = room.exits.iter().find(|ex| ex.direction == *direction)
 | 
			
		||||
        .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
 | 
			
		||||
 | 
			
		||||
    let mut mover = (*orig_mover).clone();
 | 
			
		||||
    match exit.exit_type {
 | 
			
		||||
        ExitType::Free => {}
 | 
			
		||||
        ExitType::Blocked(blocker) => {
 | 
			
		||||
            if let Some(ctx) = player_ctx.as_mut() {
 | 
			
		||||
                if !blocker.attempt_exit(ctx, &mover, exit).await? {
 | 
			
		||||
                if !blocker.attempt_exit(ctx, &mut mover, exit).await? {
 | 
			
		||||
                    user_error("Stopping movement".to_owned())?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -87,12 +88,12 @@ pub async fn attempt_move_immediate(
 | 
			
		||||
        Some(old_victim) => {
 | 
			
		||||
            if let Some((vcode, vtype)) = old_victim.split_once("/") {
 | 
			
		||||
                if let Some(vitem) = trans.find_item_by_type_code(vcode, vtype).await? {
 | 
			
		||||
                    stop_attacking(trans, mover, &vitem).await?;
 | 
			
		||||
                    stop_attacking(trans, &mover, &vitem).await?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    match mover.active_combat.as_ref().map(|ac| &ac.attacked_by[..]) {
 | 
			
		||||
    match mover.active_combat.clone().as_ref().map(|ac| &ac.attacked_by[..]) {
 | 
			
		||||
        None | Some([]) => {}
 | 
			
		||||
        Some(attackers) => {
 | 
			
		||||
            let mut attacker_names = Vec::new();
 | 
			
		||||
@ -109,26 +110,31 @@ pub async fn attempt_move_immediate(
 | 
			
		||||
            }
 | 
			
		||||
            let attacker_names_ref = attacker_names.iter().map(|n| n.as_str()).collect::<Vec<&str>>();
 | 
			
		||||
            let attacker_names_str = language::join_words(&attacker_names_ref[..]);
 | 
			
		||||
            if skill_check(mover, &SkillType::Dodge, attackers.len() as i64) >= 0.0 {
 | 
			
		||||
            if skill_check_and_grind(trans, &mut mover, &SkillType::Dodge, attackers.len() as f64 + 8.0).await? >= 0.0 {
 | 
			
		||||
                if let Some(ctx) = player_ctx.as_ref() {
 | 
			
		||||
                    trans.queue_for_session(ctx.session,
 | 
			
		||||
                                            Some(&format!("You successfully get away from {}\n",
 | 
			
		||||
                                                          &attacker_names_str))).await?;
 | 
			
		||||
                    for item in &attacker_items[..] {
 | 
			
		||||
                        stop_attacking(trans, &item, mover).await?;
 | 
			
		||||
                        stop_attacking(trans, &item, &mover).await?;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                user_error(format!("You try and fail to run past {}", &attacker_names_str))?;
 | 
			
		||||
                if let Some(ctx) = player_ctx.as_ref() {
 | 
			
		||||
                    trans.queue_for_session(ctx.session,
 | 
			
		||||
                                            Some(&format!("You try and fail to run past {}\n",
 | 
			
		||||
                                                          &attacker_names_str))).await?;
 | 
			
		||||
                }
 | 
			
		||||
                trans.save_item_model(&mover).await?;
 | 
			
		||||
                return Ok(());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let mut new_mover = (*mover).clone();
 | 
			
		||||
    new_mover.location = format!("{}/{}", "room", new_room.code);
 | 
			
		||||
    new_mover.action_type = LocationActionType::Normal;
 | 
			
		||||
    new_mover.active_combat = None;
 | 
			
		||||
    trans.save_item_model(&new_mover).await?;
 | 
			
		||||
    mover.location = format!("{}/{}", "room", new_room.code);
 | 
			
		||||
    mover.action_type = LocationActionType::Normal;
 | 
			
		||||
    mover.active_combat = None;
 | 
			
		||||
    trans.save_item_model(&mover).await?;
 | 
			
		||||
 | 
			
		||||
    if let Some(ctx) = player_ctx {
 | 
			
		||||
        look::VERB.handle(ctx, "look", "").await?;
 | 
			
		||||
@ -136,7 +142,7 @@ pub async fn attempt_move_immediate(
 | 
			
		||||
 | 
			
		||||
    if let Some(old_room_item) = trans.find_item_by_type_code("room", room.code).await? {
 | 
			
		||||
        if let Some(new_room_item) = trans.find_item_by_type_code("room", new_room.code).await? {
 | 
			
		||||
            announce_move(&trans, &new_mover, &old_room_item, &new_room_item).await?;
 | 
			
		||||
            announce_move(&trans, &mover, &old_room_item, &new_room_item).await?;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,19 +12,19 @@ pub enum BuffCause {
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 | 
			
		||||
pub enum BuffImpact {
 | 
			
		||||
    ChangeStat { stat: StatType, magnitude: i16 },
 | 
			
		||||
    ChangeSkill { stat: StatType, magnitude: i16 }
 | 
			
		||||
    ChangeSkill { skill: SkillType, magnitude: i16 }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 | 
			
		||||
pub struct Buff {
 | 
			
		||||
    description: String,
 | 
			
		||||
    cause: BuffCause,
 | 
			
		||||
    impacts: Vec<BuffImpact>
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub cause: BuffCause,
 | 
			
		||||
    pub impacts: Vec<BuffImpact>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 | 
			
		||||
pub enum SkillType {
 | 
			
		||||
    Apraise,
 | 
			
		||||
    Appraise,
 | 
			
		||||
    Blades,
 | 
			
		||||
    Bombs,
 | 
			
		||||
    Chemistry,
 | 
			
		||||
@ -35,6 +35,7 @@ pub enum SkillType {
 | 
			
		||||
    Fish,
 | 
			
		||||
    Fists,
 | 
			
		||||
    Flails,
 | 
			
		||||
    Focus,
 | 
			
		||||
    Fuck,
 | 
			
		||||
    Hack,
 | 
			
		||||
    Locksmith,
 | 
			
		||||
@ -58,6 +59,86 @@ pub enum SkillType {
 | 
			
		||||
    Whips
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SkillType {
 | 
			
		||||
    pub fn values() -> Vec<SkillType> {
 | 
			
		||||
        use SkillType::*;
 | 
			
		||||
        vec!(
 | 
			
		||||
          Appraise,
 | 
			
		||||
          Blades,
 | 
			
		||||
          Bombs,
 | 
			
		||||
          Chemistry,
 | 
			
		||||
          Climb,
 | 
			
		||||
          Clubs,
 | 
			
		||||
          Craft,
 | 
			
		||||
          Dodge,
 | 
			
		||||
          Fish,
 | 
			
		||||
          Fists,
 | 
			
		||||
          Flails,
 | 
			
		||||
          Focus,
 | 
			
		||||
          Fuck,
 | 
			
		||||
          Hack,
 | 
			
		||||
          Locksmith,
 | 
			
		||||
          Medic,
 | 
			
		||||
          Persuade,
 | 
			
		||||
          Pilot,
 | 
			
		||||
          Pistols,
 | 
			
		||||
          Quickdraw,
 | 
			
		||||
          Repair,
 | 
			
		||||
          Ride,
 | 
			
		||||
          Rifles,
 | 
			
		||||
          Scavenge,
 | 
			
		||||
          Science,
 | 
			
		||||
          Sneak,
 | 
			
		||||
          Spears,
 | 
			
		||||
          Swim,
 | 
			
		||||
          Teach,
 | 
			
		||||
          Throw,
 | 
			
		||||
          Track,
 | 
			
		||||
          Wrestle,
 | 
			
		||||
          Whips
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    pub fn display(&self) -> &'static str {
 | 
			
		||||
        use SkillType::*;
 | 
			
		||||
        match self {
 | 
			
		||||
            Appraise => "appraise",
 | 
			
		||||
            Blades => "blades",
 | 
			
		||||
            Bombs => "bombs",
 | 
			
		||||
            Chemistry => "chemistry",
 | 
			
		||||
            Climb => "climb",
 | 
			
		||||
            Clubs => "clubs",
 | 
			
		||||
            Craft => "craft",
 | 
			
		||||
            Dodge => "dodge",
 | 
			
		||||
            Fish => "fish",
 | 
			
		||||
            Fists => "fists",
 | 
			
		||||
            Flails => "flails",
 | 
			
		||||
            Focus => "focus",
 | 
			
		||||
            Fuck => "fuck",
 | 
			
		||||
            Hack => "hack",
 | 
			
		||||
            Locksmith => "locksmith",
 | 
			
		||||
            Medic => "medic",
 | 
			
		||||
            Persuade => "persuade",
 | 
			
		||||
            Pilot => "pilot",
 | 
			
		||||
            Pistols => "pistols",
 | 
			
		||||
            Quickdraw => "quickdraw",
 | 
			
		||||
            Repair => "repair",
 | 
			
		||||
            Ride => "ride",
 | 
			
		||||
            Rifles => "rifles",
 | 
			
		||||
            Scavenge => "scavenge",
 | 
			
		||||
            Science => "science",
 | 
			
		||||
            Sneak => "sneak",
 | 
			
		||||
            Spears => "spears",
 | 
			
		||||
            Swim => "swim",
 | 
			
		||||
            Teach => "teach",
 | 
			
		||||
            Throw => "throw",
 | 
			
		||||
            Track => "track",
 | 
			
		||||
            Wrestle => "wrestle",
 | 
			
		||||
            Whips => "whips"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 | 
			
		||||
pub enum StatType {
 | 
			
		||||
    Brains,
 | 
			
		||||
@ -68,6 +149,20 @@ pub enum StatType {
 | 
			
		||||
    Cool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl StatType {
 | 
			
		||||
    pub fn values() -> Vec<Self> {
 | 
			
		||||
        use StatType::*;
 | 
			
		||||
        vec!(
 | 
			
		||||
            Brains,
 | 
			
		||||
            Senses,
 | 
			
		||||
            Brawn,
 | 
			
		||||
            Reflexes,
 | 
			
		||||
            Endurance,
 | 
			
		||||
            Cool
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 | 
			
		||||
pub struct Pronouns {
 | 
			
		||||
    pub subject: String,
 | 
			
		||||
@ -175,7 +270,7 @@ impl Default for ActiveCombat {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
 | 
			
		||||
#[serde(default)]
 | 
			
		||||
pub struct Item {
 | 
			
		||||
    pub item_code: String,
 | 
			
		||||
@ -192,8 +287,8 @@ pub struct Item {
 | 
			
		||||
    pub is_challenge_attack_only: bool,
 | 
			
		||||
    
 | 
			
		||||
    pub total_xp: u64,
 | 
			
		||||
    pub total_stats: BTreeMap<StatType, u16>,
 | 
			
		||||
    pub total_skills: BTreeMap<SkillType, u16>,
 | 
			
		||||
    pub total_stats: BTreeMap<StatType, f64>,
 | 
			
		||||
    pub total_skills: BTreeMap<SkillType, f64>,
 | 
			
		||||
    pub temporary_buffs: Vec<Buff>,
 | 
			
		||||
    pub pronouns: Pronouns,
 | 
			
		||||
    pub flags: Vec<ItemFlag>,
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ pub struct UserExperienceData {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug)]
 | 
			
		||||
#[serde(default)]
 | 
			
		||||
pub struct User {
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    pub password_hash: String, // bcrypted.
 | 
			
		||||
@ -31,8 +32,9 @@ pub struct User {
 | 
			
		||||
    
 | 
			
		||||
    pub terms: UserTermData,
 | 
			
		||||
    pub experience: UserExperienceData,
 | 
			
		||||
    pub raw_skills: BTreeMap<SkillType, u16>,
 | 
			
		||||
    pub raw_stats: BTreeMap<StatType, u16>,
 | 
			
		||||
    pub raw_skills: BTreeMap<SkillType, f64>,
 | 
			
		||||
    pub raw_stats: BTreeMap<StatType, f64>,
 | 
			
		||||
    pub last_skill_improve: BTreeMap<SkillType, DateTime<Utc>>,
 | 
			
		||||
    // Reminder: Consider backwards compatibility when updating this. New fields should generally
 | 
			
		||||
    //           be an Option, or things will crash out for existing sessions.
 | 
			
		||||
}
 | 
			
		||||
@ -74,6 +76,7 @@ impl Default for User {
 | 
			
		||||
            experience: UserExperienceData::default(),
 | 
			
		||||
            raw_skills: BTreeMap::new(),
 | 
			
		||||
            raw_stats: BTreeMap::new(),
 | 
			
		||||
            last_skill_improve: BTreeMap::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    models::item::{Item, SkillType},
 | 
			
		||||
    db::DBTrans,
 | 
			
		||||
    DResult
 | 
			
		||||
    DResult,
 | 
			
		||||
    models::item::Item
 | 
			
		||||
};
 | 
			
		||||
use rand::{self, Rng};
 | 
			
		||||
 | 
			
		||||
pub mod skills;
 | 
			
		||||
 | 
			
		||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
 | 
			
		||||
                               message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
 | 
			
		||||
    for item in trans.find_items_by_location(location).await? {
 | 
			
		||||
@ -22,21 +24,3 @@ pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Optio
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 + 1, there is a 75% chance of succeeding.
 | 
			
		||||
//    level = raw skill - 1, there is a 25% chance of succeeding.
 | 
			
		||||
// Past those differences, it follows the logistic function:
 | 
			
		||||
//   Difference:     -5    -4    -3    -2   -1   0    1    2    3    4    5
 | 
			
		||||
//   Probability:    0.4%  1.2%  3.5%  10%  25%  50%  75%  90%  96%  99%  99.6%
 | 
			
		||||
pub fn skill_check(who: &Item, skill: &SkillType, level: i64) -> f64 {
 | 
			
		||||
    let user_level = who.total_skills.get(skill).unwrap_or(&0);
 | 
			
		||||
    let level_gap = level - user_level.clone() as i64;
 | 
			
		||||
    const K: f64 = 1.0986122886681098; // log 3
 | 
			
		||||
    rand::thread_rng().gen::<f64>() - 1.0 / (1.0 + (-K * (level_gap as f64)).exp())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										219
									
								
								blastmud_game/src/services/skills.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								blastmud_game/src/services/skills.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,219 @@
 | 
			
		||||
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 = 1.0986122886681098; // log 3
 | 
			
		||||
    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 + 1, there is a 75% chance of succeeding.
 | 
			
		||||
//    level = raw skill - 1, there is a 25% chance of succeeding.
 | 
			
		||||
// Past those differences, it follows the logistic function:
 | 
			
		||||
//   Difference:     -5    -4    -3    -2   -1   0    1    2    3    4    5
 | 
			
		||||
//   Probability:    0.4%  1.2%  3.5%  10%  25%  50%  75%  90%  96%  99%  99.6%
 | 
			
		||||
#[allow(unused)]
 | 
			
		||||
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.25, and so on (exponential decrease).
 | 
			
		||||
    const LAMBDA: f64 = -0.6931471805599453; // log 0.5
 | 
			
		||||
    if who.item_type == "player" && rand::thread_rng().gen::<f64>() < 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);
 | 
			
		||||
                
 | 
			
		||||
                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)
 | 
			
		||||
}
 | 
			
		||||
@ -12,6 +12,7 @@ use crate::models::{
 | 
			
		||||
    user::{User},
 | 
			
		||||
    session::Session
 | 
			
		||||
};
 | 
			
		||||
use crate::services::skills::calculate_total_stats_skills_for_user;
 | 
			
		||||
use ansi::ansi;
 | 
			
		||||
use nom::character::complete::u8;
 | 
			
		||||
 | 
			
		||||
@ -68,7 +69,7 @@ fn work_out_state(user: &User, item: &Item) -> StatbotState {
 | 
			
		||||
    if !user.raw_stats.contains_key(&StatType::Cool) {
 | 
			
		||||
        return StatbotState::Cool;
 | 
			
		||||
    }
 | 
			
		||||
    if points_left(user) != 0 {
 | 
			
		||||
    if points_left(user) != 0.0 {
 | 
			
		||||
        return StatbotState::FixTotals;
 | 
			
		||||
    }
 | 
			
		||||
    if item.sex.is_none() {
 | 
			
		||||
@ -80,23 +81,23 @@ fn work_out_state(user: &User, item: &Item) -> StatbotState {
 | 
			
		||||
    StatbotState::Done
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn points_left(user: &User) -> u16 {
 | 
			
		||||
    let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8);
 | 
			
		||||
    let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8);
 | 
			
		||||
    let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8);
 | 
			
		||||
    let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8);
 | 
			
		||||
    let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8);
 | 
			
		||||
    let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8);
 | 
			
		||||
    (62 - (brn + sen + brw + refl + end + col) as i16).max(0) as u16
 | 
			
		||||
fn points_left(user: &User) -> f64 {
 | 
			
		||||
    let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8.0);
 | 
			
		||||
    let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8.0);
 | 
			
		||||
    let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8.0);
 | 
			
		||||
    let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8.0);
 | 
			
		||||
    let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8.0);
 | 
			
		||||
    let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8.0);
 | 
			
		||||
    (62 - (brn + sen + brw + refl + end + col) as i16).max(0) as f64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn next_action_text(session: &Session, user: &User, item: &Item) -> String {
 | 
			
		||||
    let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8);
 | 
			
		||||
    let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8);
 | 
			
		||||
    let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8);
 | 
			
		||||
    let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8);
 | 
			
		||||
    let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8);
 | 
			
		||||
    let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8);
 | 
			
		||||
    let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8.0);
 | 
			
		||||
    let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8.0);
 | 
			
		||||
    let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8.0);
 | 
			
		||||
    let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8.0);
 | 
			
		||||
    let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8.0);
 | 
			
		||||
    let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8.0);
 | 
			
		||||
    let summary = format!("Brains: {}, Senses: {}, Brawn: {}, Reflexes: {}, Endurance: {}, Cool: {}. To spend: {}", brn, sen, brw, refl, end, col, points_left(user));
 | 
			
		||||
 | 
			
		||||
    let st = work_out_state(user, item);
 | 
			
		||||
@ -109,7 +110,7 @@ fn next_action_text(session: &Session, user: &User, item: &Item) -> String {
 | 
			
		||||
             brainpower you will have. If you choose 8, you don't spend any points. There \
 | 
			
		||||
             is a maximum of 15 - if you choose 15, you will spend 7 points and have 7 \
 | 
			
		||||
             left for other stats. Brains help your appraise, bombs, chemistry, craft, \
 | 
			
		||||
             hack, locksmith, medic, pursuade, pilot, repair, science and teach \
 | 
			
		||||
             hack, locksmith, medic, persuade, pilot, repair, science and teach \
 | 
			
		||||
             skills.\n\
 | 
			
		||||
             \tType <green><bold>-statbot brains 8<reset><blue> (or any other \
 | 
			
		||||
             number) to set your brains to that number. You will be able to adjust your \
 | 
			
		||||
@ -148,8 +149,8 @@ fn next_action_text(session: &Session, user: &User, item: &Item) -> String {
 | 
			
		||||
        ), if session.less_explicit_mode { "" } else { " fuck,"}, &summary),
 | 
			
		||||
        StatbotState::Cool => ansi!(
 | 
			
		||||
            "Your next job is to choose how much you keep your cool under pressure. \
 | 
			
		||||
             Cool helps your blades, bombs, fish, pistols, quickdraw, rifles and sneak \
 | 
			
		||||
             skills.\n\
 | 
			
		||||
             Cool helps your blades, bombs, fish, pistols, quickdraw, rifles, sneak \
 | 
			
		||||
             and persuade skills.\n\
 | 
			
		||||
             \tType <green><bold>-statbot cool 8<reset><blue> (or any other number) to \
 | 
			
		||||
             set your cool to that number. You will be able to adjust your stats by \
 | 
			
		||||
             sending me the new value, up until you leave here. Your stats now are: "
 | 
			
		||||
@ -199,17 +200,17 @@ async fn stat_command(ctx: &mut VerbContext<'_>, item: &Item,
 | 
			
		||||
        Ok((_, statno)) => {
 | 
			
		||||
            let points = {
 | 
			
		||||
                let user = get_user_or_fail(ctx)?;
 | 
			
		||||
                points_left(get_user_or_fail(ctx)?) + (user.raw_stats.get(stat).cloned().unwrap_or(8) - 8)
 | 
			
		||||
                points_left(get_user_or_fail(ctx)?) + (user.raw_stats.get(stat).cloned().unwrap_or(8.0) - 8.0)
 | 
			
		||||
            };
 | 
			
		||||
            if (statno - 8) as u16 > points {
 | 
			
		||||
                reply(ctx, &if points == 0 { "You have no points left".to_owned() } else {
 | 
			
		||||
                    format!("You only have {} point{} left", points, if points == 1 { "" } else { "s" })
 | 
			
		||||
            if (statno as f64 - 8.0) > points {
 | 
			
		||||
                reply(ctx, &if points == 0.0 { "You have no points left".to_owned() } else {
 | 
			
		||||
                    format!("You only have {} point{} left", points, if points == 1.0 { "" } else { "s" })
 | 
			
		||||
                }).await?;
 | 
			
		||||
                return Ok(());
 | 
			
		||||
            }
 | 
			
		||||
            {
 | 
			
		||||
                let user_mut = get_user_or_fail_mut(ctx)?;
 | 
			
		||||
                user_mut.raw_stats.insert(stat.clone(), statno as u16);
 | 
			
		||||
                user_mut.raw_stats.insert(stat.clone(), statno as f64);
 | 
			
		||||
            }
 | 
			
		||||
            let user: &User = get_user_or_fail(ctx)?;
 | 
			
		||||
            ctx.trans.save_user_model(user).await?;
 | 
			
		||||
@ -280,11 +281,12 @@ impl ExitBlocker for ChoiceRoomBlocker {
 | 
			
		||||
    async fn attempt_exit(
 | 
			
		||||
        self: &Self,
 | 
			
		||||
        ctx: &mut VerbContext,
 | 
			
		||||
        player: &Item,
 | 
			
		||||
        player: &mut Item,
 | 
			
		||||
        _exit: &Exit
 | 
			
		||||
    ) -> UResult<bool> {
 | 
			
		||||
        let user = get_user_or_fail(ctx)?;
 | 
			
		||||
        if work_out_state(user, player) == StatbotState::Done {
 | 
			
		||||
        if work_out_state(user, player) == StatbotState::Done { 
 | 
			
		||||
            calculate_total_stats_skills_for_user(player, &user);
 | 
			
		||||
            Ok(true)
 | 
			
		||||
        } else {
 | 
			
		||||
            shout(ctx, &format!(ansi!("YOU SHALL NOT PASS UNTIL YOU DO AS I SAY! <blue>{}"),
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ pub trait ExitBlocker {
 | 
			
		||||
    async fn attempt_exit(
 | 
			
		||||
        self: &Self,
 | 
			
		||||
        ctx: &mut VerbContext,
 | 
			
		||||
        player: &Item,
 | 
			
		||||
        player: &mut Item,
 | 
			
		||||
        exit: &Exit
 | 
			
		||||
    ) -> UResult<bool>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								scripts/statgen.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								scripts/statgen.hs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
{-# LANGUAGE OverloadedStrings #-}
 | 
			
		||||
import qualified Data.Text as T
 | 
			
		||||
import qualified Data.Text.IO as T
 | 
			
		||||
import qualified Data.Set as S
 | 
			
		||||
import Data.List
 | 
			
		||||
import Control.Monad
 | 
			
		||||
 | 
			
		||||
stats :: [(T.Text, [T.Text])]
 | 
			
		||||
stats = [
 | 
			
		||||
      ("brn", ["appraise", "bombs", "chemistry", "craft", "hack", "locksmith", "medic", "persuade", "pilot", "repair",
 | 
			
		||||
               "science", "teach"]),
 | 
			
		||||
      ("sen", ["appraise", "dodge", "focus", "fuck", "scavenge", "sneak", "throw", "track", "whips"]),
 | 
			
		||||
      ("brw", ["clubs", "fists", "throw"]),
 | 
			
		||||
      ("refl", ["blades", "climb", "clubs", "dodge", "locksmith", "pilot", "pistols", "quickdraw", "rifles", "spears",
 | 
			
		||||
                "whips"]),
 | 
			
		||||
      ("end", ["climb", "fish", "fists", "focus", "fuck", "scavenge", "spears", "swim"]),
 | 
			
		||||
      ("col", ["blades", "bombs", "fish", "pistols", "quickdraw", "rifles", "sneak", "persuade"])
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
doubleValue :: S.Set T.Text
 | 
			
		||||
doubleValue = S.fromList [
 | 
			
		||||
      "chemistry", "craft", "hack", "medic", "repair",
 | 
			
		||||
      "science", "swim", "teach", "track"
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
main :: IO ()
 | 
			
		||||
main =
 | 
			
		||||
  let skillSet = 
 | 
			
		||||
        sortOn snd $
 | 
			
		||||
        stats >>= (\(st, skills) -> map (\skill -> (st, skill)) skills)
 | 
			
		||||
  in
 | 
			
		||||
    forM_ skillSet $
 | 
			
		||||
      \(stat, skill) ->
 | 
			
		||||
        let mup = if skill `S.member` doubleValue then "" else " * 0.5"
 | 
			
		||||
        in 
 | 
			
		||||
        T.putStrLn $ "    target_item.total_skills.entry(SkillType::" <> (T.toTitle skill) <> ")\n\
 | 
			
		||||
                     \        .and_modify(|sk| *sk += " <> stat <> mup <> ").or_insert(brn" <> mup <> ");"
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user