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, 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(); static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
let plural_rules = PLURAL_RULES.get_or_init(|| vec!( let plural_rules = PLURAL_RULES.get_or_init(|| {
PluralRule { match_suffix: "foot", drop: 3, append_suffix: "eet" }, vec![
PluralRule { match_suffix: "tooth", drop: 4, append_suffix: "eeth" }, PluralRule {
PluralRule { match_suffix: "man", drop: 2, append_suffix: "en" }, match_suffix: "foot",
PluralRule { match_suffix: "mouse", drop: 4, append_suffix: "ice" }, drop: 3,
PluralRule { match_suffix: "louse", drop: 4, append_suffix: "ice" }, append_suffix: "eet",
PluralRule { match_suffix: "fish", drop: 0, append_suffix: "" }, },
PluralRule { match_suffix: "sheep", drop: 0, append_suffix: "" }, PluralRule {
PluralRule { match_suffix: "deer", drop: 0, append_suffix: "" }, match_suffix: "tooth",
PluralRule { match_suffix: "pox", drop: 0, append_suffix: "" }, drop: 4,
PluralRule { match_suffix: "cis", drop: 2, append_suffix: "es" }, append_suffix: "eeth",
PluralRule { match_suffix: "sis", drop: 2, append_suffix: "es" }, },
PluralRule { match_suffix: "xis", drop: 2, append_suffix: "es" }, PluralRule {
PluralRule { match_suffix: "ss", drop: 0, append_suffix: "es" }, match_suffix: "man",
PluralRule { match_suffix: "ch", drop: 0, append_suffix: "es" }, drop: 2,
PluralRule { match_suffix: "sh", drop: 0, append_suffix: "es" }, append_suffix: "en",
PluralRule { match_suffix: "ife", drop: 2, append_suffix: "ves" }, },
PluralRule { match_suffix: "lf", drop: 1, append_suffix: "ves" }, PluralRule {
PluralRule { match_suffix: "arf", drop: 1, append_suffix: "ves" }, match_suffix: "mouse",
PluralRule { match_suffix: "ay", drop: 0, append_suffix: "s" }, drop: 4,
PluralRule { match_suffix: "ey", drop: 0, append_suffix: "s" }, append_suffix: "ice",
PluralRule { match_suffix: "iy", drop: 0, append_suffix: "s" }, },
PluralRule { match_suffix: "oy", drop: 0, append_suffix: "s" }, PluralRule {
PluralRule { match_suffix: "uy", drop: 0, append_suffix: "s" }, match_suffix: "louse",
PluralRule { match_suffix: "y", drop: 1, append_suffix: "ies" }, drop: 4,
PluralRule { match_suffix: "ao", drop: 0, append_suffix: "s" }, append_suffix: "ice",
PluralRule { match_suffix: "eo", drop: 0, append_suffix: "s" }, },
PluralRule { match_suffix: "io", drop: 0, append_suffix: "s" }, PluralRule {
PluralRule { match_suffix: "oo", drop: 0, append_suffix: "s" }, match_suffix: "fish",
PluralRule { match_suffix: "uo", drop: 0, append_suffix: "s" }, 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 o rule could be much larger... we'll add specific exceptions as
// the come up. // the come up.
PluralRule { match_suffix: "o", drop: 0, append_suffix: "es" }, PluralRule {
match_suffix: "o",
drop: 0,
append_suffix: "es",
},
// Lots of possible exceptions here. // Lots of possible exceptions here.
PluralRule { match_suffix: "ex", drop: 0, append_suffix: "es" }, PluralRule {
)); match_suffix: "ex",
drop: 0,
append_suffix: "es",
},
]
});
for rule in plural_rules { for rule in plural_rules {
if input.ends_with(rule.match_suffix) { 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 { pub fn indefinite_article(countable_word: &str) -> &'static str {
@ -60,15 +203,20 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
} }
let vowels = ["a", "e", "i", "o", "u"]; let vowels = ["a", "e", "i", "o", "u"];
if !vowels.contains(&&countable_word[0..1]) { if !vowels.contains(&&countable_word[0..1]) {
if countable_word.starts_with("honor") || countable_word.starts_with("honour") || if countable_word.starts_with("honor")
countable_word.starts_with("honest") || countable_word.starts_with("hour") || || countable_word.starts_with("honour")
countable_word.starts_with("heir") { || countable_word.starts_with("honest")
|| countable_word.starts_with("hour")
|| countable_word.starts_with("heir")
{
return "an"; return "an";
} }
return "a"; return "a";
} }
if countable_word.starts_with("eu") || countable_word.starts_with("one") || if countable_word.starts_with("eu")
countable_word.starts_with("once") { || countable_word.starts_with("one")
|| countable_word.starts_with("once")
{
return "a"; return "a";
} }
if countable_word.starts_with("e") { if countable_word.starts_with("e") {
@ -82,9 +230,10 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
return "an"; return "an";
} }
if countable_word.starts_with("uni") { if countable_word.starts_with("uni") {
if countable_word.starts_with("unid") || if countable_word.starts_with("unid")
countable_word.starts_with("unim") || || countable_word.starts_with("unim")
countable_word.starts_with("unin") { || countable_word.starts_with("unin")
{
// unidentified, unimaginable, uninhabited etc... // unidentified, unimaginable, uninhabited etc...
return "an"; return "an";
} }
@ -101,8 +250,11 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
} }
return "an"; return "an";
} }
if countable_word.starts_with("ubiq") || countable_word.starts_with("uku") || countable_word.starts_with("ukr") { if countable_word.starts_with("ubiq")
return "a" || countable_word.starts_with("uku")
|| countable_word.starts_with("ukr")
{
return "a";
} }
} }
return "an"; return "an";
@ -120,13 +272,16 @@ pub fn join_words(words: &[&str]) -> String {
match words.split_last() { match words.split_last() {
None => "".to_string(), None => "".to_string(),
Some((last, [])) => last.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 { pub fn weight(grams: u64) -> String {
if grams > 999 { 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 { } else {
format!("{} g", grams) format!("{} g", grams)
} }
@ -136,7 +291,7 @@ pub fn weight(grams: u64) -> String {
mod test { mod test {
#[test] #[test]
fn pluralise_should_follow_english_rules() { fn pluralise_should_follow_english_rules() {
for (word, plural) in vec!( for (word, plural) in vec![
("cat", "cats"), ("cat", "cats"),
("wolf", "wolves"), ("wolf", "wolves"),
("scarf", "scarves"), ("scarf", "scarves"),
@ -151,14 +306,17 @@ mod test {
("killer blowfly", "killer blowflies"), ("killer blowfly", "killer blowflies"),
("house mouse", "house mice"), ("house mouse", "house mice"),
("zombie sheep", "zombie sheep"), ("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); assert_eq!(super::pluralise(word), plural);
} }
} }
#[test] #[test]
fn indefinite_article_should_follow_english_rules() { fn indefinite_article_should_follow_english_rules() {
for (article, word) in vec!( for (article, word) in vec![
("a", "cat"), ("a", "cat"),
("a", "human"), ("a", "human"),
("an", "apple"), ("an", "apple"),
@ -180,33 +338,39 @@ mod test {
("a", "user"), ("a", "user"),
("a", "ubiquitous hazard"), ("a", "ubiquitous hazard"),
("a", "unitary plan"), ("a", "unitary plan"),
) { ] {
let result = super::indefinite_article(&word.to_lowercase()); 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] #[test]
fn caps_first_works() { fn caps_first_works() {
for (inp, outp) in vec!( for (inp, outp) in vec![
("", ""), ("", ""),
("cat", "Cat"), ("cat", "Cat"),
("Cat", "Cat"), ("Cat", "Cat"),
("hello world", "Hello world"), ("hello world", "Hello world"),
) { ] {
assert_eq!(super::caps_first(inp), outp); assert_eq!(super::caps_first(inp), outp);
} }
} }
#[test] #[test]
fn join_words_works() { fn join_words_works() {
for (inp, outp) in vec!( for (inp, outp) in vec![
(vec!(), ""), (vec![], ""),
(vec!("cat"), "cat"), (vec!["cat"], "cat"),
(vec!("cat", "dog"), "cat and dog"), (vec!["cat", "dog"], "cat and dog"),
(vec!("cat", "dog", "fish"), "cat, dog and fish"), (vec!["cat", "dog", "fish"], "cat, dog and fish"),
(vec!("wolf", "cat", "dog", "fish"), "wolf, cat, dog and fish"), (
) { vec!["wolf", "cat", "dog", "fish"],
"wolf, cat, dog and fish",
),
] {
assert_eq!(super::join_words(&inp[..]), outp); assert_eq!(super::join_words(&inp[..]), outp);
} }
} }

View File

@ -30,6 +30,7 @@ mod gear;
pub mod get; pub mod get;
mod help; mod help;
mod ignore; mod ignore;
pub mod improvise;
mod install; mod install;
mod inventory; mod inventory;
mod less_explicit_mode; mod less_explicit_mode;
@ -147,6 +148,11 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"drop" => drop::VERB, "drop" => drop::VERB,
"gear" => gear::VERB, "gear" => gear::VERB,
"get" => get::VERB, "get" => get::VERB,
"improv" => improvise::VERB,
"improvise" => improvise::VERB,
"improvize" => improvise::VERB,
"install" => install::VERB, "install" => install::VERB,
"inventory" => inventory::VERB, "inventory" => inventory::VERB,
"inv" => 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) .resolve_items_by_display_name_for_player(search)
.await?[..] .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(), [match_it] => match_it.clone(),
[item1, ..] => item1.clone(), [item1, ..] => item1.clone(),
}, },
@ -346,7 +355,10 @@ pub async fn search_items_for_user<'l>(
.resolve_items_by_display_name_for_player(search) .resolve_items_by_display_name_for_player(search)
.await?[..] .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(), 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 super::{TaskHandler, TaskRunContext};
use crate::message_handler::user_commands::{ use crate::message_handler::user_commands::{
close, cut, drop, get, get_user_or_fail, movement, open, remove, use_cmd, user_error, wear, close, cut, drop, get, get_user_or_fail, improvise, movement, open, remove, use_cmd,
wield, CommandHandlingError, UResult, VerbContext, user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
}; };
use crate::models::task::{Task, TaskDetails, TaskMeta}; 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 crate::DResult;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::time; use std::time;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -47,6 +47,14 @@ pub enum QueueCommand {
Wield { Wield {
possession_id: String, possession_id: String,
}, },
ImprovWith {
possession_id: String,
},
ImprovFrom {
output: PossessionType,
possession_ids: BTreeSet<String>,
already_used: BTreeSet<String>,
},
} }
impl QueueCommand { impl QueueCommand {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
@ -62,6 +70,8 @@ impl QueueCommand {
Use { .. } => "Use", Use { .. } => "Use",
Wear { .. } => "Wear", Wear { .. } => "Wear",
Wield { .. } => "Wield", Wield { .. } => "Wield",
ImprovWith { .. } => "ImprovWith",
ImprovFrom { .. } => "ImprovFrom",
} }
} }
} }
@ -127,6 +137,14 @@ fn queue_command_registry(
"Wield", "Wield",
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &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() .into_iter()
.collect() .collect()

View File

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

View File

@ -1,30 +1,43 @@
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
models::{ models::{
item::{Item, SkillType, StatType, BuffImpact}, item::{BuffImpact, Item, SkillType, StatType},
user::User user::User,
},
services::combat::{change_health, soak_damage},
static_content::{
possession_type::{DamageDistribution, DamageType},
species::BodyPart,
}, },
DResult, DResult,
}; };
use rand::{self, Rng};
use chrono::Utc; use chrono::Utc;
use std::collections::BTreeMap;
use mockall_double::double; 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) { pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
target_item.total_stats = BTreeMap::new(); target_item.total_stats = BTreeMap::new();
// 1: Start with total stats = raw stats // 1: Start with total stats = raw stats
for stat_type in StatType::values() { for stat_type in StatType::values() {
target_item.total_stats.insert(stat_type.clone(), target_item.total_stats.insert(
*user.raw_stats.get(&stat_type).unwrap_or(&0.0)); stat_type.clone(),
*user.raw_stats.get(&stat_type).unwrap_or(&0.0),
);
} }
// 2: Apply stat (de)buffs... // 2: Apply stat (de)buffs...
for buff in &target_item.temporary_buffs { for buff in &target_item.temporary_buffs {
for impact in &buff.impacts { for impact in &buff.impacts {
match impact { match impact {
BuffImpact::ChangeStat { stat, magnitude } => { BuffImpact::ChangeStat { stat, magnitude } => {
target_item.total_stats.entry(stat.clone()) target_item
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0)) .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)); .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 // 3: Total skills = raw skills
target_item.total_skills = BTreeMap::new(); target_item.total_skills = BTreeMap::new();
for skill_type in SkillType::values() { for skill_type in SkillType::values() {
target_item.total_skills.insert(skill_type.clone(), target_item.total_skills.insert(
*user.raw_skills.get(&skill_type).unwrap_or(&0.0)); skill_type.clone(),
*user.raw_skills.get(&skill_type).unwrap_or(&0.0),
);
} }
// 4: Adjust skills by stats // 4: Adjust skills by stats
let brn = *target_item.total_stats.get(&StatType::Brains).unwrap_or(&0.0); let brn = *target_item
let sen = *target_item.total_stats.get(&StatType::Senses).unwrap_or(&0.0); .total_stats
let brw = *target_item.total_stats.get(&StatType::Brawn).unwrap_or(&0.0); .get(&StatType::Brains)
let refl = *target_item.total_stats.get(&StatType::Reflexes).unwrap_or(&0.0); .unwrap_or(&0.0);
let end = *target_item.total_stats.get(&StatType::Endurance).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); let col = *target_item.total_stats.get(&StatType::Cool).unwrap_or(&0.0);
target_item.total_skills.entry(SkillType::Appraise) target_item
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Appraise) .entry(SkillType::Appraise)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .and_modify(|sk| *sk += brn * 0.5)
target_item.total_skills.entry(SkillType::Blades) .or_insert(brn * 0.5);
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item
target_item.total_skills.entry(SkillType::Blades) .total_skills
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .entry(SkillType::Appraise)
target_item.total_skills.entry(SkillType::Bombs) .and_modify(|sk| *sk += sen * 0.5)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); .or_insert(sen * 0.5);
target_item.total_skills.entry(SkillType::Bombs) target_item
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Chemistry) .entry(SkillType::Blades)
.and_modify(|sk| *sk += brn).or_insert(brn); .and_modify(|sk| *sk += refl * 0.5)
target_item.total_skills.entry(SkillType::Climb) .or_insert(refl * 0.5);
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item
target_item.total_skills.entry(SkillType::Climb) .total_skills
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .entry(SkillType::Blades)
target_item.total_skills.entry(SkillType::Clubs) .and_modify(|sk| *sk += col * 0.5)
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); .or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Clubs) target_item
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Craft) .entry(SkillType::Bombs)
.and_modify(|sk| *sk += brn).or_insert(brn); .and_modify(|sk| *sk += brn * 0.5)
target_item.total_skills.entry(SkillType::Dodge) .or_insert(brn * 0.5);
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item
target_item.total_skills.entry(SkillType::Dodge) .total_skills
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .entry(SkillType::Bombs)
target_item.total_skills.entry(SkillType::Fish) .and_modify(|sk| *sk += col * 0.5)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Fish) target_item
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Fists) .entry(SkillType::Chemistry)
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); .and_modify(|sk| *sk += brn)
target_item.total_skills.entry(SkillType::Fists) .or_insert(brn);
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item
target_item.total_skills.entry(SkillType::Focus) .total_skills
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .entry(SkillType::Climb)
target_item.total_skills.entry(SkillType::Focus) .and_modify(|sk| *sk += refl * 0.5)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Fuck) target_item
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Fuck) .entry(SkillType::Climb)
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .and_modify(|sk| *sk += end * 0.5)
target_item.total_skills.entry(SkillType::Hack) .or_insert(end * 0.5);
.and_modify(|sk| *sk += brn).or_insert(brn); target_item
target_item.total_skills.entry(SkillType::Locksmith) .total_skills
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); .entry(SkillType::Clubs)
target_item.total_skills.entry(SkillType::Locksmith) .and_modify(|sk| *sk += brw * 0.5)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .or_insert(brw * 0.5);
target_item.total_skills.entry(SkillType::Medic) target_item
.and_modify(|sk| *sk += brn).or_insert(brn); .total_skills
target_item.total_skills.entry(SkillType::Persuade) .entry(SkillType::Clubs)
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); .and_modify(|sk| *sk += refl * 0.5)
target_item.total_skills.entry(SkillType::Persuade) .or_insert(refl * 0.5);
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item
target_item.total_skills.entry(SkillType::Pilot) .total_skills
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); .entry(SkillType::Craft)
target_item.total_skills.entry(SkillType::Pilot) .and_modify(|sk| *sk += brn)
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .or_insert(brn);
target_item.total_skills.entry(SkillType::Pistols) target_item
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Pistols) .entry(SkillType::Dodge)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .and_modify(|sk| *sk += sen * 0.5)
target_item.total_skills.entry(SkillType::Quickdraw) .or_insert(sen * 0.5);
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item
target_item.total_skills.entry(SkillType::Quickdraw) .total_skills
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .entry(SkillType::Dodge)
target_item.total_skills.entry(SkillType::Repair) .and_modify(|sk| *sk += refl * 0.5)
.and_modify(|sk| *sk += brn).or_insert(brn); .or_insert(refl * 0.5);
target_item.total_skills.entry(SkillType::Rifles) target_item
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Rifles) .entry(SkillType::Fish)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .and_modify(|sk| *sk += end * 0.5)
target_item.total_skills.entry(SkillType::Scavenge) .or_insert(end * 0.5);
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item
target_item.total_skills.entry(SkillType::Scavenge) .total_skills
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .entry(SkillType::Fish)
target_item.total_skills.entry(SkillType::Science) .and_modify(|sk| *sk += col * 0.5)
.and_modify(|sk| *sk += brn).or_insert(brn); .or_insert(col * 0.5);
target_item.total_skills.entry(SkillType::Sneak) target_item
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .total_skills
target_item.total_skills.entry(SkillType::Sneak) .entry(SkillType::Fists)
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); .and_modify(|sk| *sk += brw * 0.5)
target_item.total_skills.entry(SkillType::Spears) .or_insert(brw * 0.5);
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item
target_item.total_skills.entry(SkillType::Spears) .total_skills
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); .entry(SkillType::Fists)
target_item.total_skills.entry(SkillType::Swim) .and_modify(|sk| *sk += end * 0.5)
.and_modify(|sk| *sk += end).or_insert(brn); .or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Teach) target_item
.and_modify(|sk| *sk += brn).or_insert(brn); .total_skills
target_item.total_skills.entry(SkillType::Throw) .entry(SkillType::Focus)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .and_modify(|sk| *sk += sen * 0.5)
target_item.total_skills.entry(SkillType::Throw) .or_insert(sen * 0.5);
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); target_item
target_item.total_skills.entry(SkillType::Track) .total_skills
.and_modify(|sk| *sk += sen).or_insert(brn); .entry(SkillType::Focus)
target_item.total_skills.entry(SkillType::Whips) .and_modify(|sk| *sk += end * 0.5)
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); .or_insert(end * 0.5);
target_item.total_skills.entry(SkillType::Whips) target_item
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); .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... // 5: Apply skill (de)buffs...
for buff in &target_item.temporary_buffs { for buff in &target_item.temporary_buffs {
for impact in &buff.impacts { for impact in &buff.impacts {
match impact { match impact {
BuffImpact::ChangeSkill { skill, magnitude } => { BuffImpact::ChangeSkill { skill, magnitude } => {
target_item.total_skills.entry(skill.clone()) target_item
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0)) .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)); .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. // Note: Caller must save who because skills might update.
// Don't return error if skillcheck fails, it can fail but still grind. // 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 gap = calc_level_gap(who, skill, diff_level);
let result = skill_check_fn(gap); let result = skill_check_fn(gap);
// If the skill gap is 0, probability of learning is 0.5 // 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). // 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 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((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 let Some(mut user) = trans.find_by_username(&who.item_code).await? {
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0 || if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0
!user.last_skill_improve.get(skill) || !user
.map(|t| (Utc::now() - *t).num_seconds() > 60).unwrap_or(true) { .last_skill_improve
return Ok(result) .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()); user.last_skill_improve.insert(skill.clone(), Utc::now());
trans.queue_for_session(&sess, trans
Some(&format!("Your raw {} is now {:.2}\n", .queue_for_session(
skill.display(), user.raw_skills &sess,
.get(skill).unwrap_or(&0.0)))).await?; 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?; trans.save_user_model(&user).await?;
calculate_total_stats_skills_for_user(who, &user); 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) 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 once_cell::sync::OnceCell;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
mod blade; mod blade;
mod corp_licence; 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 struct WeaponAttackData {
pub start_messages: AttackMessageChoice, pub start_messages: AttackMessageChoice,
pub success_messages: AttackMessageChoicePart, 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 struct WeaponData {
pub uses_skill: SkillType, pub uses_skill: SkillType,
pub raw_min_to_learn: f64, pub raw_min_to_learn: f64,
@ -298,6 +317,7 @@ pub enum PossessionType {
LeatherPants, LeatherPants,
// Weapons: Whips // Weapons: Whips
AntennaWhip, AntennaWhip,
LeatherWhip,
// Weapons: Blades // Weapons: Blades
ButcherKnife, ButcherKnife,
// Medical // 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> { pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new(); static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
&RELEVANT.get_or_init(|| { &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)] #[cfg(test)]
mod tests { mod tests {
use itertools::Itertools;
use super::*; use super::*;
#[test] #[test]
fn other_damage_types_add_to_less_than_one() { 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 crate::models::item::SkillType;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -42,6 +42,45 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
}), }),
..Default::default() ..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()
}
),
)) ))
} }