Implement skillcheck to escape combat.
This commit is contained in:
parent
5d3c8bc0aa
commit
8efe2dc87a
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user