forked from blasthavers/blastmud
Make statbot start to reply to whispers.
This commit is contained in:
parent
79fea1f3b5
commit
3ad4de8f5f
@ -8,10 +8,11 @@ use uuid::Uuid;
|
|||||||
use tokio_postgres::NoTls;
|
use tokio_postgres::NoTls;
|
||||||
use crate::message_handler::ListenerSession;
|
use crate::message_handler::ListenerSession;
|
||||||
use crate::DResult;
|
use crate::DResult;
|
||||||
|
use crate::message_handler::user_commands::parsing::parse_offset;
|
||||||
use crate::models::{session::Session, user::User, item::Item};
|
use crate::models::{session::Session, user::User, item::Item};
|
||||||
use tokio_postgres::types::ToSql;
|
use tokio_postgres::types::ToSql;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
use std::sync::Arc;
|
||||||
use serde_json::{self, Value};
|
use serde_json::{self, Value};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
|
||||||
@ -175,6 +176,29 @@ impl DBPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ItemSearchParams<'l> {
|
||||||
|
pub from_item: &'l Item,
|
||||||
|
pub query: &'l str,
|
||||||
|
pub include_contents: bool,
|
||||||
|
pub include_loc_contents: bool,
|
||||||
|
pub include_active_players: bool,
|
||||||
|
pub include_all_players: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemSearchParams<'_> {
|
||||||
|
pub fn base<'l>(from_item: &'l Item, query: &'l str) -> ItemSearchParams<'l> {
|
||||||
|
ItemSearchParams {
|
||||||
|
from_item, query,
|
||||||
|
include_contents: false,
|
||||||
|
include_loc_contents: false,
|
||||||
|
include_active_players: false,
|
||||||
|
include_all_players: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl DBTrans {
|
impl DBTrans {
|
||||||
pub async fn queue_for_session(self: &Self,
|
pub async fn queue_for_session(self: &Self,
|
||||||
session: &ListenerSession,
|
session: &ListenerSession,
|
||||||
@ -238,7 +262,7 @@ impl DBTrans {
|
|||||||
// Only copy more permanent fields, others are supposed to change over time and shouldn't
|
// Only copy more permanent fields, others are supposed to change over time and shouldn't
|
||||||
// be reset on restart.
|
// be reset on restart.
|
||||||
for to_copy in ["display", "display_less_explicit", "details", "details_less_explicit",
|
for to_copy in ["display", "display_less_explicit", "details", "details_less_explicit",
|
||||||
"total_xp", "total_stats", "total_skills"] {
|
"total_xp", "total_stats", "total_skills", "pronouns"] {
|
||||||
det_ex = format!("jsonb_set({}, '{{{}}}', ${})", det_ex, to_copy, var_id);
|
det_ex = format!("jsonb_set({}, '{{{}}}', ${})", det_ex, to_copy, var_id);
|
||||||
params.push(obj_map.get(to_copy).unwrap_or(&Value::Null));
|
params.push(obj_map.get(to_copy).unwrap_or(&Value::Null));
|
||||||
var_id += 1;
|
var_id += 1;
|
||||||
@ -314,61 +338,74 @@ impl DBTrans {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_item_by_type_code(self: &Self, item_type: &str, item_code: &str) ->
|
pub async fn find_item_by_type_code(self: &Self, item_type: &str, item_code: &str) ->
|
||||||
DResult<Option<Item>> {
|
DResult<Option<Arc<Item>>> {
|
||||||
if let Some(item) = self.pg_trans()?.query_opt(
|
if let Some(item) = self.pg_trans()?.query_opt(
|
||||||
"SELECT details FROM items WHERE \
|
"SELECT details FROM items WHERE \
|
||||||
details->>'item_type' = $1 AND \
|
details->>'item_type' = $1 AND \
|
||||||
details->>'item_code' = $2", &[&item_type, &item_code]).await? {
|
details->>'item_code' = $2", &[&item_type, &item_code]).await? {
|
||||||
return Ok(serde_json::from_value(item.get("details"))?);
|
return Ok(Some(Arc::new(serde_json::from_value::<Item>(item.get("details"))?)));
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_items_by_location(self: &Self, location: &str) -> DResult<Vec<Item>> {
|
pub async fn find_items_by_location(self: &Self, location: &str) -> DResult<Vec<Arc<Item>>> {
|
||||||
Ok(self.pg_trans()?.query(
|
Ok(self.pg_trans()?.query(
|
||||||
"SELECT details FROM items WHERE details->>'location' = $1 \
|
"SELECT details FROM items WHERE details->>'location' = $1 \
|
||||||
LIMIT 20", &[&location]
|
ORDER BY details->>'display'
|
||||||
|
LIMIT 100", &[&location]
|
||||||
).await?.into_iter()
|
).await?.into_iter()
|
||||||
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
||||||
|
.map(Arc::new)
|
||||||
.collect())
|
.collect())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_items_by_display_name_for_player(
|
pub async fn resolve_items_by_display_name_for_player<'l>(
|
||||||
self: &Self,
|
self: &Self,
|
||||||
from_item: &Item,
|
search: &'l ItemSearchParams<'l>
|
||||||
query: &str,
|
) -> DResult<Arc<Vec<Arc<Item>>>> {
|
||||||
include_contents: bool,
|
|
||||||
include_loc_contents: bool,
|
|
||||||
include_active_players: bool,
|
|
||||||
include_all_players: bool
|
|
||||||
) -> DResult<Vec<Item>> {
|
|
||||||
let mut ctes: Vec<String> = Vec::new();
|
let mut ctes: Vec<String> = Vec::new();
|
||||||
let mut include_tables: Vec<&'static str> = Vec::new();
|
let mut include_tables: Vec<&'static str> = Vec::new();
|
||||||
|
|
||||||
let player_loc = &from_item.location;
|
let player_loc = &search.from_item.location;
|
||||||
let player_desig = format!("{}/{}", from_item.item_type,
|
let player_desig = format!("{}/{}", search.from_item.item_type,
|
||||||
from_item.item_code);
|
search.from_item.item_code);
|
||||||
if include_contents {
|
|
||||||
ctes.push("contents AS (\
|
let (offset, query) = parse_offset(search.query);
|
||||||
SELECT details FROM items WHERE details->>'location' = $1
|
let mut param_no: usize = 3;
|
||||||
)".to_owned());
|
let query_wildcard = query.replace("\\", "\\\\")
|
||||||
|
.replace("_", "\\_")
|
||||||
|
.replace("%", "")
|
||||||
|
.to_lowercase() + "%";
|
||||||
|
let offset_sql = offset.map(|x| (if x >= 1 { x - 1 } else { x}) as i64).unwrap_or(0);
|
||||||
|
let mut params: Vec<&(dyn ToSql + Sync)> = vec!(
|
||||||
|
&query_wildcard,
|
||||||
|
&offset_sql);
|
||||||
|
|
||||||
|
|
||||||
|
if search.include_contents {
|
||||||
|
ctes.push(format!("contents AS (\
|
||||||
|
SELECT details FROM items WHERE details->>'location' = ${}\
|
||||||
|
)", param_no));
|
||||||
|
param_no += 1;
|
||||||
|
params.push(&player_desig);
|
||||||
include_tables.push("SELECT details FROM contents");
|
include_tables.push("SELECT details FROM contents");
|
||||||
}
|
}
|
||||||
if include_loc_contents {
|
if search.include_loc_contents {
|
||||||
ctes.push("loc_contents AS (\
|
ctes.push(format!("loc_contents AS (\
|
||||||
SELECT details FROM items WHERE details->>'location' = $2
|
SELECT details FROM items WHERE details->>'location' = ${}\
|
||||||
)".to_owned());
|
)", param_no));
|
||||||
|
drop(param_no); // or increment if this is a problem.
|
||||||
|
params.push(&player_loc);
|
||||||
include_tables.push("SELECT details FROM loc_contents");
|
include_tables.push("SELECT details FROM loc_contents");
|
||||||
}
|
}
|
||||||
if include_active_players {
|
if search.include_active_players {
|
||||||
ctes.push("active_players AS (\
|
ctes.push("active_players AS (\
|
||||||
SELECT details FROM items WHERE details->>'item_type' = 'player' \
|
SELECT details FROM items WHERE details->>'item_type' = 'player' \
|
||||||
AND current_session IS NOT NULL \
|
AND current_session IS NOT NULL \
|
||||||
)".to_owned());
|
)".to_owned());
|
||||||
include_tables.push("SELECT details FROM active_players");
|
include_tables.push("SELECT details FROM active_players");
|
||||||
}
|
}
|
||||||
if include_all_players {
|
if search.include_all_players {
|
||||||
ctes.push("all_players AS (\
|
ctes.push("all_players AS (\
|
||||||
SELECT details FROM items WHERE details->>'item_type' = 'player'
|
SELECT details FROM items WHERE details->>'item_type' = 'player'
|
||||||
)".to_owned());
|
)".to_owned());
|
||||||
@ -378,20 +415,17 @@ impl DBTrans {
|
|||||||
|
|
||||||
let cte_str: String = ctes.join(", ");
|
let cte_str: String = ctes.join(", ");
|
||||||
|
|
||||||
Ok(self.pg_trans()?.query(
|
Ok(Arc::new(self.pg_trans()?.query(
|
||||||
&format!(
|
&format!(
|
||||||
"WITH {} SELECT details FROM relevant_items WHERE (lower(details->>'display') LIKE $3) \
|
"WITH {} SELECT details FROM relevant_items WHERE (lower(details->>'display') LIKE $1) \
|
||||||
OR (lower(details ->>'display_less_explicit') LIKE $3) \
|
OR (lower(details ->>'display_less_explicit') LIKE $1) \
|
||||||
ORDER BY length(details->>'display') DESC \
|
ORDER BY length(details->>'display') DESC \
|
||||||
LIMIT 2", &cte_str),
|
LIMIT 2 OFFSET $2", &cte_str),
|
||||||
&[&player_desig, &player_loc,
|
¶ms
|
||||||
&(query.replace("\\", "\\\\")
|
|
||||||
.replace("_", "\\_")
|
|
||||||
.replace("%", "")
|
|
||||||
.to_lowercase() + "%")]
|
|
||||||
).await?.into_iter()
|
).await?.into_iter()
|
||||||
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
||||||
.collect())
|
.map(Arc::new)
|
||||||
|
.collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn commit(mut self: Self) -> DResult<()> {
|
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||||
|
79
blastmud_game/src/language.rs
Normal file
79
blastmud_game/src/language.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
struct PluralRule<'l> {
|
||||||
|
match_suffix: &'l str,
|
||||||
|
drop: usize,
|
||||||
|
append_suffix: &'l str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pluralise(input: &str) -> String {
|
||||||
|
static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
|
||||||
|
let plural_rules = PLURAL_RULES.get_or_init(|| vec!(
|
||||||
|
PluralRule { match_suffix: "foot", drop: 3, append_suffix: "eet" },
|
||||||
|
PluralRule { match_suffix: "tooth", drop: 4, append_suffix: "eeth" },
|
||||||
|
PluralRule { match_suffix: "man", drop: 2, append_suffix: "en" },
|
||||||
|
PluralRule { match_suffix: "mouse", drop: 4, append_suffix: "ice" },
|
||||||
|
PluralRule { match_suffix: "louse", drop: 4, append_suffix: "ice" },
|
||||||
|
PluralRule { match_suffix: "fish", drop: 0, append_suffix: "" },
|
||||||
|
PluralRule { match_suffix: "sheep", drop: 0, append_suffix: "" },
|
||||||
|
PluralRule { match_suffix: "deer", drop: 0, append_suffix: "" },
|
||||||
|
PluralRule { match_suffix: "pox", drop: 0, append_suffix: "" },
|
||||||
|
PluralRule { match_suffix: "cis", drop: 2, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "sis", drop: 2, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "xis", drop: 2, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "ss", drop: 0, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "ch", drop: 0, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "sh", drop: 0, append_suffix: "es" },
|
||||||
|
PluralRule { match_suffix: "ife", drop: 2, append_suffix: "ves" },
|
||||||
|
PluralRule { match_suffix: "lf", drop: 1, append_suffix: "ves" },
|
||||||
|
PluralRule { match_suffix: "arf", drop: 1, append_suffix: "ves" },
|
||||||
|
PluralRule { match_suffix: "ay", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "ey", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "iy", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "oy", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "uy", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "y", drop: 1, append_suffix: "ies" },
|
||||||
|
PluralRule { match_suffix: "ao", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "eo", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "io", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "oo", drop: 0, append_suffix: "s" },
|
||||||
|
PluralRule { match_suffix: "uo", drop: 0, append_suffix: "s" },
|
||||||
|
// The o rule could be much larger... we'll add specific exceptions as
|
||||||
|
// the come up.
|
||||||
|
PluralRule { match_suffix: "o", drop: 0, append_suffix: "es" },
|
||||||
|
// Lots of possible exceptions here.
|
||||||
|
PluralRule { match_suffix: "ex", drop: 0, append_suffix: "es" },
|
||||||
|
));
|
||||||
|
|
||||||
|
for rule in plural_rules {
|
||||||
|
if input.ends_with(rule.match_suffix) {
|
||||||
|
return input[0..(input.len() - rule.drop)].to_owned() + rule.append_suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.to_owned() + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn pluralise_should_follow_english_rules() {
|
||||||
|
for (word, plural) in vec!(
|
||||||
|
("cat", "cats"),
|
||||||
|
("wolf", "wolves"),
|
||||||
|
("scarf", "scarves"),
|
||||||
|
("volcano", "volcanoes"),
|
||||||
|
("canoe", "canoes"),
|
||||||
|
("pistachio", "pistachios"),
|
||||||
|
("match", "matches"),
|
||||||
|
("the fairest sex", "the fairest sexes"),
|
||||||
|
("loud hiss", "loud hisses"),
|
||||||
|
("evil axis", "evil axes"),
|
||||||
|
("death ray", "death rays"),
|
||||||
|
("killer blowfly", "killer blowflies"),
|
||||||
|
("house mouse", "house mice"),
|
||||||
|
("zombie sheep", "zombie sheep"),
|
||||||
|
) {
|
||||||
|
assert_eq!(super::pluralise(word), plural);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ mod av;
|
|||||||
mod regular_tasks;
|
mod regular_tasks;
|
||||||
mod models;
|
mod models;
|
||||||
mod static_content;
|
mod static_content;
|
||||||
|
mod language;
|
||||||
|
|
||||||
pub type DResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
|
pub type DResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||||||
use crate::DResult;
|
use crate::DResult;
|
||||||
|
|
||||||
mod new_session;
|
mod new_session;
|
||||||
mod user_commands;
|
pub mod user_commands;
|
||||||
|
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ListenerSession {
|
pub struct ListenerSession {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use super::ListenerSession;
|
use super::ListenerSession;
|
||||||
use crate::DResult;
|
use crate::DResult;
|
||||||
use crate::db::{DBTrans, DBPool};
|
use crate::db::{DBTrans, DBPool, ItemSearchParams};
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
use phf::phf_map;
|
use phf::phf_map;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use crate::models::{session::Session, user::User};
|
use crate::models::{session::Session, user::User, item::Item};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod agree;
|
mod agree;
|
||||||
mod help;
|
mod help;
|
||||||
@ -13,15 +15,16 @@ mod ignore;
|
|||||||
mod less_explicit_mode;
|
mod less_explicit_mode;
|
||||||
mod login;
|
mod login;
|
||||||
mod look;
|
mod look;
|
||||||
mod parsing;
|
pub mod parsing;
|
||||||
mod quit;
|
mod quit;
|
||||||
mod register;
|
mod register;
|
||||||
|
mod whisper;
|
||||||
|
|
||||||
pub struct VerbContext<'l> {
|
pub struct VerbContext<'l> {
|
||||||
session: &'l ListenerSession,
|
pub session: &'l ListenerSession,
|
||||||
session_dat: &'l mut Session,
|
pub session_dat: &'l mut Session,
|
||||||
user_dat: &'l mut Option<User>,
|
pub user_dat: &'l mut Option<User>,
|
||||||
trans: &'l DBTrans
|
pub trans: &'l DBTrans
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CommandHandlingError {
|
pub enum CommandHandlingError {
|
||||||
@ -70,16 +73,10 @@ static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||||
"l" => look::VERB,
|
"l" => look::VERB,
|
||||||
"look" => look::VERB,
|
"look" => look::VERB,
|
||||||
|
"-" => whisper::VERB,
|
||||||
|
"whisper" => whisper::VERB,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn explicit_if_allowed<'l>(ctx: &VerbContext, explicit: &'l str, non_explicit: Option<&'l str>) -> &'l str {
|
|
||||||
if ctx.session_dat.less_explicit_mode {
|
|
||||||
non_explicit.unwrap_or(explicit)
|
|
||||||
} else {
|
|
||||||
explicit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
||||||
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
||||||
|
|
||||||
@ -139,3 +136,38 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_likely_explicit(msg: &str) -> bool {
|
||||||
|
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> =
|
||||||
|
OnceCell::new();
|
||||||
|
let markers = EXPLICIT_MARKER_WORDS.get_or_init(||
|
||||||
|
vec!("fuck", "sex", "cock", "cunt", "dick", "pussy", "whore",
|
||||||
|
"orgasm", "erection", "nipple", "boob", "tit"));
|
||||||
|
for word in markers {
|
||||||
|
if msg.contains(word) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
|
||||||
|
ctx.user_dat.as_ref()
|
||||||
|
.ok_or_else(|| UserError("Not logged in".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
|
||||||
|
Ok(ctx.trans.find_item_by_type_code(
|
||||||
|
"player", &get_user_or_fail(ctx)?.username.to_lowercase()).await?
|
||||||
|
.ok_or_else(|| UserError("Your player is gone, you'll need to re-register or ask an admin".to_owned()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_item_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) ->
|
||||||
|
UResult<Arc<Item>> {
|
||||||
|
Ok(match &ctx.trans.resolve_items_by_display_name_for_player(search).await?[..] {
|
||||||
|
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
|
||||||
|
[match_it] => match_it.clone(),
|
||||||
|
[item1, ..] =>
|
||||||
|
item1.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, explicit_if_allowed};
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
|
||||||
|
get_player_item_or_fail, search_item_for_user};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use ansi::{ansi, flow_around, word_wrap};
|
use ansi::{ansi, flow_around, word_wrap};
|
||||||
use crate::models::{user::User, item::{Item, LocationActionType, Subattack}};
|
use crate::db::ItemSearchParams;
|
||||||
|
use crate::models::{item::{Item, LocationActionType, Subattack}};
|
||||||
use crate::static_content::room::{self, Direction};
|
use crate::static_content::room::{self, Direction};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use std::sync::Arc;
|
||||||
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
|
use crate::language::pluralise;
|
||||||
ctx.user_dat.as_ref()
|
|
||||||
.ok_or_else(|| UserError("Not logged in".to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Item> {
|
|
||||||
Ok(ctx.trans.find_item_by_type_code(
|
|
||||||
"player", &get_user_or_fail(ctx)?.username.to_lowercase()).await?
|
|
||||||
.ok_or_else(|| UserError("Your player is gone, you'll need to re-register or ask an admin".to_owned()))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
|
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
@ -43,14 +36,8 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
|
|||||||
ctx.trans.queue_for_session(
|
ctx.trans.queue_for_session(
|
||||||
ctx.session,
|
ctx.session,
|
||||||
Some(&format!("{}\n{}\n",
|
Some(&format!("{}\n{}\n",
|
||||||
explicit_if_allowed(
|
item.display_for_session(&ctx.session_dat),
|
||||||
ctx,
|
item.details_for_session(&ctx.session_dat).unwrap_or("")
|
||||||
&item.display,
|
|
||||||
item.display_less_explicit.as_ref().map(|s|&**s)),
|
|
||||||
explicit_if_allowed(
|
|
||||||
ctx,
|
|
||||||
item.details.as_ref().map(|v|&**v).unwrap_or(""),
|
|
||||||
item.details_less_explicit.as_ref().map(|s|&**s)),
|
|
||||||
))
|
))
|
||||||
).await?;
|
).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -63,14 +50,17 @@ fn exits_for(room: &room::Room) -> String {
|
|||||||
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
|
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn describe_room(ctx: &VerbContext<'_>, room: &room::Room, contents: &str) -> UResult<()> {
|
pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item,
|
||||||
|
room: &room::Room, contents: &str) -> UResult<()> {
|
||||||
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
|
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
|
||||||
ctx.trans.queue_for_session(
|
ctx.trans.queue_for_session(
|
||||||
ctx.session,
|
ctx.session,
|
||||||
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
|
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
|
||||||
&word_wrap(&format!("{} ({})\n{}.{}\n{}\n", room.name, zone,
|
&word_wrap(&format!("{} ({})\n{}.{}\n{}\n",
|
||||||
explicit_if_allowed(ctx, room.description,
|
item.display_for_session(&ctx.session_dat),
|
||||||
room.description_less_explicit),
|
zone,
|
||||||
|
item.details_for_session(
|
||||||
|
&ctx.session_dat).unwrap_or(""),
|
||||||
contents, exits_for(room)),
|
contents, exits_for(room)),
|
||||||
|row| if row >= 5 { 80 } else { 68 }), 68))
|
|row| if row >= 5 { 80 } else { 68 }), 68))
|
||||||
).await?;
|
).await?;
|
||||||
@ -83,12 +73,12 @@ async fn list_item_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
|
|||||||
item.item_type, item.item_code)).await?;
|
item.item_type, item.item_code)).await?;
|
||||||
items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display));
|
items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display));
|
||||||
|
|
||||||
let all_groups: Vec<Vec<&Item>> = items
|
let all_groups: Vec<Vec<&Arc<Item>>> = items
|
||||||
.iter()
|
.iter()
|
||||||
.group_by(|i| &i.display)
|
.group_by(|i| &i.display)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, g)|g.collect::<Vec<&Item>>())
|
.map(|(_, g)|g.collect::<Vec<&Arc<Item>>>())
|
||||||
.collect::<Vec<Vec<&Item>>>();
|
.collect::<Vec<Vec<&Arc<Item>>>>();
|
||||||
|
|
||||||
for group_items in all_groups {
|
for group_items in all_groups {
|
||||||
let head = &group_items[0];
|
let head = &group_items[0];
|
||||||
@ -96,14 +86,15 @@ async fn list_item_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
|
|||||||
buf.push(' ');
|
buf.push(' ');
|
||||||
if group_items.len() > 1 {
|
if group_items.len() > 1 {
|
||||||
buf.push_str(&format!("{} ", group_items.len()))
|
buf.push_str(&format!("{} ", group_items.len()))
|
||||||
} else if !is_creature {
|
} else if !head.pronouns.is_proper {
|
||||||
buf.push_str("A ");
|
buf.push_str("A ");
|
||||||
}
|
}
|
||||||
buf.push_str(
|
let mut disp = head.display_for_session(&ctx.session_dat).to_owned();
|
||||||
&explicit_if_allowed(ctx,
|
if group_items.len() > 1 {
|
||||||
&head.display,
|
disp = pluralise(&disp);
|
||||||
head.display_less_explicit.as_ref().map(|v|&**v)));
|
}
|
||||||
buf.push_str(" is ");
|
buf.push_str(&disp);
|
||||||
|
buf.push_str(if group_items.len() > 1 { " are " } else { " is "});
|
||||||
match head.action_type {
|
match head.action_type {
|
||||||
LocationActionType::Sitting => buf.push_str("sitting "),
|
LocationActionType::Sitting => buf.push_str("sitting "),
|
||||||
LocationActionType::Reclining => buf.push_str("reclining "),
|
LocationActionType::Reclining => buf.push_str("reclining "),
|
||||||
@ -128,9 +119,7 @@ async fn list_item_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
|
|||||||
match ctx.trans.find_item_by_type_code(ttype, tcode).await? {
|
match ctx.trans.find_item_by_type_code(ttype, tcode).await? {
|
||||||
None => buf.push_str("someone"),
|
None => buf.push_str("someone"),
|
||||||
Some(it) => buf.push_str(
|
Some(it) => buf.push_str(
|
||||||
&explicit_if_allowed(ctx,
|
it.display_for_session(&ctx.session_dat)
|
||||||
&it.display,
|
|
||||||
it.display_less_explicit.as_ref().map(|v|&**v))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,52 +138,50 @@ impl UserVerb for Verb {
|
|||||||
|
|
||||||
let rem_trim = remaining.trim().to_lowercase();
|
let rem_trim = remaining.trim().to_lowercase();
|
||||||
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
|
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
|
||||||
let (itype, icode): (String, String) = if rem_trim == "" {
|
let item: Arc<Item> = if rem_trim == "" {
|
||||||
Ok((heretype.to_owned(), herecode.to_owned()))
|
ctx.trans.find_item_by_type_code(heretype, herecode).await?
|
||||||
|
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||||
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
||||||
if heretype != "room" {
|
if heretype != "room" {
|
||||||
// Fix this when we have planes / boats / roomkits.
|
// Fix this when we have planes / boats / roomkits.
|
||||||
user_error("Navigating outside rooms not yet supported.".to_owned())
|
user_error("Navigating outside rooms not yet supported.".to_owned())?
|
||||||
} else {
|
} else {
|
||||||
if let Some(room) = room::room_map_by_code().get(herecode) {
|
if let Some(room) = room::room_map_by_code().get(herecode) {
|
||||||
match room.exits.iter().find(|ex| ex.direction == *dir) {
|
match room.exits.iter().find(|ex| ex.direction == *dir) {
|
||||||
None => user_error("There is nothing in that direction".to_owned()),
|
None => user_error("There is nothing in that direction".to_owned())?,
|
||||||
Some(exit) => {
|
Some(exit) => {
|
||||||
match room::resolve_exit(room, exit) {
|
match room::resolve_exit(room, exit) {
|
||||||
None => user_error("There is nothing in that direction".to_owned()),
|
None => user_error("There is nothing in that direction".to_owned())?,
|
||||||
Some(room2) => Ok(("room".to_owned(), room2.code.to_owned()))
|
Some(room2) =>
|
||||||
|
ctx.trans.find_item_by_type_code("room", room2.code).await?
|
||||||
|
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("Can't find your current location".to_owned())
|
user_error("Can't find your current location".to_owned())?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if rem_trim == "me" || rem_trim == "self" {
|
} else if rem_trim == "me" || rem_trim == "self" {
|
||||||
Ok((player_item.item_type.clone(), player_item.item_code.clone()))
|
player_item.clone()
|
||||||
} else {
|
} else {
|
||||||
match &ctx.trans.resolve_items_by_display_name_for_player(
|
search_item_for_user(
|
||||||
&player_item,
|
&ctx,
|
||||||
&rem_trim,
|
&ItemSearchParams {
|
||||||
true, true, false, false
|
include_contents: true,
|
||||||
).await?[..] {
|
include_loc_contents: true,
|
||||||
[] => user_error("Sorry, I couldn't find anything matching.".to_owned()),
|
..ItemSearchParams::base(&player_item, &rem_trim)
|
||||||
[match_it] => Ok((match_it.item_type.clone(), match_it.item_code.clone())),
|
}
|
||||||
[item1, ..] if item1.display.to_lowercase() == rem_trim ||
|
).await?
|
||||||
item1.display_less_explicit.as_ref().map(|x|x.to_lowercase()) == Some(rem_trim) =>
|
};
|
||||||
Ok((item1.item_type.clone(), item1.item_code.clone())),
|
if item.item_type != "room" {
|
||||||
_ => user_error("Sorry, that name is ambiguous, please be more specific.".to_owned())
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
let item = ctx.trans.find_item_by_type_code(&itype, &icode).await?
|
|
||||||
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?;
|
|
||||||
if itype != "room" {
|
|
||||||
describe_normal_item(ctx, &item).await?;
|
describe_normal_item(ctx, &item).await?;
|
||||||
} else {
|
} else {
|
||||||
let room =
|
let room =
|
||||||
room::room_map_by_code().get(icode.as_str())
|
room::room_map_by_code().get(item.item_code.as_str())
|
||||||
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
|
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
|
||||||
describe_room(ctx, &room, &list_item_contents(ctx, &item).await?).await?;
|
describe_room(ctx, &item, &room, &list_item_contents(ctx, &item).await?).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nom::{
|
use nom::{
|
||||||
bytes::complete::{take_till1, take_while},
|
bytes::complete::{take_till, take_till1, take_while},
|
||||||
character::{complete::{space0, space1, alpha1, one_of}},
|
character::{complete::{space0, space1, alpha1, one_of, char, u8}},
|
||||||
combinator::{recognize, fail, eof},
|
combinator::{recognize, fail, eof},
|
||||||
sequence::terminated,
|
sequence::terminated,
|
||||||
branch::alt,
|
branch::alt,
|
||||||
@ -25,6 +25,26 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_to_space(input: &str) -> (&str, &str) {
|
||||||
|
fn parser(input: &str) -> IResult<&str, &str> {
|
||||||
|
terminated(take_till(|c| c == ' ' || c == '\t'), alt((space1, eof)))(input)
|
||||||
|
}
|
||||||
|
match parser(input) {
|
||||||
|
Err(_) => ("", ""), /* Impossible? */
|
||||||
|
Ok((rest, token)) => (token, rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_offset(input: &str) -> (Option<u8>, &str) {
|
||||||
|
fn parser(input: &str) -> IResult<&str, u8> {
|
||||||
|
terminated(u8, char('.'))(input)
|
||||||
|
}
|
||||||
|
match parser(input) {
|
||||||
|
Err(_) => (None, input),
|
||||||
|
Ok((rest, result)) => (Some(result), rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> {
|
pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> {
|
||||||
const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _";
|
const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _";
|
||||||
fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> {
|
fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> {
|
||||||
@ -121,4 +141,27 @@ mod tests {
|
|||||||
fn it_fails_on_long_usernames() {
|
fn it_fails_on_long_usernames() {
|
||||||
assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters"));
|
assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_to_space_splits_on_whitespace() {
|
||||||
|
assert_eq!(parse_to_space("hello world"), ("hello", "world"));
|
||||||
|
assert_eq!(parse_to_space("hello\tworld"), ("hello", "world"));
|
||||||
|
assert_eq!(parse_to_space("hello world"), ("hello", "world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_to_space_supports_missing_rest() {
|
||||||
|
assert_eq!(parse_to_space("hello"), ("hello", ""));
|
||||||
|
assert_eq!(parse_to_space(""), ("", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_offset_supports_no_offset() {
|
||||||
|
assert_eq!(parse_offset("hello world"), (None, "hello world"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_offset_supports_offset() {
|
||||||
|
assert_eq!(parse_offset("2.hello world"), (Some(2), "hello world"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use super::{user_error, parsing::parse_username};
|
use super::{user_error, parsing::parse_username};
|
||||||
use crate::models::{user::User, item::Item};
|
use crate::models::{user::User, item::{Item, Pronouns}};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
@ -36,6 +36,7 @@ impl UserVerb for Verb {
|
|||||||
display: username.to_owned(),
|
display: username.to_owned(),
|
||||||
details: Some("A non-descript individual".to_owned()),
|
details: Some("A non-descript individual".to_owned()),
|
||||||
location: "room/repro_xv_chargen".to_owned(),
|
location: "room/repro_xv_chargen".to_owned(),
|
||||||
|
pronouns: Pronouns::default_animate(),
|
||||||
..Item::default()
|
..Item::default()
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
|
56
blastmud_game/src/message_handler/user_commands/whisper.rs
Normal file
56
blastmud_game/src/message_handler/user_commands/whisper.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
|
||||||
|
ItemSearchParams, user_error,
|
||||||
|
get_player_item_or_fail, is_likely_explicit,
|
||||||
|
search_item_for_user,
|
||||||
|
parsing::parse_to_space};
|
||||||
|
use crate::static_content::npc::npc_by_code;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use ansi::{ignore_special_characters, ansi};
|
||||||
|
|
||||||
|
pub struct Verb;
|
||||||
|
#[async_trait]
|
||||||
|
impl UserVerb for Verb {
|
||||||
|
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||||
|
let (to_whom_name, say_what_raw) = parse_to_space(remaining);
|
||||||
|
let say_what = ignore_special_characters(say_what_raw);
|
||||||
|
if say_what == "" {
|
||||||
|
user_error("You need to provide a message to send.".to_owned())?;
|
||||||
|
}
|
||||||
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
|
let to_whom = search_item_for_user(ctx, &ItemSearchParams {
|
||||||
|
include_loc_contents: true,
|
||||||
|
..ItemSearchParams::base(&player_item, &to_whom_name)
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
match to_whom.item_type.as_str() {
|
||||||
|
"npc" => {}
|
||||||
|
"player" => {},
|
||||||
|
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.trans.queue_for_session(ctx.session, Some(&format!(
|
||||||
|
ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"),
|
||||||
|
player_item.display_for_session(&ctx.session_dat),
|
||||||
|
to_whom.display_for_session(&ctx.session_dat),
|
||||||
|
say_what
|
||||||
|
))).await?;
|
||||||
|
|
||||||
|
match to_whom.item_type.as_str() {
|
||||||
|
"npc" => {
|
||||||
|
let npc = npc_by_code().get(to_whom.item_code.as_str())
|
||||||
|
.map(Ok)
|
||||||
|
.unwrap_or_else(|| user_error("That NPC is no longer available".to_owned()))?;
|
||||||
|
if let Some(handler) = npc.message_handler {
|
||||||
|
handler.handle(ctx, &player_item, &to_whom, &say_what).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"player" => {
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static VERB_INT: Verb = Verb;
|
||||||
|
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use super::user::{SkillType, StatType};
|
use super::{user::{SkillType, StatType}, session::Session};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum BuffCause {
|
pub enum BuffCause {
|
||||||
@ -21,6 +21,66 @@ pub struct Buff {
|
|||||||
impacts: Vec<BuffImpact>
|
impacts: Vec<BuffImpact>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub struct Pronouns {
|
||||||
|
pub subject: String,
|
||||||
|
pub object: String,
|
||||||
|
pub intensive: String,
|
||||||
|
pub possessive: String,
|
||||||
|
// And some miscellaneous details to determine context
|
||||||
|
pub is_plural: bool, // ... are instead of ... is
|
||||||
|
pub is_proper: bool, // When naming, just ... instead of The ...
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pronouns {
|
||||||
|
pub fn default_inanimate() -> Pronouns {
|
||||||
|
Pronouns {
|
||||||
|
subject: "it".to_owned(),
|
||||||
|
object: "it".to_owned(),
|
||||||
|
intensive: "itself".to_owned(),
|
||||||
|
possessive: "its".to_owned(),
|
||||||
|
is_plural: false,
|
||||||
|
is_proper: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_animate() -> Pronouns {
|
||||||
|
Pronouns {
|
||||||
|
subject: "they".to_owned(),
|
||||||
|
object: "them".to_owned(),
|
||||||
|
intensive: "themselves".to_owned(),
|
||||||
|
possessive: "their".to_owned(),
|
||||||
|
is_plural: true,
|
||||||
|
is_proper: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn default_male() -> Pronouns {
|
||||||
|
Pronouns {
|
||||||
|
subject: "he".to_owned(),
|
||||||
|
object: "him".to_owned(),
|
||||||
|
intensive: "himself".to_owned(),
|
||||||
|
possessive: "his".to_owned(),
|
||||||
|
is_plural: false,
|
||||||
|
is_proper: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn default_female() -> Pronouns {
|
||||||
|
Pronouns {
|
||||||
|
subject: "she".to_owned(),
|
||||||
|
object: "her".to_owned(),
|
||||||
|
intensive: "herself".to_owned(),
|
||||||
|
possessive: "her".to_owned(),
|
||||||
|
is_plural: false,
|
||||||
|
is_proper: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub enum Subattack {
|
pub enum Subattack {
|
||||||
Normal,
|
Normal,
|
||||||
@ -57,6 +117,24 @@ pub struct Item {
|
|||||||
pub total_stats: BTreeMap<StatType, u64>,
|
pub total_stats: BTreeMap<StatType, u64>,
|
||||||
pub total_skills: BTreeMap<SkillType, u64>,
|
pub total_skills: BTreeMap<SkillType, u64>,
|
||||||
pub temporary_buffs: Vec<Buff>,
|
pub temporary_buffs: Vec<Buff>,
|
||||||
|
pub pronouns: Pronouns,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn display_for_session<'l>(self: &'l Self, session: &Session) -> &'l str {
|
||||||
|
session.explicit_if_allowed(&self.display,
|
||||||
|
self.display_less_explicit.as_ref().map(String::as_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn details_for_session<'l>(self: &'l Self, session: &Session) -> Option<&'l str>{
|
||||||
|
self.details.as_ref()
|
||||||
|
.map(|dets|
|
||||||
|
session.explicit_if_allowed(
|
||||||
|
dets.as_str(),
|
||||||
|
self.details_less_explicit.as_ref().map(String::as_str)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Item {
|
impl Default for Item {
|
||||||
@ -75,7 +153,8 @@ impl Default for Item {
|
|||||||
total_xp: 0,
|
total_xp: 0,
|
||||||
total_stats: BTreeMap::new(),
|
total_stats: BTreeMap::new(),
|
||||||
total_skills: BTreeMap::new(),
|
total_skills: BTreeMap::new(),
|
||||||
temporary_buffs: Vec::new()
|
temporary_buffs: Vec::new(),
|
||||||
|
pronouns: Pronouns::default_inanimate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,16 @@ pub struct Session {
|
|||||||
// be an Option, or things will crash out for existing sessions.
|
// be an Option, or things will crash out for existing sessions.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn explicit_if_allowed<'l>(self: &Self, explicit: &'l str, non_explicit: Option<&'l str>) -> &'l str {
|
||||||
|
if self.less_explicit_mode {
|
||||||
|
non_explicit.unwrap_or(explicit)
|
||||||
|
} else {
|
||||||
|
explicit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Session {
|
impl Default for Session {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Session { source: "unknown".to_owned(), less_explicit_mode: false }
|
Session { source: "unknown".to_owned(), less_explicit_mode: false }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use crate::static_content::npc::statbot::StatbotState;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct UserTermData {
|
pub struct UserTermData {
|
||||||
@ -75,6 +76,7 @@ pub struct User {
|
|||||||
|
|
||||||
pub terms: UserTermData,
|
pub terms: UserTermData,
|
||||||
pub experience: UserExperienceData,
|
pub experience: UserExperienceData,
|
||||||
|
pub statbot: Option<StatbotState>,
|
||||||
pub raw_skills: BTreeMap<SkillType, u16>,
|
pub raw_skills: BTreeMap<SkillType, u16>,
|
||||||
pub raw_stats: BTreeMap<StatType, u16>,
|
pub raw_stats: BTreeMap<StatType, u16>,
|
||||||
// Reminder: Consider backwards compatibility when updating this. New fields should generally
|
// Reminder: Consider backwards compatibility when updating this. New fields should generally
|
||||||
@ -113,6 +115,7 @@ impl Default for User {
|
|||||||
banned_until: None,
|
banned_until: None,
|
||||||
abandoned_at: None,
|
abandoned_at: None,
|
||||||
chargen_last_completed_at: None,
|
chargen_last_completed_at: None,
|
||||||
|
statbot: None,
|
||||||
|
|
||||||
terms: UserTermData::default(),
|
terms: UserTermData::default(),
|
||||||
experience: UserExperienceData::default(),
|
experience: UserExperienceData::default(),
|
||||||
|
@ -1,26 +1,53 @@
|
|||||||
use super::StaticItem;
|
use super::StaticItem;
|
||||||
use crate::models::item::Item;
|
use crate::models::item::Item;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use crate::message_handler::user_commands::{VerbContext, UResult};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
pub mod statbot;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait NPCMessageHandler {
|
||||||
|
async fn handle(
|
||||||
|
self: &Self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
source: &Item,
|
||||||
|
target: &Item,
|
||||||
|
message: &str
|
||||||
|
) -> UResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct NPC {
|
pub struct NPC {
|
||||||
pub code: &'static str,
|
pub code: &'static str,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub description: &'static str,
|
pub description: &'static str,
|
||||||
pub spawn_location: &'static str,
|
pub spawn_location: &'static str,
|
||||||
|
pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>
|
||||||
}
|
}
|
||||||
|
|
||||||
static NPC_LIST: OnceCell<Vec<NPC>> = OnceCell::new();
|
|
||||||
pub fn npc_list() -> &'static Vec<NPC> {
|
pub fn npc_list() -> &'static Vec<NPC> {
|
||||||
|
static NPC_LIST: OnceCell<Vec<NPC>> = OnceCell::new();
|
||||||
NPC_LIST.get_or_init(|| vec!(
|
NPC_LIST.get_or_init(|| vec!(
|
||||||
NPC {
|
NPC {
|
||||||
code: "repro_xv_chargen_statbot",
|
code: "repro_xv_chargen_statbot",
|
||||||
name: "Statbot",
|
name: "Statbot",
|
||||||
description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.",
|
description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.",
|
||||||
spawn_location: "room/repro_xv_chargen"
|
spawn_location: "room/repro_xv_chargen",
|
||||||
|
message_handler: Some(&statbot::StatbotMessageHandler)
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn npc_by_code() -> &'static BTreeMap<&'static str, &'static NPC> {
|
||||||
|
static NPC_CODE_MAP: OnceCell<BTreeMap<&'static str, &'static NPC>> = OnceCell::new();
|
||||||
|
NPC_CODE_MAP.get_or_init(
|
||||||
|
|| npc_list().iter()
|
||||||
|
.map(|npc| (npc.code, npc))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||||
Box::new(npc_list().iter().map(|c| StaticItem {
|
Box::new(npc_list().iter().map(|c| StaticItem {
|
||||||
item_code: c.code,
|
item_code: c.code,
|
||||||
|
70
blastmud_game/src/static_content/npc/statbot.rs
Normal file
70
blastmud_game/src/static_content/npc/statbot.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use super::NPCMessageHandler;
|
||||||
|
use crate::message_handler::user_commands::{VerbContext, UResult, get_user_or_fail};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::models::{item::Item, user::User, user::StatType};
|
||||||
|
use ansi::ansi;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
pub struct StatbotMessageHandler;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum StatbotState {
|
||||||
|
Brains,
|
||||||
|
Senses,
|
||||||
|
Brawn,
|
||||||
|
Reflexes,
|
||||||
|
Endurance,
|
||||||
|
Cool,
|
||||||
|
AssignGender,
|
||||||
|
SetDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reply(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> {
|
||||||
|
ctx.trans.queue_for_session(
|
||||||
|
ctx.session,
|
||||||
|
Some(&format!(ansi!("Statbot replies in a mechanical voice: <blue>\"{}\"<reset>\n"),
|
||||||
|
msg))
|
||||||
|
).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_action_text(user: &User) -> String {
|
||||||
|
let st = user.statbot.as_ref().unwrap_or(&StatbotState::Brains);
|
||||||
|
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 tot = brn + sen + brw + refl + end + col;
|
||||||
|
let summary = format!("Brains: {}, Senses: {}, Brawn: {}, Reflexes: {}, Endurance: {}, Cool: {}. To spend: {}", brn, sen, brw, refl, end, col, tot - 48);
|
||||||
|
match st {
|
||||||
|
StatbotState::Brains => ansi!("I am Statbot, a robot servant of the empire, put here to help you choose how your body will function. The base body has 8 each of brains, senses, brawn, reflexes, endurance and cool - but you get 14 points of improvement. Each point spent lifts that stat by one. Your first job is to choose how much 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.\n\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 stats by sending me the new value, up until you leave here. Your stats now are: ").to_owned() + &summary,
|
||||||
|
StatbotState::Senses => "".to_owned(),
|
||||||
|
StatbotState::Brawn => "".to_owned(),
|
||||||
|
StatbotState::Reflexes => "".to_owned(),
|
||||||
|
StatbotState::Endurance => "".to_owned(),
|
||||||
|
StatbotState::Cool => "".to_owned(),
|
||||||
|
StatbotState::AssignGender => "".to_owned(),
|
||||||
|
StatbotState::SetDescription => "".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NPCMessageHandler for StatbotMessageHandler {
|
||||||
|
async fn handle(
|
||||||
|
self: &Self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
_source: &Item,
|
||||||
|
_target: &Item,
|
||||||
|
message: &str
|
||||||
|
) -> UResult<()> {
|
||||||
|
let user = get_user_or_fail(ctx)?;
|
||||||
|
match message {
|
||||||
|
_ => {
|
||||||
|
reply(ctx, &next_action_text(user)).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -164,7 +164,7 @@ pub fn room_list() -> &'static Vec<Room> {
|
|||||||
various stages of development floating in them. It smells like bleach. \
|
various stages of development floating in them. It smells like bleach. \
|
||||||
Being here makes you realise you aren't exactly alive right now... you \
|
Being here makes you realise you aren't exactly alive right now... you \
|
||||||
have no body. But you sense you could go <bold>up<reset> and attach \
|
have no body. But you sense you could go <bold>up<reset> and attach \
|
||||||
your memories to a body matching your current stats."),
|
your memories to a body matching your current stats"),
|
||||||
description_less_explicit: None,
|
description_less_explicit: None,
|
||||||
grid_coords: GridCoords { x: 1, y: 0, z: 1 },
|
grid_coords: GridCoords { x: 1, y: 0, z: 1 },
|
||||||
exits: vec!()
|
exits: vec!()
|
||||||
@ -192,7 +192,9 @@ pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
|||||||
item_code: r.code.to_owned(),
|
item_code: r.code.to_owned(),
|
||||||
item_type: "room".to_owned(),
|
item_type: "room".to_owned(),
|
||||||
display: r.name.to_owned(),
|
display: r.name.to_owned(),
|
||||||
location: format!("room/{}", r.code),
|
details: Some(r.description.to_owned()),
|
||||||
|
details_less_explicit: r.description_less_explicit.map(|d|d.to_owned()),
|
||||||
|
location: format!("zone/{}", r.zone),
|
||||||
is_static: true,
|
is_static: true,
|
||||||
..Item::default()
|
..Item::default()
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user