diff --git a/blastmud_game/src/language.rs b/blastmud_game/src/language.rs index 79dcc12..f9693d2 100644 --- a/blastmud_game/src/language.rs +++ b/blastmud_game/src/language.rs @@ -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> = 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); } } diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index fcc5d99..599c39c 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -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(), }, ) diff --git a/blastmud_game/src/message_handler/user_commands/improvise.rs b/blastmud_game/src/message_handler/user_commands/improvise.rs new file mode 100644 index 0000000..e23da46 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/improvise.rs @@ -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 { + 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 { + 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 = craft_data.inputs.clone(); + + let mut to_destroy_if_success: Vec> = 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 improvise with item or improvise item from item, item, ...").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 = 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; diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index 9608f8c..58be554 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -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, + already_used: BTreeSet, + }, } 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() diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 97c0013..9fa347b 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -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( + trans: &DBTrans, + attack: &DamageDist, victim: &Item, presoak_amount: f64, part: &BodyPart, ) -> DResult { - 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::(), - )); + let damage_by_type: Vec<(&DamageType, f64)> = attack.distribute_damage(presoak_amount); - let mut clothes: Vec = ctx - .trans + let mut clothes: Vec = 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, diff --git a/blastmud_game/src/services/skills.rs b/blastmud_game/src/services/skills.rs index 60ac77d..5e1239a 100644 --- a/blastmud_game/src/services/skills.rs +++ b/blastmud_game/src/services/skills.rs @@ -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)); } _ => {} @@ -160,7 +347,7 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User } } } - + pub fn calc_level_gap(who: &Item, skill: &SkillType, diff_level: f64) -> f64 { let user_level = who.total_skills.get(skill).unwrap_or(&0.0); diff_level - user_level.clone() @@ -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 { +pub async fn skill_check_and_grind( + trans: &DBTrans, + who: &mut Item, + skill: &SkillType, + diff_level: f64, +) -> DResult { 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::() < 0.5 * (LAMBDA * (gap as f64)).exp() { + if who.item_type == "player" + && rand::thread_rng().gen::() < 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::::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(()) +} diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 8db5362..c1c867f 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -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::(), + )); + 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 &'static BTreeMap> { + static POSSESSION_NAMES: OnceCell>> = 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| 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 { static RELEVANT: OnceCell> = OnceCell::new(); &RELEVANT.get_or_init(|| { @@ -426,8 +464,64 @@ pub fn can_butcher_possessions() -> &'static Vec { }) } +pub struct CraftData { + pub skill: SkillType, + pub difficulty: f64, + pub inputs: Vec, + pub output: PossessionType, +} + +pub fn improv_table() -> &'static Vec { + static IMPROV_CELL: OnceCell> = 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> { + static MAP_CELL: OnceCell>> = 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 { + static MAP_CELL: OnceCell> = 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::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() + ); + } } diff --git a/blastmud_game/src/static_content/possession_type/head_armour.rs b/blastmud_game/src/static_content/possession_type/head_armour.rs index 23c9db7..5fa2528 100644 --- a/blastmud_game/src/static_content/possession_type/head_armour.rs +++ b/blastmud_game/src/static_content/possession_type/head_armour.rs @@ -74,7 +74,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { damage_probability_per_soak: 0.1 } ), - ).into_iter().collect() + ).into_iter().collect() }), ..Default::default() } diff --git a/blastmud_game/src/static_content/possession_type/whip.rs b/blastmud_game/src/static_content/possession_type/whip.rs index 13d6280..97e52fe 100644 --- a/blastmud_game/src/static_content/possession_type/whip.rs +++ b/blastmud_game/src/static_content/possession_type/whip.rs @@ -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() + } + ), )) }