forked from blasthavers/blastmud
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,
|
append_suffix: &'l str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pluralise(input: &str) -> String {
|
pub fn pluralise(orig_input: &str) -> String {
|
||||||
|
let mut extra_suffix: &str = "";
|
||||||
|
let mut input: &str = orig_input;
|
||||||
|
'wordsplit: for split_word in vec!["pair"] {
|
||||||
|
for (idx, _) in input.match_indices(split_word) {
|
||||||
|
let end_idx = idx + split_word.len();
|
||||||
|
if end_idx == input.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (idx == 0 || &input[idx - 1..idx] == " ") && &input[end_idx..end_idx + 1] == " " {
|
||||||
|
extra_suffix = &input[end_idx..];
|
||||||
|
input = &input[0..end_idx];
|
||||||
|
break 'wordsplit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
|
static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
|
||||||
let plural_rules = PLURAL_RULES.get_or_init(|| vec!(
|
let plural_rules = PLURAL_RULES.get_or_init(|| {
|
||||||
PluralRule { match_suffix: "foot", drop: 3, append_suffix: "eet" },
|
vec![
|
||||||
PluralRule { match_suffix: "tooth", drop: 4, append_suffix: "eeth" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "man", drop: 2, append_suffix: "en" },
|
match_suffix: "foot",
|
||||||
PluralRule { match_suffix: "mouse", drop: 4, append_suffix: "ice" },
|
drop: 3,
|
||||||
PluralRule { match_suffix: "louse", drop: 4, append_suffix: "ice" },
|
append_suffix: "eet",
|
||||||
PluralRule { match_suffix: "fish", drop: 0, append_suffix: "" },
|
},
|
||||||
PluralRule { match_suffix: "sheep", drop: 0, append_suffix: "" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "deer", drop: 0, append_suffix: "" },
|
match_suffix: "tooth",
|
||||||
PluralRule { match_suffix: "pox", drop: 0, append_suffix: "" },
|
drop: 4,
|
||||||
PluralRule { match_suffix: "cis", drop: 2, append_suffix: "es" },
|
append_suffix: "eeth",
|
||||||
PluralRule { match_suffix: "sis", drop: 2, append_suffix: "es" },
|
},
|
||||||
PluralRule { match_suffix: "xis", drop: 2, append_suffix: "es" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "ss", drop: 0, append_suffix: "es" },
|
match_suffix: "man",
|
||||||
PluralRule { match_suffix: "ch", drop: 0, append_suffix: "es" },
|
drop: 2,
|
||||||
PluralRule { match_suffix: "sh", drop: 0, append_suffix: "es" },
|
append_suffix: "en",
|
||||||
PluralRule { match_suffix: "ife", drop: 2, append_suffix: "ves" },
|
},
|
||||||
PluralRule { match_suffix: "lf", drop: 1, append_suffix: "ves" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "arf", drop: 1, append_suffix: "ves" },
|
match_suffix: "mouse",
|
||||||
PluralRule { match_suffix: "ay", drop: 0, append_suffix: "s" },
|
drop: 4,
|
||||||
PluralRule { match_suffix: "ey", drop: 0, append_suffix: "s" },
|
append_suffix: "ice",
|
||||||
PluralRule { match_suffix: "iy", drop: 0, append_suffix: "s" },
|
},
|
||||||
PluralRule { match_suffix: "oy", drop: 0, append_suffix: "s" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "uy", drop: 0, append_suffix: "s" },
|
match_suffix: "louse",
|
||||||
PluralRule { match_suffix: "y", drop: 1, append_suffix: "ies" },
|
drop: 4,
|
||||||
PluralRule { match_suffix: "ao", drop: 0, append_suffix: "s" },
|
append_suffix: "ice",
|
||||||
PluralRule { match_suffix: "eo", drop: 0, append_suffix: "s" },
|
},
|
||||||
PluralRule { match_suffix: "io", drop: 0, append_suffix: "s" },
|
PluralRule {
|
||||||
PluralRule { match_suffix: "oo", drop: 0, append_suffix: "s" },
|
match_suffix: "fish",
|
||||||
PluralRule { match_suffix: "uo", drop: 0, append_suffix: "s" },
|
drop: 0,
|
||||||
|
append_suffix: "",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "sheep",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "deer",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "pox",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "cis",
|
||||||
|
drop: 2,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "sis",
|
||||||
|
drop: 2,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "xis",
|
||||||
|
drop: 2,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ss",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ch",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "sh",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ife",
|
||||||
|
drop: 2,
|
||||||
|
append_suffix: "ves",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "lf",
|
||||||
|
drop: 1,
|
||||||
|
append_suffix: "ves",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "arf",
|
||||||
|
drop: 1,
|
||||||
|
append_suffix: "ves",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ay",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ey",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "iy",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "oy",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "uy",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "y",
|
||||||
|
drop: 1,
|
||||||
|
append_suffix: "ies",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "ao",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "eo",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "io",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "oo",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
|
PluralRule {
|
||||||
|
match_suffix: "uo",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "s",
|
||||||
|
},
|
||||||
// The o rule could be much larger... we'll add specific exceptions as
|
// The o rule could be much larger... we'll add specific exceptions as
|
||||||
// the come up.
|
// the come up.
|
||||||
PluralRule { match_suffix: "o", drop: 0, append_suffix: "es" },
|
PluralRule {
|
||||||
|
match_suffix: "o",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
// Lots of possible exceptions here.
|
// Lots of possible exceptions here.
|
||||||
PluralRule { match_suffix: "ex", drop: 0, append_suffix: "es" },
|
PluralRule {
|
||||||
));
|
match_suffix: "ex",
|
||||||
|
drop: 0,
|
||||||
|
append_suffix: "es",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
for rule in plural_rules {
|
for rule in plural_rules {
|
||||||
if input.ends_with(rule.match_suffix) {
|
if input.ends_with(rule.match_suffix) {
|
||||||
return input[0..(input.len() - rule.drop)].to_owned() + rule.append_suffix;
|
return input[0..(input.len() - rule.drop)].to_owned()
|
||||||
|
+ rule.append_suffix
|
||||||
|
+ extra_suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.to_owned() + "s"
|
input.to_owned() + "s" + extra_suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indefinite_article(countable_word: &str) -> &'static str {
|
pub fn indefinite_article(countable_word: &str) -> &'static str {
|
||||||
@ -60,15 +203,20 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
let vowels = ["a", "e", "i", "o", "u"];
|
let vowels = ["a", "e", "i", "o", "u"];
|
||||||
if !vowels.contains(&&countable_word[0..1]) {
|
if !vowels.contains(&&countable_word[0..1]) {
|
||||||
if countable_word.starts_with("honor") || countable_word.starts_with("honour") ||
|
if countable_word.starts_with("honor")
|
||||||
countable_word.starts_with("honest") || countable_word.starts_with("hour") ||
|
|| countable_word.starts_with("honour")
|
||||||
countable_word.starts_with("heir") {
|
|| countable_word.starts_with("honest")
|
||||||
|
|| countable_word.starts_with("hour")
|
||||||
|
|| countable_word.starts_with("heir")
|
||||||
|
{
|
||||||
return "an";
|
return "an";
|
||||||
}
|
}
|
||||||
return "a";
|
return "a";
|
||||||
}
|
}
|
||||||
if countable_word.starts_with("eu") || countable_word.starts_with("one") ||
|
if countable_word.starts_with("eu")
|
||||||
countable_word.starts_with("once") {
|
|| countable_word.starts_with("one")
|
||||||
|
|| countable_word.starts_with("once")
|
||||||
|
{
|
||||||
return "a";
|
return "a";
|
||||||
}
|
}
|
||||||
if countable_word.starts_with("e") {
|
if countable_word.starts_with("e") {
|
||||||
@ -82,9 +230,10 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
|||||||
return "an";
|
return "an";
|
||||||
}
|
}
|
||||||
if countable_word.starts_with("uni") {
|
if countable_word.starts_with("uni") {
|
||||||
if countable_word.starts_with("unid") ||
|
if countable_word.starts_with("unid")
|
||||||
countable_word.starts_with("unim") ||
|
|| countable_word.starts_with("unim")
|
||||||
countable_word.starts_with("unin") {
|
|| countable_word.starts_with("unin")
|
||||||
|
{
|
||||||
// unidentified, unimaginable, uninhabited etc...
|
// unidentified, unimaginable, uninhabited etc...
|
||||||
return "an";
|
return "an";
|
||||||
}
|
}
|
||||||
@ -101,8 +250,11 @@ pub fn indefinite_article(countable_word: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
return "an";
|
return "an";
|
||||||
}
|
}
|
||||||
if countable_word.starts_with("ubiq") || countable_word.starts_with("uku") || countable_word.starts_with("ukr") {
|
if countable_word.starts_with("ubiq")
|
||||||
return "a"
|
|| countable_word.starts_with("uku")
|
||||||
|
|| countable_word.starts_with("ukr")
|
||||||
|
{
|
||||||
|
return "a";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "an";
|
return "an";
|
||||||
@ -120,13 +272,16 @@ pub fn join_words(words: &[&str]) -> String {
|
|||||||
match words.split_last() {
|
match words.split_last() {
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
Some((last, [])) => last.to_string(),
|
Some((last, [])) => last.to_string(),
|
||||||
Some((last, rest)) => rest.join(", ") + " and " + last
|
Some((last, rest)) => rest.join(", ") + " and " + last,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weight(grams: u64) -> String {
|
pub fn weight(grams: u64) -> String {
|
||||||
if grams > 999 {
|
if grams > 999 {
|
||||||
format!("{} kg", Decimal::from_i128_with_scale(grams as i128, 3).normalize())
|
format!(
|
||||||
|
"{} kg",
|
||||||
|
Decimal::from_i128_with_scale(grams as i128, 3).normalize()
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("{} g", grams)
|
format!("{} g", grams)
|
||||||
}
|
}
|
||||||
@ -136,7 +291,7 @@ pub fn weight(grams: u64) -> String {
|
|||||||
mod test {
|
mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn pluralise_should_follow_english_rules() {
|
fn pluralise_should_follow_english_rules() {
|
||||||
for (word, plural) in vec!(
|
for (word, plural) in vec![
|
||||||
("cat", "cats"),
|
("cat", "cats"),
|
||||||
("wolf", "wolves"),
|
("wolf", "wolves"),
|
||||||
("scarf", "scarves"),
|
("scarf", "scarves"),
|
||||||
@ -151,14 +306,17 @@ mod test {
|
|||||||
("killer blowfly", "killer blowflies"),
|
("killer blowfly", "killer blowflies"),
|
||||||
("house mouse", "house mice"),
|
("house mouse", "house mice"),
|
||||||
("zombie sheep", "zombie sheep"),
|
("zombie sheep", "zombie sheep"),
|
||||||
) {
|
("brown pair of pants", "brown pairs of pants"),
|
||||||
|
("good pair", "good pairs"),
|
||||||
|
("repair kit", "repair kits"),
|
||||||
|
] {
|
||||||
assert_eq!(super::pluralise(word), plural);
|
assert_eq!(super::pluralise(word), plural);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn indefinite_article_should_follow_english_rules() {
|
fn indefinite_article_should_follow_english_rules() {
|
||||||
for (article, word) in vec!(
|
for (article, word) in vec![
|
||||||
("a", "cat"),
|
("a", "cat"),
|
||||||
("a", "human"),
|
("a", "human"),
|
||||||
("an", "apple"),
|
("an", "apple"),
|
||||||
@ -180,33 +338,39 @@ mod test {
|
|||||||
("a", "user"),
|
("a", "user"),
|
||||||
("a", "ubiquitous hazard"),
|
("a", "ubiquitous hazard"),
|
||||||
("a", "unitary plan"),
|
("a", "unitary plan"),
|
||||||
) {
|
] {
|
||||||
let result = super::indefinite_article(&word.to_lowercase());
|
let result = super::indefinite_article(&word.to_lowercase());
|
||||||
assert_eq!(format!("{} {}", result, word), format!("{} {}", article, word));
|
assert_eq!(
|
||||||
|
format!("{} {}", result, word),
|
||||||
|
format!("{} {}", article, word)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn caps_first_works() {
|
fn caps_first_works() {
|
||||||
for (inp, outp) in vec!(
|
for (inp, outp) in vec![
|
||||||
("", ""),
|
("", ""),
|
||||||
("cat", "Cat"),
|
("cat", "Cat"),
|
||||||
("Cat", "Cat"),
|
("Cat", "Cat"),
|
||||||
("hello world", "Hello world"),
|
("hello world", "Hello world"),
|
||||||
) {
|
] {
|
||||||
assert_eq!(super::caps_first(inp), outp);
|
assert_eq!(super::caps_first(inp), outp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn join_words_works() {
|
fn join_words_works() {
|
||||||
for (inp, outp) in vec!(
|
for (inp, outp) in vec![
|
||||||
(vec!(), ""),
|
(vec![], ""),
|
||||||
(vec!("cat"), "cat"),
|
(vec!["cat"], "cat"),
|
||||||
(vec!("cat", "dog"), "cat and dog"),
|
(vec!["cat", "dog"], "cat and dog"),
|
||||||
(vec!("cat", "dog", "fish"), "cat, dog and fish"),
|
(vec!["cat", "dog", "fish"], "cat, dog and fish"),
|
||||||
(vec!("wolf", "cat", "dog", "fish"), "wolf, cat, dog and fish"),
|
(
|
||||||
) {
|
vec!["wolf", "cat", "dog", "fish"],
|
||||||
|
"wolf, cat, dog and fish",
|
||||||
|
),
|
||||||
|
] {
|
||||||
assert_eq!(super::join_words(&inp[..]), outp);
|
assert_eq!(super::join_words(&inp[..]), outp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ mod gear;
|
|||||||
pub mod get;
|
pub mod get;
|
||||||
mod help;
|
mod help;
|
||||||
mod ignore;
|
mod ignore;
|
||||||
|
pub mod improvise;
|
||||||
mod install;
|
mod install;
|
||||||
mod inventory;
|
mod inventory;
|
||||||
mod less_explicit_mode;
|
mod less_explicit_mode;
|
||||||
@ -147,6 +148,11 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
"drop" => drop::VERB,
|
"drop" => drop::VERB,
|
||||||
"gear" => gear::VERB,
|
"gear" => gear::VERB,
|
||||||
"get" => get::VERB,
|
"get" => get::VERB,
|
||||||
|
|
||||||
|
"improv" => improvise::VERB,
|
||||||
|
"improvise" => improvise::VERB,
|
||||||
|
"improvize" => improvise::VERB,
|
||||||
|
|
||||||
"install" => install::VERB,
|
"install" => install::VERB,
|
||||||
"inventory" => inventory::VERB,
|
"inventory" => inventory::VERB,
|
||||||
"inv" => inventory::VERB,
|
"inv" => inventory::VERB,
|
||||||
@ -329,7 +335,10 @@ pub async fn search_item_for_user<'l>(
|
|||||||
.resolve_items_by_display_name_for_player(search)
|
.resolve_items_by_display_name_for_player(search)
|
||||||
.await?[..]
|
.await?[..]
|
||||||
{
|
{
|
||||||
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
|
[] => user_error(format!(
|
||||||
|
"Sorry, I couldn't find anything matching \"{}\".",
|
||||||
|
search.query
|
||||||
|
))?,
|
||||||
[match_it] => match_it.clone(),
|
[match_it] => match_it.clone(),
|
||||||
[item1, ..] => item1.clone(),
|
[item1, ..] => item1.clone(),
|
||||||
},
|
},
|
||||||
@ -346,7 +355,10 @@ pub async fn search_items_for_user<'l>(
|
|||||||
.resolve_items_by_display_name_for_player(search)
|
.resolve_items_by_display_name_for_player(search)
|
||||||
.await?[..]
|
.await?[..]
|
||||||
{
|
{
|
||||||
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
|
[] => user_error(format!(
|
||||||
|
"Sorry, I couldn't find anything matching \"{}\".",
|
||||||
|
search.query
|
||||||
|
))?,
|
||||||
v => v.into_iter().map(|it| it.clone()).collect(),
|
v => v.into_iter().map(|it| it.clone()).collect(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
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 super::{TaskHandler, TaskRunContext};
|
||||||
use crate::message_handler::user_commands::{
|
use crate::message_handler::user_commands::{
|
||||||
close, cut, drop, get, get_user_or_fail, movement, open, remove, use_cmd, user_error, wear,
|
close, cut, drop, get, get_user_or_fail, improvise, movement, open, remove, use_cmd,
|
||||||
wield, CommandHandlingError, UResult, VerbContext,
|
user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
|
||||||
};
|
};
|
||||||
use crate::models::task::{Task, TaskDetails, TaskMeta};
|
use crate::models::task::{Task, TaskDetails, TaskMeta};
|
||||||
use crate::static_content::room::Direction;
|
use crate::static_content::{possession_type::PossessionType, room::Direction};
|
||||||
use crate::DResult;
|
use crate::DResult;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@ -47,6 +47,14 @@ pub enum QueueCommand {
|
|||||||
Wield {
|
Wield {
|
||||||
possession_id: String,
|
possession_id: String,
|
||||||
},
|
},
|
||||||
|
ImprovWith {
|
||||||
|
possession_id: String,
|
||||||
|
},
|
||||||
|
ImprovFrom {
|
||||||
|
output: PossessionType,
|
||||||
|
possession_ids: BTreeSet<String>,
|
||||||
|
already_used: BTreeSet<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
impl QueueCommand {
|
impl QueueCommand {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
@ -62,6 +70,8 @@ impl QueueCommand {
|
|||||||
Use { .. } => "Use",
|
Use { .. } => "Use",
|
||||||
Wear { .. } => "Wear",
|
Wear { .. } => "Wear",
|
||||||
Wield { .. } => "Wield",
|
Wield { .. } => "Wield",
|
||||||
|
ImprovWith { .. } => "ImprovWith",
|
||||||
|
ImprovFrom { .. } => "ImprovFrom",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,6 +137,14 @@ fn queue_command_registry(
|
|||||||
"Wield",
|
"Wield",
|
||||||
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"ImprovWith",
|
||||||
|
&improvise::WithQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ImprovFrom",
|
||||||
|
&improvise::FromQueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -16,7 +16,9 @@ use crate::{
|
|||||||
static_content::{
|
static_content::{
|
||||||
journals::{award_journal_if_needed, check_journal_for_kill},
|
journals::{award_journal_if_needed, check_journal_for_kill},
|
||||||
npc::npc_by_code,
|
npc::npc_by_code,
|
||||||
possession_type::{fist, possession_data, DamageType, WeaponAttackData, WeaponData},
|
possession_type::{
|
||||||
|
fist, possession_data, DamageDistribution, DamageType, WeaponAttackData, WeaponData,
|
||||||
|
},
|
||||||
species::{species_info_map, BodyPart},
|
species::{species_info_map, BodyPart},
|
||||||
},
|
},
|
||||||
DResult,
|
DResult,
|
||||||
@ -30,25 +32,16 @@ use rand::{prelude::IteratorRandom, Rng};
|
|||||||
use rand_distr::{Distribution, Normal};
|
use rand_distr::{Distribution, Normal};
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
async fn soak_damage(
|
pub async fn soak_damage<DamageDist: DamageDistribution>(
|
||||||
ctx: &mut TaskRunContext<'_>,
|
trans: &DBTrans,
|
||||||
attack: &WeaponAttackData,
|
attack: &DamageDist,
|
||||||
victim: &Item,
|
victim: &Item,
|
||||||
presoak_amount: f64,
|
presoak_amount: f64,
|
||||||
part: &BodyPart,
|
part: &BodyPart,
|
||||||
) -> DResult<f64> {
|
) -> DResult<f64> {
|
||||||
let mut damage_by_type: Vec<(&DamageType, f64)> = attack
|
let damage_by_type: Vec<(&DamageType, f64)> = attack.distribute_damage(presoak_amount);
|
||||||
.other_damage_types
|
|
||||||
.iter()
|
|
||||||
.map(|(frac, dtype)| (dtype, frac * presoak_amount))
|
|
||||||
.collect();
|
|
||||||
damage_by_type.push((
|
|
||||||
&attack.base_damage_type,
|
|
||||||
presoak_amount - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut clothes: Vec<Item> = ctx
|
let mut clothes: Vec<Item> = trans
|
||||||
.trans
|
|
||||||
.find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
|
.find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
@ -83,9 +76,9 @@ async fn soak_damage(
|
|||||||
clothing.health -= clothes_damage;
|
clothing.health -= clothes_damage;
|
||||||
if victim.item_type == "player" {
|
if victim.item_type == "player" {
|
||||||
if let Some((vic_sess, sess_dat)) =
|
if let Some((vic_sess, sess_dat)) =
|
||||||
ctx.trans.find_session_for_player(&victim.item_code).await?
|
trans.find_session_for_player(&victim.item_code).await?
|
||||||
{
|
{
|
||||||
ctx.trans
|
trans
|
||||||
.queue_for_session(
|
.queue_for_session(
|
||||||
&vic_sess,
|
&vic_sess,
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
@ -104,14 +97,14 @@ async fn soak_damage(
|
|||||||
|
|
||||||
for clothing in &clothes {
|
for clothing in &clothes {
|
||||||
if clothing.health <= 0 {
|
if clothing.health <= 0 {
|
||||||
ctx.trans
|
trans
|
||||||
.delete_item(&clothing.item_type, &clothing.item_code)
|
.delete_item(&clothing.item_type, &clothing.item_code)
|
||||||
.await?;
|
.await?;
|
||||||
if victim.item_type == "player" {
|
if victim.item_type == "player" {
|
||||||
if let Some((vic_sess, sess_dat)) =
|
if let Some((vic_sess, sess_dat)) =
|
||||||
ctx.trans.find_session_for_player(&victim.item_code).await?
|
trans.find_session_for_player(&victim.item_code).await?
|
||||||
{
|
{
|
||||||
ctx.trans
|
trans
|
||||||
.queue_for_session(
|
.queue_for_session(
|
||||||
&vic_sess,
|
&vic_sess,
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
@ -214,8 +207,8 @@ async fn process_attack(
|
|||||||
.max(1.0) as i64;
|
.max(1.0) as i64;
|
||||||
ctx.trans.save_item_model(&attacker_item).await?;
|
ctx.trans.save_item_model(&attacker_item).await?;
|
||||||
let actual_damage = soak_damage(
|
let actual_damage = soak_damage(
|
||||||
ctx,
|
&ctx.trans,
|
||||||
&attack,
|
attack,
|
||||||
victim_item,
|
victim_item,
|
||||||
actual_damage_presoak as f64,
|
actual_damage_presoak as f64,
|
||||||
&part,
|
&part,
|
||||||
|
@ -1,30 +1,43 @@
|
|||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
item::{Item, SkillType, StatType, BuffImpact},
|
item::{BuffImpact, Item, SkillType, StatType},
|
||||||
user::User
|
user::User,
|
||||||
|
},
|
||||||
|
services::combat::{change_health, soak_damage},
|
||||||
|
static_content::{
|
||||||
|
possession_type::{DamageDistribution, DamageType},
|
||||||
|
species::BodyPart,
|
||||||
},
|
},
|
||||||
DResult,
|
DResult,
|
||||||
};
|
};
|
||||||
use rand::{self, Rng};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
#[double] use crate::db::DBTrans;
|
use rand::{self, Rng};
|
||||||
|
use rand_distr::{Distribution, Normal};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
|
pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
|
||||||
target_item.total_stats = BTreeMap::new();
|
target_item.total_stats = BTreeMap::new();
|
||||||
// 1: Start with total stats = raw stats
|
// 1: Start with total stats = raw stats
|
||||||
for stat_type in StatType::values() {
|
for stat_type in StatType::values() {
|
||||||
target_item.total_stats.insert(stat_type.clone(),
|
target_item.total_stats.insert(
|
||||||
*user.raw_stats.get(&stat_type).unwrap_or(&0.0));
|
stat_type.clone(),
|
||||||
|
*user.raw_stats.get(&stat_type).unwrap_or(&0.0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 2: Apply stat (de)buffs...
|
// 2: Apply stat (de)buffs...
|
||||||
for buff in &target_item.temporary_buffs {
|
for buff in &target_item.temporary_buffs {
|
||||||
for impact in &buff.impacts {
|
for impact in &buff.impacts {
|
||||||
match impact {
|
match impact {
|
||||||
BuffImpact::ChangeStat { stat, magnitude } => {
|
BuffImpact::ChangeStat { stat, magnitude } => {
|
||||||
target_item.total_stats.entry(stat.clone())
|
target_item
|
||||||
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0))
|
.total_stats
|
||||||
|
.entry(stat.clone())
|
||||||
|
.and_modify(|old_value| {
|
||||||
|
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||||
|
})
|
||||||
.or_insert((*magnitude).max(0.0));
|
.or_insert((*magnitude).max(0.0));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -34,125 +47,299 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User
|
|||||||
// 3: Total skills = raw skills
|
// 3: Total skills = raw skills
|
||||||
target_item.total_skills = BTreeMap::new();
|
target_item.total_skills = BTreeMap::new();
|
||||||
for skill_type in SkillType::values() {
|
for skill_type in SkillType::values() {
|
||||||
target_item.total_skills.insert(skill_type.clone(),
|
target_item.total_skills.insert(
|
||||||
*user.raw_skills.get(&skill_type).unwrap_or(&0.0));
|
skill_type.clone(),
|
||||||
|
*user.raw_skills.get(&skill_type).unwrap_or(&0.0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 4: Adjust skills by stats
|
// 4: Adjust skills by stats
|
||||||
let brn = *target_item.total_stats.get(&StatType::Brains).unwrap_or(&0.0);
|
let brn = *target_item
|
||||||
let sen = *target_item.total_stats.get(&StatType::Senses).unwrap_or(&0.0);
|
.total_stats
|
||||||
let brw = *target_item.total_stats.get(&StatType::Brawn).unwrap_or(&0.0);
|
.get(&StatType::Brains)
|
||||||
let refl = *target_item.total_stats.get(&StatType::Reflexes).unwrap_or(&0.0);
|
.unwrap_or(&0.0);
|
||||||
let end = *target_item.total_stats.get(&StatType::Endurance).unwrap_or(&0.0);
|
let sen = *target_item
|
||||||
|
.total_stats
|
||||||
|
.get(&StatType::Senses)
|
||||||
|
.unwrap_or(&0.0);
|
||||||
|
let brw = *target_item
|
||||||
|
.total_stats
|
||||||
|
.get(&StatType::Brawn)
|
||||||
|
.unwrap_or(&0.0);
|
||||||
|
let refl = *target_item
|
||||||
|
.total_stats
|
||||||
|
.get(&StatType::Reflexes)
|
||||||
|
.unwrap_or(&0.0);
|
||||||
|
let end = *target_item
|
||||||
|
.total_stats
|
||||||
|
.get(&StatType::Endurance)
|
||||||
|
.unwrap_or(&0.0);
|
||||||
let col = *target_item.total_stats.get(&StatType::Cool).unwrap_or(&0.0);
|
let col = *target_item.total_stats.get(&StatType::Cool).unwrap_or(&0.0);
|
||||||
target_item.total_skills.entry(SkillType::Appraise)
|
target_item
|
||||||
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Appraise)
|
.entry(SkillType::Appraise)
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.and_modify(|sk| *sk += brn * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Blades)
|
.or_insert(brn * 0.5);
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Blades)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.entry(SkillType::Appraise)
|
||||||
target_item.total_skills.entry(SkillType::Bombs)
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
|
.or_insert(sen * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Bombs)
|
target_item
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Chemistry)
|
.entry(SkillType::Blades)
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Climb)
|
.or_insert(refl * 0.5);
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Climb)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.entry(SkillType::Blades)
|
||||||
target_item.total_skills.entry(SkillType::Clubs)
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
.or_insert(col * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Clubs)
|
target_item
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Craft)
|
.entry(SkillType::Bombs)
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.and_modify(|sk| *sk += brn * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Dodge)
|
.or_insert(brn * 0.5);
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Dodge)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.entry(SkillType::Bombs)
|
||||||
target_item.total_skills.entry(SkillType::Fish)
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.or_insert(col * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Fish)
|
target_item
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Fists)
|
.entry(SkillType::Chemistry)
|
||||||
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
.and_modify(|sk| *sk += brn)
|
||||||
target_item.total_skills.entry(SkillType::Fists)
|
.or_insert(brn);
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Focus)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.entry(SkillType::Climb)
|
||||||
target_item.total_skills.entry(SkillType::Focus)
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.or_insert(refl * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Fuck)
|
target_item
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Fuck)
|
.entry(SkillType::Climb)
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Hack)
|
.or_insert(end * 0.5);
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Locksmith)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
|
.entry(SkillType::Clubs)
|
||||||
target_item.total_skills.entry(SkillType::Locksmith)
|
.and_modify(|sk| *sk += brw * 0.5)
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.or_insert(brw * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Medic)
|
target_item
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Persuade)
|
.entry(SkillType::Clubs)
|
||||||
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Persuade)
|
.or_insert(refl * 0.5);
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Pilot)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5);
|
.entry(SkillType::Craft)
|
||||||
target_item.total_skills.entry(SkillType::Pilot)
|
.and_modify(|sk| *sk += brn)
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.or_insert(brn);
|
||||||
target_item.total_skills.entry(SkillType::Pistols)
|
target_item
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Pistols)
|
.entry(SkillType::Dodge)
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Quickdraw)
|
.or_insert(sen * 0.5);
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Quickdraw)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.entry(SkillType::Dodge)
|
||||||
target_item.total_skills.entry(SkillType::Repair)
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.or_insert(refl * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Rifles)
|
target_item
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Rifles)
|
.entry(SkillType::Fish)
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Scavenge)
|
.or_insert(end * 0.5);
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Scavenge)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.entry(SkillType::Fish)
|
||||||
target_item.total_skills.entry(SkillType::Science)
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.or_insert(col * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Sneak)
|
target_item
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Sneak)
|
.entry(SkillType::Fists)
|
||||||
.and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5);
|
.and_modify(|sk| *sk += brw * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Spears)
|
.or_insert(brw * 0.5);
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Spears)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5);
|
.entry(SkillType::Fists)
|
||||||
target_item.total_skills.entry(SkillType::Swim)
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
.and_modify(|sk| *sk += end).or_insert(brn);
|
.or_insert(end * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Teach)
|
target_item
|
||||||
.and_modify(|sk| *sk += brn).or_insert(brn);
|
.total_skills
|
||||||
target_item.total_skills.entry(SkillType::Throw)
|
.entry(SkillType::Focus)
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
target_item.total_skills.entry(SkillType::Throw)
|
.or_insert(sen * 0.5);
|
||||||
.and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5);
|
target_item
|
||||||
target_item.total_skills.entry(SkillType::Track)
|
.total_skills
|
||||||
.and_modify(|sk| *sk += sen).or_insert(brn);
|
.entry(SkillType::Focus)
|
||||||
target_item.total_skills.entry(SkillType::Whips)
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
.and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5);
|
.or_insert(end * 0.5);
|
||||||
target_item.total_skills.entry(SkillType::Whips)
|
target_item
|
||||||
.and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5);
|
.total_skills
|
||||||
|
.entry(SkillType::Fuck)
|
||||||
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
|
.or_insert(sen * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Fuck)
|
||||||
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
|
.or_insert(end * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Hack)
|
||||||
|
.and_modify(|sk| *sk += brn)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Locksmith)
|
||||||
|
.and_modify(|sk| *sk += brn * 0.5)
|
||||||
|
.or_insert(brn * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Locksmith)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Medic)
|
||||||
|
.and_modify(|sk| *sk += brn)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Persuade)
|
||||||
|
.and_modify(|sk| *sk += brn * 0.5)
|
||||||
|
.or_insert(brn * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Persuade)
|
||||||
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
|
.or_insert(col * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Pilot)
|
||||||
|
.and_modify(|sk| *sk += brn * 0.5)
|
||||||
|
.or_insert(brn * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Pilot)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Pistols)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Pistols)
|
||||||
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
|
.or_insert(col * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Quickdraw)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Quickdraw)
|
||||||
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
|
.or_insert(col * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Repair)
|
||||||
|
.and_modify(|sk| *sk += brn)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Rifles)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Rifles)
|
||||||
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
|
.or_insert(col * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Scavenge)
|
||||||
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
|
.or_insert(sen * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Scavenge)
|
||||||
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
|
.or_insert(end * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Science)
|
||||||
|
.and_modify(|sk| *sk += brn)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Sneak)
|
||||||
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
|
.or_insert(sen * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Sneak)
|
||||||
|
.and_modify(|sk| *sk += col * 0.5)
|
||||||
|
.or_insert(col * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Spears)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Spears)
|
||||||
|
.and_modify(|sk| *sk += end * 0.5)
|
||||||
|
.or_insert(end * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Swim)
|
||||||
|
.and_modify(|sk| *sk += end)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Teach)
|
||||||
|
.and_modify(|sk| *sk += brn)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Throw)
|
||||||
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
|
.or_insert(sen * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Throw)
|
||||||
|
.and_modify(|sk| *sk += brw * 0.5)
|
||||||
|
.or_insert(brw * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Track)
|
||||||
|
.and_modify(|sk| *sk += sen)
|
||||||
|
.or_insert(brn);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Whips)
|
||||||
|
.and_modify(|sk| *sk += sen * 0.5)
|
||||||
|
.or_insert(sen * 0.5);
|
||||||
|
target_item
|
||||||
|
.total_skills
|
||||||
|
.entry(SkillType::Whips)
|
||||||
|
.and_modify(|sk| *sk += refl * 0.5)
|
||||||
|
.or_insert(refl * 0.5);
|
||||||
// 5: Apply skill (de)buffs...
|
// 5: Apply skill (de)buffs...
|
||||||
for buff in &target_item.temporary_buffs {
|
for buff in &target_item.temporary_buffs {
|
||||||
for impact in &buff.impacts {
|
for impact in &buff.impacts {
|
||||||
match impact {
|
match impact {
|
||||||
BuffImpact::ChangeSkill { skill, magnitude } => {
|
BuffImpact::ChangeSkill { skill, magnitude } => {
|
||||||
target_item.total_skills.entry(skill.clone())
|
target_item
|
||||||
.and_modify(|old_value| *old_value = (*old_value + magnitude.clone() as f64).max(0.0))
|
.total_skills
|
||||||
|
.entry(skill.clone())
|
||||||
|
.and_modify(|old_value| {
|
||||||
|
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||||
|
})
|
||||||
.or_insert((*magnitude).max(0.0));
|
.or_insert((*magnitude).max(0.0));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -188,27 +375,47 @@ pub fn skill_check_only(who: &Item, skill: &SkillType, diff_level: f64) -> f64 {
|
|||||||
|
|
||||||
// Note: Caller must save who because skills might update.
|
// Note: Caller must save who because skills might update.
|
||||||
// Don't return error if skillcheck fails, it can fail but still grind.
|
// Don't return error if skillcheck fails, it can fail but still grind.
|
||||||
pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &SkillType, diff_level: f64) -> DResult<f64> {
|
pub async fn skill_check_and_grind(
|
||||||
|
trans: &DBTrans,
|
||||||
|
who: &mut Item,
|
||||||
|
skill: &SkillType,
|
||||||
|
diff_level: f64,
|
||||||
|
) -> DResult<f64> {
|
||||||
let gap = calc_level_gap(who, skill, diff_level);
|
let gap = calc_level_gap(who, skill, diff_level);
|
||||||
let result = skill_check_fn(gap);
|
let result = skill_check_fn(gap);
|
||||||
|
|
||||||
// If the skill gap is 0, probability of learning is 0.5
|
// If the skill gap is 0, probability of learning is 0.5
|
||||||
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
||||||
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
||||||
if who.item_type == "player" && rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp() {
|
if who.item_type == "player"
|
||||||
|
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp()
|
||||||
|
{
|
||||||
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
||||||
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
||||||
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0 ||
|
if *user.raw_skills.get(skill).unwrap_or(&0.0) >= 15.0
|
||||||
!user.last_skill_improve.get(skill)
|
|| !user
|
||||||
.map(|t| (Utc::now() - *t).num_seconds() > 60).unwrap_or(true) {
|
.last_skill_improve
|
||||||
return Ok(result)
|
.get(skill)
|
||||||
|
.map(|t| (Utc::now() - *t).num_seconds() > 60)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
user.raw_skills.entry(skill.clone()).and_modify(|raw| *raw += 0.01).or_insert(0.01);
|
user.raw_skills
|
||||||
|
.entry(skill.clone())
|
||||||
|
.and_modify(|raw| *raw += 0.01)
|
||||||
|
.or_insert(0.01);
|
||||||
user.last_skill_improve.insert(skill.clone(), Utc::now());
|
user.last_skill_improve.insert(skill.clone(), Utc::now());
|
||||||
trans.queue_for_session(&sess,
|
trans
|
||||||
Some(&format!("Your raw {} is now {:.2}\n",
|
.queue_for_session(
|
||||||
skill.display(), user.raw_skills
|
&sess,
|
||||||
.get(skill).unwrap_or(&0.0)))).await?;
|
Some(&format!(
|
||||||
|
"Your raw {} is now {:.2}\n",
|
||||||
|
skill.display(),
|
||||||
|
user.raw_skills.get(skill).unwrap_or(&0.0)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
trans.save_user_model(&user).await?;
|
trans.save_user_model(&user).await?;
|
||||||
calculate_total_stats_skills_for_user(who, &user);
|
calculate_total_stats_skills_for_user(who, &user);
|
||||||
}
|
}
|
||||||
@ -217,3 +424,45 @@ pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &Skil
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SkillFailDamage {
|
||||||
|
pub damage_type: DamageType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DamageDistribution for SkillFailDamage {
|
||||||
|
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)> {
|
||||||
|
vec![(&self.damage_type, total)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn crit_fail_penalty_for_skill(
|
||||||
|
trans: &DBTrans,
|
||||||
|
who: &mut Item,
|
||||||
|
skill: &SkillType,
|
||||||
|
) -> DResult<()> {
|
||||||
|
use SkillType::*;
|
||||||
|
let (msg, part, dist) = match skill {
|
||||||
|
Bombs | Chemistry => (
|
||||||
|
"Ow! You burn your hand.",
|
||||||
|
BodyPart::Hands,
|
||||||
|
SkillFailDamage {
|
||||||
|
damage_type: DamageType::Shock,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Craft | Repair => (
|
||||||
|
"Ow! You catch your hand on something sharp.",
|
||||||
|
BodyPart::Hands,
|
||||||
|
SkillFailDamage {
|
||||||
|
damage_type: DamageType::Slash,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
let actual_damage_presoak = Normal::<f64>::new(5.0, 1.0)?
|
||||||
|
.sample(&mut rand::thread_rng())
|
||||||
|
.floor()
|
||||||
|
.max(1.0);
|
||||||
|
let final_damage = soak_damage(trans, &dist, who, actual_damage_presoak, &part).await?;
|
||||||
|
change_health(trans, -final_damage as i64, who, &msg, &msg).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ use async_trait::async_trait;
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
mod blade;
|
mod blade;
|
||||||
mod corp_licence;
|
mod corp_licence;
|
||||||
@ -56,6 +56,10 @@ impl DamageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait DamageDistribution {
|
||||||
|
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&DamageType, f64)>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WeaponAttackData {
|
pub struct WeaponAttackData {
|
||||||
pub start_messages: AttackMessageChoice,
|
pub start_messages: AttackMessageChoice,
|
||||||
pub success_messages: AttackMessageChoicePart,
|
pub success_messages: AttackMessageChoicePart,
|
||||||
@ -94,6 +98,21 @@ impl Default for WeaponAttackData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DamageDistribution for WeaponAttackData {
|
||||||
|
fn distribute_damage<'l>(self: &'l Self, total: f64) -> Vec<(&'l DamageType, f64)> {
|
||||||
|
let mut damage_by_type: Vec<(&DamageType, f64)> = self
|
||||||
|
.other_damage_types
|
||||||
|
.iter()
|
||||||
|
.map(|(frac, dtype)| (dtype, frac * total))
|
||||||
|
.collect();
|
||||||
|
damage_by_type.push((
|
||||||
|
&self.base_damage_type,
|
||||||
|
total - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
|
||||||
|
));
|
||||||
|
damage_by_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WeaponData {
|
pub struct WeaponData {
|
||||||
pub uses_skill: SkillType,
|
pub uses_skill: SkillType,
|
||||||
pub raw_min_to_learn: f64,
|
pub raw_min_to_learn: f64,
|
||||||
@ -298,6 +317,7 @@ pub enum PossessionType {
|
|||||||
LeatherPants,
|
LeatherPants,
|
||||||
// Weapons: Whips
|
// Weapons: Whips
|
||||||
AntennaWhip,
|
AntennaWhip,
|
||||||
|
LeatherWhip,
|
||||||
// Weapons: Blades
|
// Weapons: Blades
|
||||||
ButcherKnife,
|
ButcherKnife,
|
||||||
// Medical
|
// Medical
|
||||||
@ -410,6 +430,24 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn possession_type_names() -> &'static BTreeMap<String, Vec<PossessionType>> {
|
||||||
|
static POSSESSION_NAMES: OnceCell<BTreeMap<String, Vec<PossessionType>>> = OnceCell::new();
|
||||||
|
&POSSESSION_NAMES.get_or_init(|| {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
for (pt, pd) in possession_data() {
|
||||||
|
map.entry(pd.display.to_lowercase())
|
||||||
|
.and_modify(|l: &mut Vec<PossessionType>| l.push(pt.clone()))
|
||||||
|
.or_insert_with(|| vec![pt.clone()]);
|
||||||
|
for alias in &pd.aliases {
|
||||||
|
map.entry(alias.to_lowercase())
|
||||||
|
.and_modify(|l| l.push(pt.clone()))
|
||||||
|
.or_insert_with(|| vec![pt.clone()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
||||||
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
|
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
|
||||||
&RELEVANT.get_or_init(|| {
|
&RELEVANT.get_or_init(|| {
|
||||||
@ -426,8 +464,64 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CraftData {
|
||||||
|
pub skill: SkillType,
|
||||||
|
pub difficulty: f64,
|
||||||
|
pub inputs: Vec<PossessionType>,
|
||||||
|
pub output: PossessionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn improv_table() -> &'static Vec<CraftData> {
|
||||||
|
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
|
||||||
|
IMPROV_CELL.get_or_init(|| {
|
||||||
|
vec![CraftData {
|
||||||
|
skill: SkillType::Craft,
|
||||||
|
difficulty: 6.0,
|
||||||
|
inputs: vec![
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
PossessionType::AnimalSkin,
|
||||||
|
],
|
||||||
|
output: PossessionType::LeatherWhip,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn improv_by_ingredient() -> &'static BTreeMap<PossessionType, Vec<&'static CraftData>> {
|
||||||
|
static MAP_CELL: OnceCell<BTreeMap<PossessionType, Vec<&'static CraftData>>> = OnceCell::new();
|
||||||
|
MAP_CELL.get_or_init(|| {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
for cd in improv_table() {
|
||||||
|
let inps: BTreeSet<&PossessionType> = cd.inputs.iter().collect();
|
||||||
|
for inp in inps {
|
||||||
|
map.entry(inp.clone())
|
||||||
|
.and_modify(|l: &mut Vec<&'static CraftData>| l.push(cd))
|
||||||
|
.or_insert(vec![cd]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn improv_by_output() -> &'static BTreeMap<PossessionType, &'static CraftData> {
|
||||||
|
static MAP_CELL: OnceCell<BTreeMap<PossessionType, &'static CraftData>> = OnceCell::new();
|
||||||
|
MAP_CELL.get_or_init(|| {
|
||||||
|
improv_table()
|
||||||
|
.iter()
|
||||||
|
.map(|cd| (cd.output.clone(), cd))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn other_damage_types_add_to_less_than_one() {
|
fn other_damage_types_add_to_less_than_one() {
|
||||||
@ -444,4 +538,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn possession_type_names_works() {
|
||||||
|
assert!(possession_type_names()
|
||||||
|
.get("whip")
|
||||||
|
.unwrap()
|
||||||
|
.contains(&PossessionType::AntennaWhip));
|
||||||
|
assert!(possession_type_names()
|
||||||
|
.get("animal skin")
|
||||||
|
.unwrap()
|
||||||
|
.contains(&PossessionType::AnimalSkin));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_one_way_to_improv_each_item() {
|
||||||
|
assert_eq!(
|
||||||
|
improv_table()
|
||||||
|
.iter()
|
||||||
|
.group_by(|cd| cd.output.clone())
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|g| if g.1.count() == 1 { None } else { Some(g.0) })
|
||||||
|
.collect::<Vec<PossessionType>>(),
|
||||||
|
Vec::new()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_improv_item_has_possession_data_for_output() {
|
||||||
|
assert_eq!(
|
||||||
|
improv_table().iter().filter_map(|cd| if possession_data().get(&cd.output).is_none() { Some(cd) } else None ).collect(),
|
||||||
|
Vec::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
|
use super::{DamageType, PossessionData, PossessionType, WeaponAttackData, WeaponData};
|
||||||
use crate::models::item::SkillType;
|
use crate::models::item::SkillType;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
@ -42,6 +42,45 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
|||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
(PossessionType::LeatherWhip,
|
||||||
|
PossessionData {
|
||||||
|
display: "leather whip",
|
||||||
|
details: "A whip made from stitched together animal skins... it looks like a formidable weapon, and in the right hands will make someone look like Indiana Jones!",
|
||||||
|
aliases: vec!("whip"),
|
||||||
|
weapon_data: Some(WeaponData {
|
||||||
|
uses_skill: SkillType::Whips,
|
||||||
|
raw_min_to_learn: 0.0,
|
||||||
|
raw_max_to_learn: 2.0,
|
||||||
|
normal_attack: WeaponAttackData {
|
||||||
|
start_messages: vec!(
|
||||||
|
Box::new(|attacker, victim, exp|
|
||||||
|
format!("{} lines up {} leather whip for a strike on {}",
|
||||||
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
|
&attacker.pronouns.possessive,
|
||||||
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
success_messages: vec!(
|
||||||
|
Box::new(|attacker, victim, part, exp|
|
||||||
|
format!("{}'s leather whip scores a painful red line across {}'s {}",
|
||||||
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
|
&part.display(victim.sex.clone())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mean_damage: 4.0,
|
||||||
|
stdev_damage: 2.0,
|
||||||
|
base_damage_type: DamageType::Beat,
|
||||||
|
other_damage_types: vec!((0.25, DamageType::Slash)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user