Implement grinding.

This commit is contained in:
Condorra 2023-01-20 23:08:40 +11:00
parent b2012d4d18
commit 0a5b9cc94e
9 changed files with 435 additions and 76 deletions

View File

@ -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 =

View File

@ -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?;
}
}

View File

@ -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>,

View File

@ -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(),
}
}
}

View File

@ -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())
}

View 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)
}

View File

@ -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 {
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>{}"),

View File

@ -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
View 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 <> ");"