Add an improvise command to craft things without tools.

This commit is contained in:
Condorra 2023-06-12 00:36:55 +10:00
parent 3292dcc13b
commit cd40573345
9 changed files with 1297 additions and 236 deletions

View File

@ -7,51 +7,194 @@ struct PluralRule<'l> {
append_suffix: &'l str,
}
pub fn pluralise(input: &str) -> String {
pub fn pluralise(orig_input: &str) -> String {
let mut extra_suffix: &str = "";
let mut input: &str = orig_input;
'wordsplit: for split_word in vec!["pair"] {
for (idx, _) in input.match_indices(split_word) {
let end_idx = idx + split_word.len();
if end_idx == input.len() {
continue;
}
if (idx == 0 || &input[idx - 1..idx] == " ") && &input[end_idx..end_idx + 1] == " " {
extra_suffix = &input[end_idx..];
input = &input[0..end_idx];
break 'wordsplit;
}
}
}
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" },
));
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;
return input[0..(input.len() - rule.drop)].to_owned()
+ rule.append_suffix
+ extra_suffix;
}
}
input.to_owned() + "s"
input.to_owned() + "s" + extra_suffix
}
pub fn indefinite_article(countable_word: &str) -> &'static str {
@ -60,17 +203,22 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
}
let vowels = ["a", "e", "i", "o", "u"];
if !vowels.contains(&&countable_word[0..1]) {
if countable_word.starts_with("honor") || countable_word.starts_with("honour") ||
countable_word.starts_with("honest") || countable_word.starts_with("hour") ||
countable_word.starts_with("heir") {
return "an";
}
if countable_word.starts_with("honor")
|| countable_word.starts_with("honour")
|| countable_word.starts_with("honest")
|| countable_word.starts_with("hour")
|| countable_word.starts_with("heir")
{
return "an";
}
return "a";
}
if countable_word.starts_with("eu")
|| countable_word.starts_with("one")
|| countable_word.starts_with("once")
{
return "a";
}
if countable_word.starts_with("eu") || countable_word.starts_with("one") ||
countable_word.starts_with("once") {
return "a";
}
if countable_word.starts_with("e") {
if countable_word.starts_with("ewe") {
return "a";
@ -82,12 +230,13 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
return "an";
}
if countable_word.starts_with("uni") {
if countable_word.starts_with("unid") ||
countable_word.starts_with("unim") ||
countable_word.starts_with("unin") {
// unidentified, unimaginable, uninhabited etc...
return "an";
}
if countable_word.starts_with("unid")
|| countable_word.starts_with("unim")
|| countable_word.starts_with("unin")
{
// unidentified, unimaginable, uninhabited etc...
return "an";
}
// Words like unilateral
return "a";
}
@ -101,8 +250,11 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
}
return "an";
}
if countable_word.starts_with("ubiq") || countable_word.starts_with("uku") || countable_word.starts_with("ukr") {
return "a"
if countable_word.starts_with("ubiq")
|| countable_word.starts_with("uku")
|| countable_word.starts_with("ukr")
{
return "a";
}
}
return "an";
@ -120,13 +272,16 @@ 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
Some((last, rest)) => rest.join(", ") + " and " + last,
}
}
pub fn weight(grams: u64) -> String {
if grams > 999 {
format!("{} kg", Decimal::from_i128_with_scale(grams as i128, 3).normalize())
format!(
"{} kg",
Decimal::from_i128_with_scale(grams as i128, 3).normalize()
)
} else {
format!("{} g", grams)
}
@ -136,7 +291,7 @@ pub fn weight(grams: u64) -> String {
mod test {
#[test]
fn pluralise_should_follow_english_rules() {
for (word, plural) in vec!(
for (word, plural) in vec![
("cat", "cats"),
("wolf", "wolves"),
("scarf", "scarves"),
@ -151,14 +306,17 @@ mod test {
("killer blowfly", "killer blowflies"),
("house mouse", "house mice"),
("zombie sheep", "zombie sheep"),
) {
("brown pair of pants", "brown pairs of pants"),
("good pair", "good pairs"),
("repair kit", "repair kits"),
] {
assert_eq!(super::pluralise(word), plural);
}
}
#[test]
fn indefinite_article_should_follow_english_rules() {
for (article, word) in vec!(
for (article, word) in vec![
("a", "cat"),
("a", "human"),
("an", "apple"),
@ -180,33 +338,39 @@ mod test {
("a", "user"),
("a", "ubiquitous hazard"),
("a", "unitary plan"),
) {
] {
let result = super::indefinite_article(&word.to_lowercase());
assert_eq!(format!("{} {}", result, word), format!("{} {}", article, word));
assert_eq!(
format!("{} {}", result, word),
format!("{} {}", article, word)
);
}
}
#[test]
fn caps_first_works() {
for (inp, outp) in vec!(
for (inp, outp) in vec![
("", ""),
("cat", "Cat"),
("Cat", "Cat"),
("hello world", "Hello world"),
) {
] {
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"),
) {
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

@ -30,6 +30,7 @@ mod gear;
pub mod get;
mod help;
mod ignore;
pub mod improvise;
mod install;
mod inventory;
mod less_explicit_mode;
@ -147,6 +148,11 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"drop" => drop::VERB,
"gear" => gear::VERB,
"get" => get::VERB,
"improv" => improvise::VERB,
"improvise" => improvise::VERB,
"improvize" => improvise::VERB,
"install" => install::VERB,
"inventory" => inventory::VERB,
"inv" => inventory::VERB,
@ -329,7 +335,10 @@ pub async fn search_item_for_user<'l>(
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
[] => user_error(format!(
"Sorry, I couldn't find anything matching \"{}\".",
search.query
))?,
[match_it] => match_it.clone(),
[item1, ..] => item1.clone(),
},
@ -346,7 +355,10 @@ pub async fn search_items_for_user<'l>(
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
[] => user_error(format!(
"Sorry, I couldn't find anything matching \"{}\".",
search.query
))?,
v => v.into_iter().map(|it| it.clone()).collect(),
},
)

View File

@ -0,0 +1,459 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
language::{self, indefinite_article},
models::item::Item,
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{
comms::broadcast_to_room,
destroy_container,
skills::{crit_fail_penalty_for_skill, skill_check_and_grind},
},
static_content::possession_type::{
improv_by_ingredient, improv_by_output, possession_data, possession_type_names,
PossessionData, PossessionType,
},
};
use ansi::ansi;
use async_trait::async_trait;
use rand::seq::IteratorRandom;
use rand::seq::SliceRandom;
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time;
pub struct WithQueueHandler;
#[async_trait]
impl QueueCommandHandler for WithQueueHandler {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let item_id = match command {
QueueCommand::ImprovWith { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != player_item.refstr() {
user_error("You try improvising but realise you no longer have it.".to_owned())?;
}
broadcast_to_room(
&ctx.trans,
&player_item.location,
None,
&format!(
"{} tries to work out what {} can make from {}.\n",
&player_item.display_for_sentence(true, 1, true),
&player_item.pronouns.subject,
&item.display_for_sentence(true, 1, false),
),
Some(&format!(
"{} tries to work out what {} can make from {}.\n",
&player_item.display_for_sentence(false, 1, true),
&player_item.pronouns.subject,
&item.display_for_sentence(false, 1, false),
)),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let item_id = match command {
QueueCommand::ImprovWith { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != player_item.refstr() {
user_error("You try improvising but realise you no longer have it.".to_owned())?;
}
let opts: Vec<&'static PossessionData> = improv_by_ingredient()
.get(
item.possession_type
.as_ref()
.ok_or_else(|| UserError("You can't improvise with that!".to_owned()))?,
)
.ok_or_else(|| {
UserError(format!(
"You can't think of anything you could make with {}",
item.display_for_session(&ctx.session_dat)
))
})?
.iter()
.filter_map(|it| possession_data().get(&it.output).map(|v| *v))
.filter(|pd| !ctx.session_dat.less_explicit_mode || pd.display_less_explicit.is_none())
.collect();
let result_data = opts
.as_slice()
.choose(&mut rand::thread_rng())
.ok_or_else(|| {
UserError(format!(
"You can't think of anything you could make with {}",
item.display_for_session(&ctx.session_dat)
))
})?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You think you could make {} {} from {}\n",
indefinite_article(result_data.display),
result_data.display,
item.display_for_session(&ctx.session_dat)
)),
)
.await?;
Ok(())
}
}
pub struct FromQueueHandler;
#[async_trait]
impl QueueCommandHandler for FromQueueHandler {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let (already_used, item_ids) = match command {
QueueCommand::ImprovFrom {
possession_ids,
already_used,
..
} => (already_used, possession_ids),
_ => user_error("Unexpected command".to_owned())?,
};
if !already_used.is_empty() {
return Ok(time::Duration::from_secs(1));
}
for item_id in item_ids {
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != player_item.refstr() {
user_error("You try improvising but realise you no longer have the things you'd planned to use."
.to_owned())?;
}
}
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let (output, possession_ids, already_used) = match command {
QueueCommand::ImprovFrom {
output,
possession_ids,
already_used,
} => (output, possession_ids, already_used),
_ => user_error("Unexpected command".to_owned())?,
};
let craft_data = improv_by_output().get(&output).ok_or_else(|| {
UserError("You don't think it is possible to improvise that.".to_owned())
})?;
let mut ingredients_left: Vec<PossessionType> = craft_data.inputs.clone();
let mut to_destroy_if_success: Vec<Arc<Item>> = Vec::new();
for item_id in already_used {
let item = ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
.ok_or_else(|| UserError("Item used in crafting not found.".to_owned()))?;
to_destroy_if_success.push(item.clone());
let possession_type = item
.possession_type
.as_ref()
.ok_or_else(|| UserError("Item used in crafting not a possession.".to_owned()))?;
if let Some(match_pos) = ingredients_left.iter().position(|pt| pt == possession_type) {
ingredients_left.remove(match_pos);
}
}
let mut possession_id_iter = possession_ids.iter();
match possession_id_iter.next() {
None => {
let choice = ingredients_left
.iter()
.choose(&mut rand::thread_rng())
.clone();
match choice {
// Nothing left to add, and nothing needed - success!
None => {
for item in to_destroy_if_success {
destroy_container(&ctx.trans, &item).await?;
}
let mut new_item: Item = craft_data.output.clone().into();
new_item.item_code = ctx.trans.alloc_item_code().await?.to_string();
new_item.location = player_item.refstr();
ctx.trans.create_item(&new_item).await?;
broadcast_to_room(
&ctx.trans,
&player_item.location,
None,
&format!(
"{} proudly holds up the {} {} just made.\n",
&player_item.display_for_sentence(true, 1, true),
&new_item.display_for_sentence(true, 1, false),
&player_item.pronouns.subject
),
Some(&format!(
"{} proudly holds up the {} {} just made.\n",
&player_item.display_for_sentence(false, 1, true),
&new_item.display_for_sentence(false, 1, false),
&player_item.pronouns.subject
)),
)
.await?;
}
// Nothing left to add, but recipe incomplete.
Some(missing_type) => {
let possession_data =
possession_data().get(missing_type).ok_or_else(|| {
UserError(
"It looks like it's no longer possible to improvise that."
.to_owned(),
)
})?;
user_error(format!(
"You realise you'll also need {} {} to craft that.",
language::indefinite_article(possession_data.display),
possession_data.display
))?;
}
}
}
Some(possession_id) => {
let item = ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
.ok_or_else(|| {
UserError(
"An item you planned to use for crafting seems to be gone.".to_owned(),
)
})?;
if !ingredients_left.contains(
item.possession_type
.as_ref()
.ok_or_else(|| UserError("Uncraftable item used.".to_owned()))?,
) {
user_error(format!(
"You try adding {}, but it doesn't really seem to fit right.",
&item.display_for_session(&ctx.session_dat)
))?;
}
let mut player_item_mut = (*player_item).clone();
let skill_result = skill_check_and_grind(
&ctx.trans,
&mut player_item_mut,
&craft_data.skill,
craft_data.difficulty,
)
.await?;
if skill_result <= -0.5 {
crit_fail_penalty_for_skill(
&ctx.trans,
&mut player_item_mut,
&craft_data.skill,
)
.await?;
ctx.trans
.delete_item(&item.item_type, &item.item_code)
.await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You try adding {}, but it goes badly and you waste it.\n",
&item.display_for_session(&ctx.session_dat)
)),
)
.await?;
} else if skill_result <= 0.0 {
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You try and fail at adding {}.\n",
&item.display_for_session(&ctx.session_dat)
)),
)
.await?;
} else {
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You try adding {}.\n",
&item.display_for_session(&ctx.session_dat),
)),
)
.await?;
let mut new_possession_ids = possession_ids.clone();
new_possession_ids.remove(possession_id);
let mut new_already_used = already_used.clone();
new_already_used.insert(possession_id.clone());
ctx.session_dat.queue.push_front(QueueCommand::ImprovFrom {
output: output.clone(),
possession_ids: new_possession_ids,
already_used: new_already_used,
});
}
ctx.trans.save_item_model(&player_item_mut).await?;
}
}
Ok(())
}
}
async fn improv_query(
ctx: &mut VerbContext<'_>,
player_item: &Item,
with_what: &str,
) -> UResult<()> {
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(player_item, with_what)
},
)
.await?;
if item.item_type != "possession" {
user_error("You can't improvise with that!".to_owned())?
}
queue_command(
ctx,
&QueueCommand::ImprovWith {
possession_id: item.item_code.clone(),
},
)
.await
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
if rtrim.starts_with("with ") {
return improv_query(ctx, &player_item, rtrim["with ".len()..].trim_start()).await;
}
let (output, inputs_str) = rtrim.split_once(" from ").ok_or_else(
|| UserError(ansi!("Try <bold>improvise with <reset>item or <bold>improvise<reset> item <bold>from<reset> item, item, ...<reset>").to_owned()))?;
let output_type: PossessionType = match possession_type_names()
.get(&output.trim().to_lowercase())
.map(|x| x.as_slice())
.unwrap_or_else(|| &[])
{
[] => user_error("I don't recognise the thing you want to make.".to_owned())?,
[t] => t.clone(),
_ => user_error(
"You'll have to be more specific about what you want to make.".to_owned(),
)?,
};
let inputs = inputs_str.split(",").map(|v| v.trim());
let mut input_ids: BTreeSet<String> = BTreeSet::new();
for mut input in inputs {
let mut use_limit = Some(1);
if input == "all" || input.starts_with("all ") {
input = input[3..].trim();
use_limit = None;
} else if let (Some(n), remaining2) = parse_count(input) {
use_limit = Some(n);
input = remaining2;
}
let items = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
limit: use_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, input)
},
)
.await?;
for item in items {
if item.item_type != "possession" {
user_error(format!(
"You can't improvise with {}!",
&item.display_for_session(&ctx.session_dat)
))?
}
input_ids.insert(item.item_code.to_owned());
}
}
queue_command(
ctx,
&QueueCommand::ImprovFrom {
output: output_type,
possession_ids: input_ids,
already_used: BTreeSet::new(),
},
)
.await
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,16 +1,16 @@
use super::{TaskHandler, TaskRunContext};
use crate::message_handler::user_commands::{
close, cut, drop, get, get_user_or_fail, movement, open, remove, use_cmd, user_error, wear,
wield, CommandHandlingError, UResult, VerbContext,
close, cut, drop, get, get_user_or_fail, improvise, movement, open, remove, use_cmd,
user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
};
use crate::models::task::{Task, TaskDetails, TaskMeta};
use crate::static_content::room::Direction;
use crate::static_content::{possession_type::PossessionType, room::Direction};
use crate::DResult;
use async_trait::async_trait;
use chrono::Utc;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::time;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -47,6 +47,14 @@ pub enum QueueCommand {
Wield {
possession_id: String,
},
ImprovWith {
possession_id: String,
},
ImprovFrom {
output: PossessionType,
possession_ids: BTreeSet<String>,
already_used: BTreeSet<String>,
},
}
impl QueueCommand {
pub fn name(&self) -> &'static str {
@ -62,6 +70,8 @@ impl QueueCommand {
Use { .. } => "Use",
Wear { .. } => "Wear",
Wield { .. } => "Wield",
ImprovWith { .. } => "ImprovWith",
ImprovFrom { .. } => "ImprovFrom",
}
}
}
@ -127,6 +137,14 @@ fn queue_command_registry(
"Wield",
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"ImprovWith",
&improvise::WithQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"ImprovFrom",
&improvise::FromQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
]
.into_iter()
.collect()

View File

@ -16,7 +16,9 @@ use crate::{
static_content::{
journals::{award_journal_if_needed, check_journal_for_kill},
npc::npc_by_code,
possession_type::{fist, possession_data, DamageType, WeaponAttackData, WeaponData},
possession_type::{
fist, possession_data, DamageDistribution, DamageType, WeaponAttackData, WeaponData,
},
species::{species_info_map, BodyPart},
},
DResult,
@ -30,25 +32,16 @@ use rand::{prelude::IteratorRandom, Rng};
use rand_distr::{Distribution, Normal};
use std::time;
async fn soak_damage(
ctx: &mut TaskRunContext<'_>,
attack: &WeaponAttackData,
pub async fn soak_damage<DamageDist: DamageDistribution>(
trans: &DBTrans,
attack: &DamageDist,
victim: &Item,
presoak_amount: f64,
part: &BodyPart,
) -> DResult<f64> {
let mut damage_by_type: Vec<(&DamageType, f64)> = attack
.other_damage_types
.iter()
.map(|(frac, dtype)| (dtype, frac * presoak_amount))
.collect();
damage_by_type.push((
&attack.base_damage_type,
presoak_amount - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
));
let damage_by_type: Vec<(&DamageType, f64)> = attack.distribute_damage(presoak_amount);
let mut clothes: Vec<Item> = ctx
.trans
let mut clothes: Vec<Item> = trans
.find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
.await?
.iter()
@ -83,9 +76,9 @@ async fn soak_damage(
clothing.health -= clothes_damage;
if victim.item_type == "player" {
if let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await?
trans.find_session_for_player(&victim.item_code).await?
{
ctx.trans
trans
.queue_for_session(
&vic_sess,
Some(&format!(
@ -104,14 +97,14 @@ async fn soak_damage(
for clothing in &clothes {
if clothing.health <= 0 {
ctx.trans
trans
.delete_item(&clothing.item_type, &clothing.item_code)
.await?;
if victim.item_type == "player" {
if let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await?
trans.find_session_for_player(&victim.item_code).await?
{
ctx.trans
trans
.queue_for_session(
&vic_sess,
Some(&format!(
@ -214,8 +207,8 @@ async fn process_attack(
.max(1.0) as i64;
ctx.trans.save_item_model(&attacker_item).await?;
let actual_damage = soak_damage(
ctx,
&attack,
&ctx.trans,
attack,
victim_item,
actual_damage_presoak as f64,
&part,

View File

@ -1,30 +1,43 @@
#[double]
use crate::db::DBTrans;
use crate::{
models::{
item::{Item, SkillType, StatType, BuffImpact},
user::User
item::{BuffImpact, Item, SkillType, StatType},
user::User,
},
services::combat::{change_health, soak_damage},
static_content::{
possession_type::{DamageDistribution, DamageType},
species::BodyPart,
},
DResult,
};
use rand::{self, Rng};
use chrono::Utc;
use std::collections::BTreeMap;
use mockall_double::double;
#[double] use crate::db::DBTrans;
use rand::{self, Rng};
use rand_distr::{Distribution, Normal};
use std::collections::BTreeMap;
pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
target_item.total_stats = BTreeMap::new();
// 1: Start with total stats = raw stats
for stat_type in StatType::values() {
target_item.total_stats.insert(stat_type.clone(),
*user.raw_stats.get(&stat_type).unwrap_or(&0.0));
target_item.total_stats.insert(
stat_type.clone(),
*user.raw_stats.get(&stat_type).unwrap_or(&0.0),
);
}
// 2: Apply stat (de)buffs...
for buff in &target_item.temporary_buffs {
for impact in &buff.impacts {
match impact {
BuffImpact::ChangeStat { stat, magnitude } => {
target_item.total_stats.entry(stat.clone())
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0))
target_item
.total_stats
.entry(stat.clone())
.and_modify(|old_value| {
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
})
.or_insert((*magnitude).max(0.0));
}
_ => {}
@ -34,125 +47,299 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User
// 3: Total skills = raw skills
target_item.total_skills = BTreeMap::new();
for skill_type in SkillType::values() {
target_item.total_skills.insert(skill_type.clone(),
*user.raw_skills.get(&skill_type).unwrap_or(&0.0));
target_item.total_skills.insert(
skill_type.clone(),
*user.raw_skills.get(&skill_type).unwrap_or(&0.0),
);
}
// 4: Adjust skills by stats
let brn = *target_item.total_stats.get(&StatType::Brains).unwrap_or(&0.0);
let sen = *target_item.total_stats.get(&StatType::Senses).unwrap_or(&0.0);
let brw = *target_item.total_stats.get(&StatType::Brawn).unwrap_or(&0.0);
let refl = *target_item.total_stats.get(&StatType::Reflexes).unwrap_or(&0.0);
let end = *target_item.total_stats.get(&StatType::Endurance).unwrap_or(&0.0);
let brn = *target_item
.total_stats
.get(&StatType::Brains)
.unwrap_or(&0.0);
let sen = *target_item
.total_stats
.get(&StatType::Senses)
.unwrap_or(&0.0);
let brw = *target_item
.total_stats
.get(&StatType::Brawn)
.unwrap_or(&0.0);
let refl = *target_item
.total_stats
.get(&StatType::Reflexes)
.unwrap_or(&0.0);
let end = *target_item
.total_stats
.get(&StatType::Endurance)
.unwrap_or(&0.0);
let col = *target_item.total_stats.get(&StatType::Cool).unwrap_or(&0.0);
target_item.total_skills.entry(SkillType::Appraise)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
target_item.total_skills.entry(SkillType::Appraise)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Blades)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Blades)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Bombs)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
target_item.total_skills.entry(SkillType::Bombs)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Chemistry)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Climb)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Climb)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Clubs)
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
target_item.total_skills.entry(SkillType::Clubs)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Craft)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Dodge)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Dodge)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Fish)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Fish)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Fists)
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
target_item.total_skills.entry(SkillType::Fists)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Focus)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Focus)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Fuck)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Fuck)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Hack)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Locksmith)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
target_item.total_skills.entry(SkillType::Locksmith)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Medic)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Persuade)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
target_item.total_skills.entry(SkillType::Persuade)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Pilot)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
target_item.total_skills.entry(SkillType::Pilot)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Pistols)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Pistols)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Quickdraw)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Quickdraw)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Repair)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Rifles)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Rifles)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Scavenge)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Scavenge)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Science)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Sneak)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Sneak)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Spears)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Spears)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Swim)
.and_modify(|sk| *sk += end).or_insert(brn);
target_item.total_skills.entry(SkillType::Teach)
.and_modify(|sk| *sk += brn).or_insert(brn);
target_item.total_skills.entry(SkillType::Throw)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Throw)
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
target_item.total_skills.entry(SkillType::Track)
.and_modify(|sk| *sk += sen).or_insert(brn);
target_item.total_skills.entry(SkillType::Whips)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Whips)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Appraise)
.and_modify(|sk| *sk += brn * 0.5)
.or_insert(brn * 0.5);
target_item
.total_skills
.entry(SkillType::Appraise)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Blades)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Blades)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Bombs)
.and_modify(|sk| *sk += brn * 0.5)
.or_insert(brn * 0.5);
target_item
.total_skills
.entry(SkillType::Bombs)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Chemistry)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Climb)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Climb)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Clubs)
.and_modify(|sk| *sk += brw * 0.5)
.or_insert(brw * 0.5);
target_item
.total_skills
.entry(SkillType::Clubs)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Craft)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Dodge)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Dodge)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Fish)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Fish)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Fists)
.and_modify(|sk| *sk += brw * 0.5)
.or_insert(brw * 0.5);
target_item
.total_skills
.entry(SkillType::Fists)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Focus)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Focus)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Fuck)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Fuck)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Hack)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Locksmith)
.and_modify(|sk| *sk += brn * 0.5)
.or_insert(brn * 0.5);
target_item
.total_skills
.entry(SkillType::Locksmith)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Medic)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Persuade)
.and_modify(|sk| *sk += brn * 0.5)
.or_insert(brn * 0.5);
target_item
.total_skills
.entry(SkillType::Persuade)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Pilot)
.and_modify(|sk| *sk += brn * 0.5)
.or_insert(brn * 0.5);
target_item
.total_skills
.entry(SkillType::Pilot)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Pistols)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Pistols)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Quickdraw)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Quickdraw)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Repair)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Rifles)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Rifles)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Scavenge)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Scavenge)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Science)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Sneak)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Sneak)
.and_modify(|sk| *sk += col * 0.5)
.or_insert(col * 0.5);
target_item
.total_skills
.entry(SkillType::Spears)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
target_item
.total_skills
.entry(SkillType::Spears)
.and_modify(|sk| *sk += end * 0.5)
.or_insert(end * 0.5);
target_item
.total_skills
.entry(SkillType::Swim)
.and_modify(|sk| *sk += end)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Teach)
.and_modify(|sk| *sk += brn)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Throw)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Throw)
.and_modify(|sk| *sk += brw * 0.5)
.or_insert(brw * 0.5);
target_item
.total_skills
.entry(SkillType::Track)
.and_modify(|sk| *sk += sen)
.or_insert(brn);
target_item
.total_skills
.entry(SkillType::Whips)
.and_modify(|sk| *sk += sen * 0.5)
.or_insert(sen * 0.5);
target_item
.total_skills
.entry(SkillType::Whips)
.and_modify(|sk| *sk += refl * 0.5)
.or_insert(refl * 0.5);
// 5: Apply skill (de)buffs...
for buff in &target_item.temporary_buffs {
for impact in &buff.impacts {
match impact {
BuffImpact::ChangeSkill { skill, magnitude } => {
target_item.total_skills.entry(skill.clone())
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0))
target_item
.total_skills
.entry(skill.clone())
.and_modify(|old_value| {
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
})
.or_insert((*magnitude).max(0.0));
}
_ => {}
@ -188,27 +375,47 @@ pub fn skill_check_only(who: &Item, skill: &SkillType, diff_level: f64) -> f64 {
// Note: Caller must save who because skills might update.
// Don't return error if skillcheck fails, it can fail but still grind.
pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &SkillType, diff_level: f64) -> DResult<f64> {
pub async fn skill_check_and_grind(
trans: &DBTrans,
who: &mut Item,
skill: &SkillType,
diff_level: f64,
) -> DResult<f64> {
let gap = calc_level_gap(who, skill, diff_level);
let result = skill_check_fn(gap);
// If the skill gap is 0, probability of learning is 0.5
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
if who.item_type == "player" && rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp() {
if who.item_type == "player"
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp()
{
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0 ||
!user.last_skill_improve.get(skill)
.map(|t| (Utc::now() - *t).num_seconds() > 60).unwrap_or(true) {
return Ok(result)
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0
|| !user
.last_skill_improve
.get(skill)
.map(|t| (Utc::now() - *t).num_seconds() > 60)
.unwrap_or(true)
{
return Ok(result);
}
user.raw_skills.entry(skill.clone()).and_modify(|raw| *raw += 0.01).or_insert(0.01);
user.raw_skills
.entry(skill.clone())
.and_modify(|raw| *raw += 0.01)
.or_insert(0.01);
user.last_skill_improve.insert(skill.clone(), Utc::now());
trans.queue_for_session(&sess,
Some(&format!("Your raw {} is now {:.2}\n",
skill.display(), user.raw_skills
.get(skill).unwrap_or(&0.0)))).await?;
trans
.queue_for_session(
&sess,
Some(&format!(
"Your raw {} is now {:.2}\n",
skill.display(),
user.raw_skills.get(skill).unwrap_or(&0.0)
)),
)
.await?;
trans.save_user_model(&user).await?;
calculate_total_stats_skills_for_user(who, &user);
}
@ -217,3 +424,45 @@ pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &Skil
Ok(result)
}
struct SkillFailDamage {
pub damage_type: DamageType,
}
impl DamageDistribution for SkillFailDamage {
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)> {
vec![(&self.damage_type, total)]
}
}
pub async fn crit_fail_penalty_for_skill(
trans: &DBTrans,
who: &mut Item,
skill: &SkillType,
) -> DResult<()> {
use SkillType::*;
let (msg, part, dist) = match skill {
Bombs | Chemistry => (
"Ow! You burn your hand.",
BodyPart::Hands,
SkillFailDamage {
damage_type: DamageType::Shock,
},
),
Craft | Repair => (
"Ow! You catch your hand on something sharp.",
BodyPart::Hands,
SkillFailDamage {
damage_type: DamageType::Slash,
},
),
_ => return Ok(()),
};
let actual_damage_presoak = Normal::<f64>::new(5.0, 1.0)?
.sample(&mut rand::thread_rng())
.floor()
.max(1.0);
let final_damage = soak_damage(trans, &dist, who, actual_damage_presoak, &part).await?;
change_health(trans, -final_damage as i64, who, &msg, &msg).await?;
Ok(())
}

View File

@ -8,7 +8,7 @@ use async_trait::async_trait;
use once_cell::sync::OnceCell;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
mod blade;
mod corp_licence;
@ -56,6 +56,10 @@ impl DamageType {
}
}
pub trait DamageDistribution {
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)>;
}
pub struct WeaponAttackData {
pub start_messages: AttackMessageChoice,
pub success_messages: AttackMessageChoicePart,
@ -94,6 +98,21 @@ impl Default for WeaponAttackData {
}
}
impl DamageDistribution for WeaponAttackData {
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&'l DamageType, f64)> {
let mut damage_by_type: Vec<(&DamageType, f64)> = self
.other_damage_types
.iter()
.map(|(frac, dtype)| (dtype, frac * total))
.collect();
damage_by_type.push((
&self.base_damage_type,
total - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
));
damage_by_type
}
}
pub struct WeaponData {
pub uses_skill: SkillType,
pub raw_min_to_learn: f64,
@ -298,6 +317,7 @@ pub enum PossessionType {
LeatherPants,
// Weapons: Whips
AntennaWhip,
LeatherWhip,
// Weapons: Blades
ButcherKnife,
// Medical
@ -410,6 +430,24 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
})
}
pub fn possession_type_names() -> &'static BTreeMap<String, Vec<PossessionType>> {
static POSSESSION_NAMES: OnceCell<BTreeMap<String, Vec<PossessionType>>> = OnceCell::new();
&POSSESSION_NAMES.get_or_init(|| {
let mut map = BTreeMap::new();
for (pt, pd) in possession_data() {
map.entry(pd.display.to_lowercase())
.and_modify(|l: &mut Vec<PossessionType>| l.push(pt.clone()))
.or_insert_with(|| vec![pt.clone()]);
for alias in &pd.aliases {
map.entry(alias.to_lowercase())
.and_modify(|l| l.push(pt.clone()))
.or_insert_with(|| vec![pt.clone()]);
}
}
map
})
}
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
&RELEVANT.get_or_init(|| {
@ -426,8 +464,64 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
})
}
pub struct CraftData {
pub skill: SkillType,
pub difficulty: f64,
pub inputs: Vec<PossessionType>,
pub output: PossessionType,
}
pub fn improv_table() -> &'static Vec<CraftData> {
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
IMPROV_CELL.get_or_init(|| {
vec![CraftData {
skill: SkillType::Craft,
difficulty: 6.0,
inputs: vec![
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
PossessionType::AnimalSkin,
],
output: PossessionType::LeatherWhip,
}]
})
}
pub fn improv_by_ingredient() -> &'static BTreeMap<PossessionType, Vec<&'static CraftData>> {
static MAP_CELL: OnceCell<BTreeMap<PossessionType, Vec<&'static CraftData>>> = OnceCell::new();
MAP_CELL.get_or_init(|| {
let mut map = BTreeMap::new();
for cd in improv_table() {
let inps: BTreeSet<&PossessionType> = cd.inputs.iter().collect();
for inp in inps {
map.entry(inp.clone())
.and_modify(|l: &mut Vec<&'static CraftData>| l.push(cd))
.or_insert(vec![cd]);
}
}
map
})
}
pub fn improv_by_output() -> &'static BTreeMap<PossessionType, &'static CraftData> {
static MAP_CELL: OnceCell<BTreeMap<PossessionType, &'static CraftData>> = OnceCell::new();
MAP_CELL.get_or_init(|| {
improv_table()
.iter()
.map(|cd| (cd.output.clone(), cd))
.collect()
})
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use super::*;
#[test]
fn other_damage_types_add_to_less_than_one() {
@ -444,4 +538,37 @@ mod tests {
}
}
}
#[test]
fn possession_type_names_works() {
assert!(possession_type_names()
.get("whip")
.unwrap()
.contains(&PossessionType::AntennaWhip));
assert!(possession_type_names()
.get("animal skin")
.unwrap()
.contains(&PossessionType::AnimalSkin));
}
#[test]
fn only_one_way_to_improv_each_item() {
assert_eq!(
improv_table()
.iter()
.group_by(|cd| cd.output.clone())
.into_iter()
.filter_map(|g| if g.1.count() == 1 { None } else { Some(g.0) })
.collect::<Vec<PossessionType>>(),
Vec::new()
)
}
#[test]
fn every_improv_item_has_possession_data_for_output() {
assert_eq!(
improv_table().iter().filter_map(|cd| if possession_data().get(&cd.output).is_none() { Some(cd) } else None ).collect(),
Vec::new()
);
}
}

View File

@ -1,4 +1,4 @@
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
use super::{DamageType, PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
@ -42,6 +42,45 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
}),
..Default::default()
}
)
),
(PossessionType::LeatherWhip,
PossessionData {
display: "leather whip",
details: "A whip made from stitched together animal skins... it looks like a formidable weapon, and in the right hands will make someone look like Indiana Jones!",
aliases: vec!("whip"),
weapon_data: Some(WeaponData {
uses_skill: SkillType::Whips,
raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} lines up {} leather whip for a strike on {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s leather whip scores a painful red line across {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 4.0,
stdev_damage: 2.0,
base_damage_type: DamageType::Beat,
other_damage_types: vec!((0.25, DamageType::Slash)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}
),
))
}