diff --git a/blastmud_game/src/language.rs b/blastmud_game/src/language.rs index 3db2ee0..b5fb985 100644 --- a/blastmud_game/src/language.rs +++ b/blastmud_game/src/language.rs @@ -61,6 +61,14 @@ pub fn caps_first(inp: &str) -> String { } } +pub fn join_words(words: &[&str]) -> String { + match words.split_last() { + None => "".to_string(), + Some((last, [])) => last.to_string(), + Some((last, rest)) => rest.join(", ") + " and " + last + } +} + #[cfg(test)] mod test { #[test] @@ -96,4 +104,17 @@ mod test { assert_eq!(super::caps_first(inp), outp); } } + + #[test] + fn join_words_works() { + for (inp, outp) in vec!( + (vec!(), ""), + (vec!("cat"), "cat"), + (vec!("cat", "dog"), "cat and dog"), + (vec!("cat", "dog", "fish"), "cat, dog and fish"), + (vec!("wolf", "cat", "dog", "fish"), "wolf, cat, dog and fish"), + ) { + assert_eq!(super::join_words(&inp[..]), outp); + } + } } diff --git a/blastmud_game/src/message_handler/user_commands/attack.rs b/blastmud_game/src/message_handler/user_commands/attack.rs index ce21fae..60cf3fd 100644 --- a/blastmud_game/src/message_handler/user_commands/attack.rs +++ b/blastmud_game/src/message_handler/user_commands/attack.rs @@ -7,8 +7,25 @@ use crate::db::{DBTrans, ItemSearchParams}; use crate::models::{item::{Item, LocationActionType, Subattack}}; use async_recursion::async_recursion; +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() { + let old_attacker = format!("{}/{}", by_whom.item_type, by_whom.item_code); + ac.attacked_by.retain(|v| v != &old_attacker); + trans.save_item_model(&new_to_whom).await?; + } + let mut new_by_whom = (*by_whom).clone(); + if let Some(ac) = new_by_whom.active_combat.as_mut() { + ac.attacking = None; + } + new_by_whom.action_type = LocationActionType::Normal; + trans.save_item_model(&new_by_whom).await?; + + Ok(()) +} + #[async_recursion] -async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> { +pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> { let mut msg_exp = String::new(); let mut msg_nonexp = String::new(); let mut verb: String = "attacks".to_string(); @@ -24,12 +41,7 @@ async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResul user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?, Some((cur_type, cur_code)) => { if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? { - let mut cur_item = (*cur_item_arc).clone(); - if let Some(ac) = cur_item.active_combat.as_mut() { - let old_attacker = format!("{}/{}", by_whom.item_type, by_whom.item_code); - ac.attacked_by.retain(|v| v != &old_attacker); - trans.save_item_model(&cur_item).await?; - } + stop_attacking(trans, by_whom, &cur_item_arc).await?; } } _ => {} diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 22d1fd3..49de610 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -1,11 +1,13 @@ use super::{ VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail, - look + look, + attack::stop_attacking, }; use async_trait::async_trait; use crate::{ DResult, + language, regular_tasks::queued_command::{ QueueCommandHandler, QueueCommand, @@ -13,8 +15,15 @@ use crate::{ }, static_content::room::{self, Direction, ExitType}, db::DBTrans, - models::item::Item, - services::broadcast_to_room, + models::item::{ + Item, + SkillType, + LocationActionType + }, + services::{ + broadcast_to_room, + skill_check + } }; use std::time; @@ -43,6 +52,97 @@ pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, ar Ok(()) } +pub async fn attempt_move_immediate( + trans: &DBTrans, + mover: &Item, + direction: &Direction, + mut player_ctx: Option<&mut VerbContext<'_>> +) -> UResult<()> { + let (heretype, herecode) = 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())? + } + let room = room::room_map_by_code().get(herecode) + .ok_or_else(|| UserError("Can't find your current location".to_owned()))?; + let exit = room.exits.iter().find(|ex| ex.direction == *direction) + .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; + + 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? { + user_error("Stopping movement".to_owned())?; + } + } + } + } + + let new_room = + room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?; + + match mover.active_combat.as_ref().and_then(|ac| ac.attacking.clone()) { + None => {} + 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?; + } + } + } + } + match mover.active_combat.as_ref().map(|ac| &ac.attacked_by[..]) { + None | Some([]) => {} + Some(attackers) => { + let mut attacker_names = Vec::new(); + let mut attacker_items = Vec::new(); + if let Some(ctx) = player_ctx.as_ref() { + for attacker in &attackers[..] { + if let Some((acode, atype)) = attacker.split_once("/") { + if let Some(aitem) = trans.find_item_by_type_code(acode, atype).await? { + attacker_names.push(aitem.display_for_session(ctx.session_dat)); + attacker_items.push(aitem); + } + } + } + } + let attacker_names_ref = attacker_names.iter().map(|n| n.as_str()).collect::>(); + let attacker_names_str = language::join_words(&attacker_names_ref[..]); + if skill_check(mover, &SkillType::Dodge, attackers.len() as i64) >= 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?; + } + } + } else { + user_error(format!("You try and fail to run past {}", &attacker_names_str))?; + } + } + } + + 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?; + + if let Some(ctx) = player_ctx { + look::VERB.handle(ctx, "look", "").await?; + } + + 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?; + } + } + + Ok(()) +} + pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { @@ -59,40 +159,7 @@ impl QueueCommandHandler for QueueHandler { _ => user_error("Unexpected command".to_owned())? }; let player_item = get_player_item_or_fail(ctx).await?; - let (heretype, herecode) = player_item.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())? - } - let room = room::room_map_by_code().get(herecode) - .ok_or_else(|| UserError("Can't find your current location".to_owned()))?; - let exit = room.exits.iter().find(|ex| ex.direction == *direction) - .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; - - match exit.exit_type { - ExitType::Free => {} - ExitType::Blocked(blocker) => { - if !blocker.attempt_exit(ctx, &player_item, exit).await? { - user_error("Stopping movement".to_owned())?; - } - } - } - - let new_room = - room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?; - let mut new_player_item = (*player_item).clone(); - new_player_item.location = format!("{}/{}", "room", new_room.code); - ctx.trans.save_item_model(&new_player_item).await?; - - - look::VERB.handle(ctx, "look", "").await?; - - if let Some(old_room_item) = ctx.trans.find_item_by_type_code("room", room.code).await? { - if let Some(new_room_item) = ctx.trans.find_item_by_type_code("room", new_room.code).await? { - announce_move(&ctx.trans, &new_player_item, &old_room_item, &new_room_item).await?; - } - } - + attempt_move_immediate(ctx.trans, &player_item, direction, Some(ctx)).await?; Ok(()) } } diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 7b4a7f1..e429e52 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -1,7 +1,7 @@ use serde::{Serialize, Deserialize}; use std::collections::BTreeMap; use crate::language; -use super::{user::{SkillType, StatType}, session::Session}; +use super::session::Session; #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum BuffCause { @@ -22,6 +22,52 @@ pub struct Buff { impacts: Vec } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum SkillType { + Apraise, + Blades, + Bombs, + Chemistry, + Climb, + Clubs, + Craft, + Dodge, + Fish, + Fists, + Flails, + Fuck, + Hack, + Locksmith, + Medic, + Persuade, + Pilot, + Pistols, + Quickdraw, + Repair, + Ride, + Rifles, + Scavenge, + Science, + Sneak, + Spears, + Swim, + Teach, + Throw, + Track, + Wrestle, + Whips +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum StatType { + Brains, + Senses, + Brawn, + Reflexes, + Endurance, + Cool +} + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct Pronouns { pub subject: String, diff --git a/blastmud_game/src/models/user.rs b/blastmud_game/src/models/user.rs index fa1b0ea..5fedcd7 100644 --- a/blastmud_game/src/models/user.rs +++ b/blastmud_game/src/models/user.rs @@ -1,5 +1,6 @@ use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; +use super::item::{SkillType, StatType}; use std::collections::BTreeMap; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -17,51 +18,6 @@ pub struct UserExperienceData { pub crafted_items: BTreeMap } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum SkillType { - Apraise, - Blades, - Bombs, - Chemistry, - Climb, - Clubs, - Craft, - Fish, - Fists, - Flails, - Fuck, - Hack, - Locksmith, - Medic, - Persuade, - Pilot, - Pistols, - Quickdraw, - Repair, - Ride, - Rifles, - Scavenge, - Science, - Sneak, - Spears, - Swim, - Teach, - Throw, - Track, - Wrestle, - Whips -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum StatType { - Brains, - Senses, - Brawn, - Reflexes, - Endurance, - Cool -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct User { pub username: String, diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index 3663da6..9b85c7a 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -1,8 +1,9 @@ use crate::{ - models::item::Item, + models::item::{Item, SkillType}, db::DBTrans, DResult }; +use rand::{self, Rng}; 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,3 +23,20 @@ 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::() - 1.0 / (1.0 + (-K * (level_gap as f64)).exp()) +} diff --git a/blastmud_game/src/static_content/npc/statbot.rs b/blastmud_game/src/static_content/npc/statbot.rs index 947e033..f0a045d 100644 --- a/blastmud_game/src/static_content/npc/statbot.rs +++ b/blastmud_game/src/static_content/npc/statbot.rs @@ -8,8 +8,8 @@ use crate::message_handler::user_commands::{ }; use async_trait::async_trait; use crate::models::{ - item::{Item, Sex, Pronouns}, - user::{User, StatType}, + item::{Item, Sex, Pronouns, StatType}, + user::{User}, session::Session }; use ansi::ansi;