forked from blasthavers/blastmud
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
#[test]
|
#[test]
|
||||||
@ -96,4 +104,17 @@ mod test {
|
|||||||
assert_eq!(super::caps_first(inp), outp);
|
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 crate::models::{item::{Item, LocationActionType, Subattack}};
|
||||||
use async_recursion::async_recursion;
|
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_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_exp = String::new();
|
||||||
let mut msg_nonexp = String::new();
|
let mut msg_nonexp = String::new();
|
||||||
let mut verb: String = "attacks".to_string();
|
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))?,
|
user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?,
|
||||||
Some((cur_type, cur_code)) => {
|
Some((cur_type, cur_code)) => {
|
||||||
if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? {
|
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();
|
stop_attacking(trans, by_whom, &cur_item_arc).await?;
|
||||||
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?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use super::{
|
use super::{
|
||||||
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
|
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
|
||||||
get_player_item_or_fail,
|
get_player_item_or_fail,
|
||||||
look
|
look,
|
||||||
|
attack::stop_attacking,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use crate::{
|
use crate::{
|
||||||
DResult,
|
DResult,
|
||||||
|
language,
|
||||||
regular_tasks::queued_command::{
|
regular_tasks::queued_command::{
|
||||||
QueueCommandHandler,
|
QueueCommandHandler,
|
||||||
QueueCommand,
|
QueueCommand,
|
||||||
@ -13,8 +15,15 @@ use crate::{
|
|||||||
},
|
},
|
||||||
static_content::room::{self, Direction, ExitType},
|
static_content::room::{self, Direction, ExitType},
|
||||||
db::DBTrans,
|
db::DBTrans,
|
||||||
models::item::Item,
|
models::item::{
|
||||||
services::broadcast_to_room,
|
Item,
|
||||||
|
SkillType,
|
||||||
|
LocationActionType
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
broadcast_to_room,
|
||||||
|
skill_check
|
||||||
|
}
|
||||||
};
|
};
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
@ -43,6 +52,97 @@ pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, ar
|
|||||||
Ok(())
|
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;
|
pub struct QueueHandler;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl QueueCommandHandler for QueueHandler {
|
impl QueueCommandHandler for QueueHandler {
|
||||||
@ -59,40 +159,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
_ => user_error("Unexpected command".to_owned())?
|
_ => user_error("Unexpected command".to_owned())?
|
||||||
};
|
};
|
||||||
let player_item = get_player_item_or_fail(ctx).await?;
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
|
attempt_move_immediate(ctx.trans, &player_item, direction, Some(ctx)).await?;
|
||||||
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?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use crate::language;
|
use crate::language;
|
||||||
use super::{user::{SkillType, StatType}, session::Session};
|
use super::session::Session;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub enum BuffCause {
|
pub enum BuffCause {
|
||||||
@ -22,6 +22,52 @@ pub struct Buff {
|
|||||||
impacts: Vec<BuffImpact>
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub struct Pronouns {
|
pub struct Pronouns {
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use super::item::{SkillType, StatType};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -17,51 +18,6 @@ pub struct UserExperienceData {
|
|||||||
pub crafted_items: BTreeMap<String, u64>
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::item::Item,
|
models::item::{Item, SkillType},
|
||||||
db::DBTrans,
|
db::DBTrans,
|
||||||
DResult
|
DResult
|
||||||
};
|
};
|
||||||
|
use rand::{self, Rng};
|
||||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
||||||
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
||||||
for item in trans.find_items_by_location(location).await? {
|
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(())
|
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 async_trait::async_trait;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
item::{Item, Sex, Pronouns},
|
item::{Item, Sex, Pronouns, StatType},
|
||||||
user::{User, StatType},
|
user::{User},
|
||||||
session::Session
|
session::Session
|
||||||
};
|
};
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
|
Loading…
Reference in New Issue
Block a user