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