Implement skillcheck to escape combat.

This commit is contained in:
Condorra 2023-01-15 23:16:02 +11:00
parent 5d3c8bc0aa
commit 8efe2dc87a
7 changed files with 213 additions and 93 deletions

View File

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

View File

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

View File

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

View File

@ -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<BuffImpact>
}
#[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,

View File

@ -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<String, u64>
}
#[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,

View File

@ -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::<f64>() - 1.0 / (1.0 + (-K * (level_gap as f64)).exp())
}

View File

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