Add an improvise command to craft things without tools.
This commit is contained in:
parent
3292dcc13b
commit
cd40573345
@ -7,51 +7,194 @@ struct PluralRule<'l> {
|
||||
append_suffix: &'l str,
|
||||
}
|
||||
|
||||
pub fn pluralise(input: &str) -> String {
|
||||
pub fn pluralise(orig_input: &str) -> String {
|
||||
let mut extra_suffix: &str = "";
|
||||
let mut input: &str = orig_input;
|
||||
'wordsplit: for split_word in vec!["pair"] {
|
||||
for (idx, _) in input.match_indices(split_word) {
|
||||
let end_idx = idx + split_word.len();
|
||||
if end_idx == input.len() {
|
||||
continue;
|
||||
}
|
||||
if (idx == 0 || &input[idx - 1..idx] == " ") && &input[end_idx..end_idx + 1] == " " {
|
||||
extra_suffix = &input[end_idx..];
|
||||
input = &input[0..end_idx];
|
||||
break 'wordsplit;
|
||||
}
|
||||
}
|
||||
}
|
||||
static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
|
||||
let plural_rules = PLURAL_RULES.get_or_init(|| vec!(
|
||||
PluralRule { match_suffix: "foot", drop: 3, append_suffix: "eet" },
|
||||
PluralRule { match_suffix: "tooth", drop: 4, append_suffix: "eeth" },
|
||||
PluralRule { match_suffix: "man", drop: 2, append_suffix: "en" },
|
||||
PluralRule { match_suffix: "mouse", drop: 4, append_suffix: "ice" },
|
||||
PluralRule { match_suffix: "louse", drop: 4, append_suffix: "ice" },
|
||||
PluralRule { match_suffix: "fish", drop: 0, append_suffix: "" },
|
||||
PluralRule { match_suffix: "sheep", drop: 0, append_suffix: "" },
|
||||
PluralRule { match_suffix: "deer", drop: 0, append_suffix: "" },
|
||||
PluralRule { match_suffix: "pox", drop: 0, append_suffix: "" },
|
||||
PluralRule { match_suffix: "cis", drop: 2, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "sis", drop: 2, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "xis", drop: 2, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "ss", drop: 0, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "ch", drop: 0, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "sh", drop: 0, append_suffix: "es" },
|
||||
PluralRule { match_suffix: "ife", drop: 2, append_suffix: "ves" },
|
||||
PluralRule { match_suffix: "lf", drop: 1, append_suffix: "ves" },
|
||||
PluralRule { match_suffix: "arf", drop: 1, append_suffix: "ves" },
|
||||
PluralRule { match_suffix: "ay", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "ey", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "iy", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "oy", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "uy", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "y", drop: 1, append_suffix: "ies" },
|
||||
PluralRule { match_suffix: "ao", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "eo", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "io", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "oo", drop: 0, append_suffix: "s" },
|
||||
PluralRule { match_suffix: "uo", drop: 0, append_suffix: "s" },
|
||||
// The o rule could be much larger... we'll add specific exceptions as
|
||||
// the come up.
|
||||
PluralRule { match_suffix: "o", drop: 0, append_suffix: "es" },
|
||||
// Lots of possible exceptions here.
|
||||
PluralRule { match_suffix: "ex", drop: 0, append_suffix: "es" },
|
||||
));
|
||||
let plural_rules = PLURAL_RULES.get_or_init(|| {
|
||||
vec![
|
||||
PluralRule {
|
||||
match_suffix: "foot",
|
||||
drop: 3,
|
||||
append_suffix: "eet",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "tooth",
|
||||
drop: 4,
|
||||
append_suffix: "eeth",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "man",
|
||||
drop: 2,
|
||||
append_suffix: "en",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "mouse",
|
||||
drop: 4,
|
||||
append_suffix: "ice",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "louse",
|
||||
drop: 4,
|
||||
append_suffix: "ice",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "fish",
|
||||
drop: 0,
|
||||
append_suffix: "",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "sheep",
|
||||
drop: 0,
|
||||
append_suffix: "",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "deer",
|
||||
drop: 0,
|
||||
append_suffix: "",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "pox",
|
||||
drop: 0,
|
||||
append_suffix: "",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "cis",
|
||||
drop: 2,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "sis",
|
||||
drop: 2,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "xis",
|
||||
drop: 2,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ss",
|
||||
drop: 0,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ch",
|
||||
drop: 0,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "sh",
|
||||
drop: 0,
|
||||
append_suffix: "es",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ife",
|
||||
drop: 2,
|
||||
append_suffix: "ves",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "lf",
|
||||
drop: 1,
|
||||
append_suffix: "ves",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "arf",
|
||||
drop: 1,
|
||||
append_suffix: "ves",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ay",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ey",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "iy",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "oy",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "uy",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "y",
|
||||
drop: 1,
|
||||
append_suffix: "ies",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "ao",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "eo",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "io",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "oo",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
PluralRule {
|
||||
match_suffix: "uo",
|
||||
drop: 0,
|
||||
append_suffix: "s",
|
||||
},
|
||||
// The o rule could be much larger... we'll add specific exceptions as
|
||||
// the come up.
|
||||
PluralRule {
|
||||
match_suffix: "o",
|
||||
drop: 0,
|
||||
append_suffix: "es",
|
||||
},
|
||||
// Lots of possible exceptions here.
|
||||
PluralRule {
|
||||
match_suffix: "ex",
|
||||
drop: 0,
|
||||
append_suffix: "es",
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
for rule in plural_rules {
|
||||
if input.ends_with(rule.match_suffix) {
|
||||
return input[0..(input.len() - rule.drop)].to_owned() + rule.append_suffix;
|
||||
return input[0..(input.len() - rule.drop)].to_owned()
|
||||
+ rule.append_suffix
|
||||
+ extra_suffix;
|
||||
}
|
||||
}
|
||||
input.to_owned() + "s"
|
||||
input.to_owned() + "s" + extra_suffix
|
||||
}
|
||||
|
||||
pub fn indefinite_article(countable_word: &str) -> &'static str {
|
||||
@ -60,17 +203,22 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
||||
}
|
||||
let vowels = ["a", "e", "i", "o", "u"];
|
||||
if !vowels.contains(&&countable_word[0..1]) {
|
||||
if countable_word.starts_with("honor") || countable_word.starts_with("honour") ||
|
||||
countable_word.starts_with("honest") || countable_word.starts_with("hour") ||
|
||||
countable_word.starts_with("heir") {
|
||||
return "an";
|
||||
}
|
||||
if countable_word.starts_with("honor")
|
||||
|| countable_word.starts_with("honour")
|
||||
|| countable_word.starts_with("honest")
|
||||
|| countable_word.starts_with("hour")
|
||||
|| countable_word.starts_with("heir")
|
||||
{
|
||||
return "an";
|
||||
}
|
||||
return "a";
|
||||
}
|
||||
if countable_word.starts_with("eu")
|
||||
|| countable_word.starts_with("one")
|
||||
|| countable_word.starts_with("once")
|
||||
{
|
||||
return "a";
|
||||
}
|
||||
if countable_word.starts_with("eu") || countable_word.starts_with("one") ||
|
||||
countable_word.starts_with("once") {
|
||||
return "a";
|
||||
}
|
||||
if countable_word.starts_with("e") {
|
||||
if countable_word.starts_with("ewe") {
|
||||
return "a";
|
||||
@ -82,12 +230,13 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
||||
return "an";
|
||||
}
|
||||
if countable_word.starts_with("uni") {
|
||||
if countable_word.starts_with("unid") ||
|
||||
countable_word.starts_with("unim") ||
|
||||
countable_word.starts_with("unin") {
|
||||
// unidentified, unimaginable, uninhabited etc...
|
||||
return "an";
|
||||
}
|
||||
if countable_word.starts_with("unid")
|
||||
|| countable_word.starts_with("unim")
|
||||
|| countable_word.starts_with("unin")
|
||||
{
|
||||
// unidentified, unimaginable, uninhabited etc...
|
||||
return "an";
|
||||
}
|
||||
// Words like unilateral
|
||||
return "a";
|
||||
}
|
||||
@ -101,8 +250,11 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
||||
}
|
||||
return "an";
|
||||
}
|
||||
if countable_word.starts_with("ubiq") || countable_word.starts_with("uku") || countable_word.starts_with("ukr") {
|
||||
return "a"
|
||||
if countable_word.starts_with("ubiq")
|
||||
|| countable_word.starts_with("uku")
|
||||
|| countable_word.starts_with("ukr")
|
||||
{
|
||||
return "a";
|
||||
}
|
||||
}
|
||||
return "an";
|
||||
@ -120,13 +272,16 @@ pub fn join_words(words: &[&str]) -> String {
|
||||
match words.split_last() {
|
||||
None => "".to_string(),
|
||||
Some((last, [])) => last.to_string(),
|
||||
Some((last, rest)) => rest.join(", ") + " and " + last
|
||||
Some((last, rest)) => rest.join(", ") + " and " + last,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weight(grams: u64) -> String {
|
||||
if grams > 999 {
|
||||
format!("{} kg", Decimal::from_i128_with_scale(grams as i128, 3).normalize())
|
||||
format!(
|
||||
"{} kg",
|
||||
Decimal::from_i128_with_scale(grams as i128, 3).normalize()
|
||||
)
|
||||
} else {
|
||||
format!("{} g", grams)
|
||||
}
|
||||
@ -136,7 +291,7 @@ pub fn weight(grams: u64) -> String {
|
||||
mod test {
|
||||
#[test]
|
||||
fn pluralise_should_follow_english_rules() {
|
||||
for (word, plural) in vec!(
|
||||
for (word, plural) in vec![
|
||||
("cat", "cats"),
|
||||
("wolf", "wolves"),
|
||||
("scarf", "scarves"),
|
||||
@ -151,14 +306,17 @@ mod test {
|
||||
("killer blowfly", "killer blowflies"),
|
||||
("house mouse", "house mice"),
|
||||
("zombie sheep", "zombie sheep"),
|
||||
) {
|
||||
("brown pair of pants", "brown pairs of pants"),
|
||||
("good pair", "good pairs"),
|
||||
("repair kit", "repair kits"),
|
||||
] {
|
||||
assert_eq!(super::pluralise(word), plural);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indefinite_article_should_follow_english_rules() {
|
||||
for (article, word) in vec!(
|
||||
for (article, word) in vec![
|
||||
("a", "cat"),
|
||||
("a", "human"),
|
||||
("an", "apple"),
|
||||
@ -180,33 +338,39 @@ mod test {
|
||||
("a", "user"),
|
||||
("a", "ubiquitous hazard"),
|
||||
("a", "unitary plan"),
|
||||
) {
|
||||
] {
|
||||
let result = super::indefinite_article(&word.to_lowercase());
|
||||
assert_eq!(format!("{} {}", result, word), format!("{} {}", article, word));
|
||||
assert_eq!(
|
||||
format!("{} {}", result, word),
|
||||
format!("{} {}", article, word)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_first_works() {
|
||||
for (inp, outp) in vec!(
|
||||
for (inp, outp) in vec![
|
||||
("", ""),
|
||||
("cat", "Cat"),
|
||||
("Cat", "Cat"),
|
||||
("hello world", "Hello world"),
|
||||
) {
|
||||
] {
|
||||
assert_eq!(super::caps_first(inp), outp);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_words_works() {
|
||||
for (inp, outp) in vec!(
|
||||
(vec!(), ""),
|
||||
(vec!("cat"), "cat"),
|
||||
(vec!("cat", "dog"), "cat and dog"),
|
||||
(vec!("cat", "dog", "fish"), "cat, dog and fish"),
|
||||
(vec!("wolf", "cat", "dog", "fish"), "wolf, cat, dog and fish"),
|
||||
) {
|
||||
for (inp, outp) in vec![
|
||||
(vec![], ""),
|
||||
(vec!["cat"], "cat"),
|
||||
(vec!["cat", "dog"], "cat and dog"),
|
||||
(vec!["cat", "dog", "fish"], "cat, dog and fish"),
|
||||
(
|
||||
vec!["wolf", "cat", "dog", "fish"],
|
||||
"wolf, cat, dog and fish",
|
||||
),
|
||||
] {
|
||||
assert_eq!(super::join_words(&inp[..]), outp);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
},
|
||||
)
|
||||
|
459
blastmud_game/src/message_handler/user_commands/improvise.rs
Normal file
459
blastmud_game/src/message_handler/user_commands/improvise.rs
Normal 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;
|
@ -1,16 +1,16 @@
|
||||
use super::{TaskHandler, TaskRunContext};
|
||||
use crate::message_handler::user_commands::{
|
||||
close, cut, drop, get, get_user_or_fail, movement, open, remove, use_cmd, user_error, wear,
|
||||
wield, CommandHandlingError, UResult, VerbContext,
|
||||
close, cut, drop, get, get_user_or_fail, improvise, movement, open, remove, use_cmd,
|
||||
user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
|
||||
};
|
||||
use crate::models::task::{Task, TaskDetails, TaskMeta};
|
||||
use crate::static_content::room::Direction;
|
||||
use crate::static_content::{possession_type::PossessionType, room::Direction};
|
||||
use crate::DResult;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -47,6 +47,14 @@ pub enum QueueCommand {
|
||||
Wield {
|
||||
possession_id: String,
|
||||
},
|
||||
ImprovWith {
|
||||
possession_id: String,
|
||||
},
|
||||
ImprovFrom {
|
||||
output: PossessionType,
|
||||
possession_ids: BTreeSet<String>,
|
||||
already_used: BTreeSet<String>,
|
||||
},
|
||||
}
|
||||
impl QueueCommand {
|
||||
pub fn name(&self) -> &'static str {
|
||||
@ -62,6 +70,8 @@ impl QueueCommand {
|
||||
Use { .. } => "Use",
|
||||
Wear { .. } => "Wear",
|
||||
Wield { .. } => "Wield",
|
||||
ImprovWith { .. } => "ImprovWith",
|
||||
ImprovFrom { .. } => "ImprovFrom",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,6 +137,14 @@ fn queue_command_registry(
|
||||
"Wield",
|
||||
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"ImprovWith",
|
||||
&improvise::WithQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"ImprovFrom",
|
||||
&improvise::FromQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
|
@ -16,7 +16,9 @@ use crate::{
|
||||
static_content::{
|
||||
journals::{award_journal_if_needed, check_journal_for_kill},
|
||||
npc::npc_by_code,
|
||||
possession_type::{fist, possession_data, DamageType, WeaponAttackData, WeaponData},
|
||||
possession_type::{
|
||||
fist, possession_data, DamageDistribution, DamageType, WeaponAttackData, WeaponData,
|
||||
},
|
||||
species::{species_info_map, BodyPart},
|
||||
},
|
||||
DResult,
|
||||
@ -30,25 +32,16 @@ use rand::{prelude::IteratorRandom, Rng};
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use std::time;
|
||||
|
||||
async fn soak_damage(
|
||||
ctx: &mut TaskRunContext<'_>,
|
||||
attack: &WeaponAttackData,
|
||||
pub async fn soak_damage<DamageDist: DamageDistribution>(
|
||||
trans: &DBTrans,
|
||||
attack: &DamageDist,
|
||||
victim: &Item,
|
||||
presoak_amount: f64,
|
||||
part: &BodyPart,
|
||||
) -> DResult<f64> {
|
||||
let mut damage_by_type: Vec<(&DamageType, f64)> = attack
|
||||
.other_damage_types
|
||||
.iter()
|
||||
.map(|(frac, dtype)| (dtype, frac * presoak_amount))
|
||||
.collect();
|
||||
damage_by_type.push((
|
||||
&attack.base_damage_type,
|
||||
presoak_amount - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
|
||||
));
|
||||
let damage_by_type: Vec<(&DamageType, f64)> = attack.distribute_damage(presoak_amount);
|
||||
|
||||
let mut clothes: Vec<Item> = ctx
|
||||
.trans
|
||||
let mut clothes: Vec<Item> = trans
|
||||
.find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
|
||||
.await?
|
||||
.iter()
|
||||
@ -83,9 +76,9 @@ async fn soak_damage(
|
||||
clothing.health -= clothes_damage;
|
||||
if victim.item_type == "player" {
|
||||
if let Some((vic_sess, sess_dat)) =
|
||||
ctx.trans.find_session_for_player(&victim.item_code).await?
|
||||
trans.find_session_for_player(&victim.item_code).await?
|
||||
{
|
||||
ctx.trans
|
||||
trans
|
||||
.queue_for_session(
|
||||
&vic_sess,
|
||||
Some(&format!(
|
||||
@ -104,14 +97,14 @@ async fn soak_damage(
|
||||
|
||||
for clothing in &clothes {
|
||||
if clothing.health <= 0 {
|
||||
ctx.trans
|
||||
trans
|
||||
.delete_item(&clothing.item_type, &clothing.item_code)
|
||||
.await?;
|
||||
if victim.item_type == "player" {
|
||||
if let Some((vic_sess, sess_dat)) =
|
||||
ctx.trans.find_session_for_player(&victim.item_code).await?
|
||||
trans.find_session_for_player(&victim.item_code).await?
|
||||
{
|
||||
ctx.trans
|
||||
trans
|
||||
.queue_for_session(
|
||||
&vic_sess,
|
||||
Some(&format!(
|
||||
@ -214,8 +207,8 @@ async fn process_attack(
|
||||
.max(1.0) as i64;
|
||||
ctx.trans.save_item_model(&attacker_item).await?;
|
||||
let actual_damage = soak_damage(
|
||||
ctx,
|
||||
&attack,
|
||||
&ctx.trans,
|
||||
attack,
|
||||
victim_item,
|
||||
actual_damage_presoak as f64,
|
||||
&part,
|
||||
|
@ -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<f64> {
|
||||
pub async fn skill_check_and_grind(
|
||||
trans: &DBTrans,
|
||||
who: &mut Item,
|
||||
skill: &SkillType,
|
||||
diff_level: f64,
|
||||
) -> DResult<f64> {
|
||||
let gap = calc_level_gap(who, skill, diff_level);
|
||||
let result = skill_check_fn(gap);
|
||||
|
||||
// If the skill gap is 0, probability of learning is 0.5
|
||||
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
||||
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
||||
if who.item_type == "player" && rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp() {
|
||||
if who.item_type == "player"
|
||||
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp()
|
||||
{
|
||||
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
||||
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
||||
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0 ||
|
||||
!user.last_skill_improve.get(skill)
|
||||
.map(|t| (Utc::now() - *t).num_seconds() > 60).unwrap_or(true) {
|
||||
return Ok(result)
|
||||
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0
|
||||
|| !user
|
||||
.last_skill_improve
|
||||
.get(skill)
|
||||
.map(|t| (Utc::now() - *t).num_seconds() > 60)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
user.raw_skills.entry(skill.clone()).and_modify(|raw| *raw += 0.01).or_insert(0.01);
|
||||
user.raw_skills
|
||||
.entry(skill.clone())
|
||||
.and_modify(|raw| *raw += 0.01)
|
||||
.or_insert(0.01);
|
||||
user.last_skill_improve.insert(skill.clone(), Utc::now());
|
||||
trans.queue_for_session(&sess,
|
||||
Some(&format!("Your raw {} is now {:.2}\n",
|
||||
skill.display(), user.raw_skills
|
||||
.get(skill).unwrap_or(&0.0)))).await?;
|
||||
trans
|
||||
.queue_for_session(
|
||||
&sess,
|
||||
Some(&format!(
|
||||
"Your raw {} is now {:.2}\n",
|
||||
skill.display(),
|
||||
user.raw_skills.get(skill).unwrap_or(&0.0)
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
trans.save_user_model(&user).await?;
|
||||
calculate_total_stats_skills_for_user(who, &user);
|
||||
}
|
||||
@ -217,3 +424,45 @@ pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &Skil
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
struct SkillFailDamage {
|
||||
pub damage_type: DamageType,
|
||||
}
|
||||
|
||||
impl DamageDistribution for SkillFailDamage {
|
||||
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)> {
|
||||
vec![(&self.damage_type, total)]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn crit_fail_penalty_for_skill(
|
||||
trans: &DBTrans,
|
||||
who: &mut Item,
|
||||
skill: &SkillType,
|
||||
) -> DResult<()> {
|
||||
use SkillType::*;
|
||||
let (msg, part, dist) = match skill {
|
||||
Bombs | Chemistry => (
|
||||
"Ow! You burn your hand.",
|
||||
BodyPart::Hands,
|
||||
SkillFailDamage {
|
||||
damage_type: DamageType::Shock,
|
||||
},
|
||||
),
|
||||
Craft | Repair => (
|
||||
"Ow! You catch your hand on something sharp.",
|
||||
BodyPart::Hands,
|
||||
SkillFailDamage {
|
||||
damage_type: DamageType::Slash,
|
||||
},
|
||||
),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let actual_damage_presoak = Normal::<f64>::new(5.0, 1.0)?
|
||||
.sample(&mut rand::thread_rng())
|
||||
.floor()
|
||||
.max(1.0);
|
||||
let final_damage = soak_damage(trans, &dist, who, actual_damage_presoak, &part).await?;
|
||||
change_health(trans, -final_damage as i64, who, &msg, &msg).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use async_trait::async_trait;
|
||||
use once_cell::sync::OnceCell;
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod blade;
|
||||
mod corp_licence;
|
||||
@ -56,6 +56,10 @@ impl DamageType {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DamageDistribution {
|
||||
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)>;
|
||||
}
|
||||
|
||||
pub struct WeaponAttackData {
|
||||
pub start_messages: AttackMessageChoice,
|
||||
pub success_messages: AttackMessageChoicePart,
|
||||
@ -94,6 +98,21 @@ impl Default for WeaponAttackData {
|
||||
}
|
||||
}
|
||||
|
||||
impl DamageDistribution for WeaponAttackData {
|
||||
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&'l DamageType, f64)> {
|
||||
let mut damage_by_type: Vec<(&DamageType, f64)> = self
|
||||
.other_damage_types
|
||||
.iter()
|
||||
.map(|(frac, dtype)| (dtype, frac * total))
|
||||
.collect();
|
||||
damage_by_type.push((
|
||||
&self.base_damage_type,
|
||||
total - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
|
||||
));
|
||||
damage_by_type
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WeaponData {
|
||||
pub uses_skill: SkillType,
|
||||
pub raw_min_to_learn: f64,
|
||||
@ -298,6 +317,7 @@ pub enum PossessionType {
|
||||
LeatherPants,
|
||||
// Weapons: Whips
|
||||
AntennaWhip,
|
||||
LeatherWhip,
|
||||
// Weapons: Blades
|
||||
ButcherKnife,
|
||||
// Medical
|
||||
@ -410,6 +430,24 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
|
||||
})
|
||||
}
|
||||
|
||||
pub fn possession_type_names() -> &'static BTreeMap<String, Vec<PossessionType>> {
|
||||
static POSSESSION_NAMES: OnceCell<BTreeMap<String, Vec<PossessionType>>> = OnceCell::new();
|
||||
&POSSESSION_NAMES.get_or_init(|| {
|
||||
let mut map = BTreeMap::new();
|
||||
for (pt, pd) in possession_data() {
|
||||
map.entry(pd.display.to_lowercase())
|
||||
.and_modify(|l: &mut Vec<PossessionType>| l.push(pt.clone()))
|
||||
.or_insert_with(|| vec![pt.clone()]);
|
||||
for alias in &pd.aliases {
|
||||
map.entry(alias.to_lowercase())
|
||||
.and_modify(|l| l.push(pt.clone()))
|
||||
.or_insert_with(|| vec![pt.clone()]);
|
||||
}
|
||||
}
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
||||
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
|
||||
&RELEVANT.get_or_init(|| {
|
||||
@ -426,8 +464,64 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
||||
})
|
||||
}
|
||||
|
||||
pub struct CraftData {
|
||||
pub skill: SkillType,
|
||||
pub difficulty: f64,
|
||||
pub inputs: Vec<PossessionType>,
|
||||
pub output: PossessionType,
|
||||
}
|
||||
|
||||
pub fn improv_table() -> &'static Vec<CraftData> {
|
||||
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
|
||||
IMPROV_CELL.get_or_init(|| {
|
||||
vec![CraftData {
|
||||
skill: SkillType::Craft,
|
||||
difficulty: 6.0,
|
||||
inputs: vec![
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
PossessionType::AnimalSkin,
|
||||
],
|
||||
output: PossessionType::LeatherWhip,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
pub fn improv_by_ingredient() -> &'static BTreeMap<PossessionType, Vec<&'static CraftData>> {
|
||||
static MAP_CELL: OnceCell<BTreeMap<PossessionType, Vec<&'static CraftData>>> = OnceCell::new();
|
||||
MAP_CELL.get_or_init(|| {
|
||||
let mut map = BTreeMap::new();
|
||||
for cd in improv_table() {
|
||||
let inps: BTreeSet<&PossessionType> = cd.inputs.iter().collect();
|
||||
for inp in inps {
|
||||
map.entry(inp.clone())
|
||||
.and_modify(|l: &mut Vec<&'static CraftData>| l.push(cd))
|
||||
.or_insert(vec![cd]);
|
||||
}
|
||||
}
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
pub fn improv_by_output() -> &'static BTreeMap<PossessionType, &'static CraftData> {
|
||||
static MAP_CELL: OnceCell<BTreeMap<PossessionType, &'static CraftData>> = OnceCell::new();
|
||||
MAP_CELL.get_or_init(|| {
|
||||
improv_table()
|
||||
.iter()
|
||||
.map(|cd| (cd.output.clone(), cd))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn other_damage_types_add_to_less_than_one() {
|
||||
@ -444,4 +538,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn possession_type_names_works() {
|
||||
assert!(possession_type_names()
|
||||
.get("whip")
|
||||
.unwrap()
|
||||
.contains(&PossessionType::AntennaWhip));
|
||||
assert!(possession_type_names()
|
||||
.get("animal skin")
|
||||
.unwrap()
|
||||
.contains(&PossessionType::AnimalSkin));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_one_way_to_improv_each_item() {
|
||||
assert_eq!(
|
||||
improv_table()
|
||||
.iter()
|
||||
.group_by(|cd| cd.output.clone())
|
||||
.into_iter()
|
||||
.filter_map(|g| if g.1.count() == 1 { None } else { Some(g.0) })
|
||||
.collect::<Vec<PossessionType>>(),
|
||||
Vec::new()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_improv_item_has_possession_data_for_output() {
|
||||
assert_eq!(
|
||||
improv_table().iter().filter_map(|cd| if possession_data().get(&cd.output).is_none() { Some(cd) } else None ).collect(),
|
||||
Vec::new()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
),
|
||||
))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user