Implement soaks

Also fix some minor combat bugs found, including the refocusing bug,
and the dead NPCs talking (mk II) bug.
This commit is contained in:
Condorra 2023-05-25 22:51:52 +10:00
parent 2747dddd90
commit c78dc64a7f
9 changed files with 440 additions and 179 deletions

View File

@ -11,12 +11,12 @@ use crate::{
journal::JournalType,
},
static_content::{
possession_type::{WeaponData, possession_data, fist},
possession_type::{WeaponData, WeaponAttackData, DamageType, possession_data, fist},
npc::npc_by_code,
species::species_info_map,
journals::{check_journal_for_kill, award_journal_if_needed}
},
message_handler::user_commands::{user_error, UResult},
message_handler::user_commands::{user_error, UResult, CommandHandlingError},
regular_tasks::{TaskRunContext, TaskHandler},
DResult,
};
@ -27,9 +27,167 @@ use chrono::Utc;
use async_recursion::async_recursion;
use std::time;
use ansi::ansi;
use rand::prelude::IteratorRandom;
use rand::{prelude::IteratorRandom, Rng};
use rand_distr::{Normal, Distribution};
async fn soak_damage(ctx: &mut TaskRunContext<'_>, attack: &WeaponAttackData, victim: &Item, presoak_amount: f64) -> 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 mut clothes: Vec<Item> =
ctx.trans.find_by_action_and_location(&victim.refstr(),
&LocationActionType::Worn).await?
.iter().map(|cl| (*cl.as_ref()).clone()).collect();
clothes.sort_unstable_by(|c1, c2| c2.action_type_started.cmp(&c1.action_type_started));
let mut total_damage = 0.0;
for (damage_type, mut damage_amount) in &damage_by_type {
for mut clothing in &mut clothes {
if let Some(soak) = clothing.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.wear_data.as_ref())
.and_then(|wd| wd.soaks.get(&damage_type)) {
if damage_amount <= 0.0 {
break;
}
let soak_amount: f64 = ((soak.max_soak - soak.min_soak) *
rand::thread_rng().gen::<f64>()).min(damage_amount);
damage_amount -= soak_amount;
let clothes_damage = ((0..(soak_amount as i64))
.filter(|_| rand::thread_rng().gen::<f64>() <
soak.damage_probability_per_soak).count()
as u64).min(clothing.health);
if clothes_damage > 0 {
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?
{
ctx.trans.queue_for_session(
&vic_sess,
Some(&format!("A few bits and pieces fly off your {}.\n",
clothing.display_for_session(&sess_dat)))
).await?;
}
}
}
}
}
total_damage += damage_amount;
}
for clothing in &clothes {
if clothing.health <= 0 {
ctx.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?
{
ctx.trans.queue_for_session(
&vic_sess,
Some(&format!("Your {} is completely destroyed; it crumbles away to nothing.\n",
clothing.display_for_session(&sess_dat)))
).await?;
}
}
}
}
Ok(total_damage)
}
async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, victim_item: &mut Item,
attack: &WeaponAttackData, weapon: &WeaponData) -> DResult<bool> {
let attack_skill = *attacker_item.total_skills.get(&weapon.uses_skill).unwrap_or(&0.0);
let victim_dodge_skill = *victim_item.total_skills.get(&SkillType::Dodge).unwrap_or(&0.0);
let dodge_result = skill_check_and_grind(ctx.trans, victim_item, &SkillType::Dodge,
attack_skill).await?;
let user_opt = if attacker_item.item_type == "player" {
ctx.trans.find_by_username(&attacker_item.item_code).await?
} else { None };
let attack_result = if let Some(user) = user_opt {
let raw_skill = *user.raw_skills.get(&weapon.uses_skill).unwrap_or(&0.0);
if raw_skill >= weapon.raw_min_to_learn && raw_skill <= weapon.raw_max_to_learn {
skill_check_and_grind(ctx.trans, attacker_item, &weapon.uses_skill,
victim_dodge_skill).await?
} else {
skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill)
}
} else {
skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill)
};
if dodge_result > attack_result {
let msg_exp = format!("{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(true, 1, true),
attacker_item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(false, 1, true),
attacker_item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
ctx.trans.save_item_model(&attacker_item).await?;
ctx.trans.save_item_model(&victim_item).await?;
} else {
// TODO: Parry system of some kind?
// Determine body part...
let part = victim_item.species.sample_body_part();
// TODO: Armour / soaks
let mut mean_damage: f64 = attack.mean_damage;
for scaling in attack.skill_scaling.iter() {
let skill = *attacker_item.total_skills.get(&scaling.skill).unwrap_or(&0.0);
if skill >= scaling.min_skill {
mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min;
}
}
let actual_damage_presoak = Normal::new(mean_damage,
attack.stdev_damage)?
.sample(&mut rand::thread_rng()).floor().max(1.0) as i64;
ctx.trans.save_item_model(&attacker_item).await?;
let actual_damage = soak_damage(ctx, &attack, victim_item, actual_damage_presoak as f64).await? as i64;
let msg_exp = attack.success_message(&attacker_item, victim_item,
&part, true);
let msg_nonexp = attack.success_message(&attacker_item, victim_item,
&part, false);
if actual_damage == 0 {
let msg_exp = format!(
"{}'s attack bounces off {}'s {}.\n",
&attacker_item.display_for_sentence(true, 1, true),
&victim_item.display_for_sentence(true, 1, false),
&part.display(victim_item.sex.clone())
);
let msg_nonexp =
format!(
"{}'s attack bounces off {}'s {}.\n",
attacker_item.display_for_sentence(false, 1, true),
victim_item.display_for_sentence(false, 1, false),
&part.display(None)
);
broadcast_to_room(&ctx.trans, &victim_item.location,
None, &msg_exp, Some(&msg_nonexp)).await?;
} else if change_health(ctx.trans, -actual_damage, victim_item,
&msg_exp, &msg_nonexp).await? {
ctx.trans.save_item_model(victim_item).await?;
return Ok(true);
}
ctx.trans.save_item_model(victim_item).await?;
}
let msg_exp = &(attack.start_message(&attacker_item, victim_item, true) + ".\n");
let msg_nonexp = &(attack.start_message(&attacker_item, victim_item, false) + ".\n");
broadcast_to_room(ctx.trans, &attacker_item.location, None, msg_exp, Some(msg_nonexp)).await?;
Ok(false)
}
#[derive(Clone)]
pub struct AttackTaskHandler;
#[async_trait]
@ -57,72 +215,12 @@ impl TaskHandler for AttackTaskHandler {
}
let weapon = what_wielded(ctx.trans, &attacker_item).await?;
let attack_skill = *attacker_item.total_skills.get(&weapon.uses_skill).unwrap_or(&0.0);
let victim_dodge_skill = *victim_item.total_skills.get(&SkillType::Dodge).unwrap_or(&0.0);
let dodge_result = skill_check_and_grind(ctx.trans, &mut victim_item, &SkillType::Dodge,
attack_skill).await?;
let user_opt = if ctype == "player" { ctx.trans.find_by_username(ccode).await? } else { None };
let attack_result = if let Some(user) = user_opt {
let raw_skill = *user.raw_skills.get(&weapon.uses_skill).unwrap_or(&0.0);
if raw_skill >= weapon.raw_min_to_learn && raw_skill <= weapon.raw_max_to_learn {
skill_check_and_grind(ctx.trans, &mut attacker_item, &weapon.uses_skill,
victim_dodge_skill).await?
} else {
skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill)
}
if process_attack(ctx, &mut attacker_item, &mut victim_item, &weapon.normal_attack, &weapon).await? {
Ok(None)
} else {
skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill)
};
if dodge_result > attack_result {
let msg_exp = format!("{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(true, 1, true),
attacker_item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(false, 1, true),
attacker_item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
ctx.trans.save_item_model(&attacker_item).await?;
ctx.trans.save_item_model(&victim_item).await?;
} else {
// TODO: Parry system of some kind?
// Determine body part...
let part = victim_item.species.sample_body_part();
// TODO: Armour / soaks
// TODO: Calculate damage etc... and display health impact.
let mut mean_damage: f64 = weapon.normal_attack_mean_damage;
for scaling in weapon.normal_attack_skill_scaling.iter() {
let skill = *attacker_item.total_skills.get(&scaling.skill).unwrap_or(&0.0);
if skill >= scaling.min_skill {
mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min;
}
}
let actual_damage = Normal::new(mean_damage,
weapon.normal_attack_stdev_damage)?
.sample(&mut rand::thread_rng()).floor().max(1.0) as i64;
ctx.trans.save_item_model(&attacker_item).await?;
let msg_exp = weapon.normal_attack_success_message(&attacker_item, &victim_item,
&part, true);
let msg_nonexp = weapon.normal_attack_success_message(&attacker_item, &victim_item,
&part, false);
if change_health(ctx.trans, -actual_damage, &mut victim_item,
&msg_exp, &msg_nonexp).await? {
ctx.trans.save_item_model(&victim_item).await?;
return Ok(None);
}
ctx.trans.save_item_model(&victim_item).await?;
Ok(Some(attack_speed(&attacker_item)))
}
let msg_exp = &(weapon.normal_attack_start_message(&attacker_item, &victim_item, true) + ".\n");
let msg_nonexp = &(weapon.normal_attack_start_message(&attacker_item, &victim_item, false) + ".\n");
broadcast_to_room(ctx.trans, &attacker_item.location, None, msg_exp, Some(msg_nonexp)).await?;
Ok(Some(attack_speed(&attacker_item)))
}
}
@ -332,14 +430,20 @@ pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_
if let Some((vtype, vcode)) = new_vic.split_once("/") {
if let Some(vic_item) = trans.find_item_by_type_code(vtype, vcode).await? {
let mut new_vic_item = (*vic_item).clone();
start_attack_mut(trans, new_by_whom, &mut new_vic_item);
match start_attack_mut(trans, new_by_whom, &mut new_vic_item).await {
Err(CommandHandlingError::UserError(_)) | Ok(()) => {},
Err(CommandHandlingError::SystemError(e)) => return Err(e),
}
trans.save_item_model(&new_vic_item).await?;
}
}
} else {
new_by_whom.action_type = LocationActionType::Normal;
}
} else {
new_by_whom.action_type = LocationActionType::Normal;
}
}
new_by_whom.action_type = LocationActionType::Normal;
Ok(())
}
@ -430,8 +534,8 @@ pub async fn start_attack_mut(trans: &DBTrans, by_whom: &mut Item, to_whom: &mut
);
let wielded = what_wielded(trans, by_whom).await?;
msg_exp.push_str(&(wielded.normal_attack_start_message(by_whom, to_whom, true) + ".\n"));
msg_nonexp.push_str(&(wielded.normal_attack_start_message(by_whom, to_whom, false) + ".\n"));
msg_exp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, true) + ".\n"));
msg_nonexp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, false) + ".\n"));
broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?;

View File

@ -270,7 +270,7 @@ impl TaskHandler for NPCSayTaskHandler {
Some(r) => r
};
if npc_item.death_data.is_none() {
if npc_item.death_data.is_some() {
return Ok(None);
}

View File

@ -33,15 +33,56 @@ pub struct SkillScaling {
pub mean_damage_per_point_over_min: f64
}
#[allow(unused)]
#[derive(Eq,Ord,Clone,PartialEq,PartialOrd)]
pub enum DamageType {
Beat,
Slash,
Pierce,
Shock,
Bullet
}
pub struct WeaponAttackData {
pub start_messages: AttackMessageChoice,
pub success_messages: AttackMessageChoicePart,
pub mean_damage: f64,
pub stdev_damage: f64,
pub base_damage_type: DamageType,
pub other_damage_types: Vec<(f64, DamageType)>, // Allocation fractions.
pub skill_scaling: Vec<SkillScaling>,
}
impl Default for WeaponAttackData {
fn default() -> Self {
Self {
start_messages:
vec!(Box::new(|attacker, victim, exp| format!(
"{} makes an attack on {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp,1, false)))),
success_messages:
vec!(Box::new(|attacker, victim, part, exp|
format!("{}'s attack on {} hits {} {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&victim.pronouns.possessive,
part.display(victim.sex.clone())
))),
mean_damage: 1.0,
stdev_damage: 2.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!(),
skill_scaling: vec!()
}
}
}
pub struct WeaponData {
pub uses_skill: SkillType,
pub raw_min_to_learn: f64,
pub raw_max_to_learn: f64,
pub normal_attack_start_messages: AttackMessageChoice,
pub normal_attack_success_messages: AttackMessageChoicePart,
pub normal_attack_mean_damage: f64,
pub normal_attack_stdev_damage: f64,
pub normal_attack_skill_scaling: Vec<SkillScaling>,
pub normal_attack: WeaponAttackData,
}
impl Default for WeaponData {
@ -50,22 +91,7 @@ impl Default for WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 0.0,
raw_max_to_learn: 15.0,
normal_attack_start_messages:
vec!(Box::new(|attacker, victim, exp| format!(
"{} makes an attack on {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp,1, false)))),
normal_attack_success_messages:
vec!(Box::new(|attacker, victim, part, exp|
format!("{}'s attack on {} hits {} {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&victim.pronouns.possessive,
part.display(victim.sex.clone())
))),
normal_attack_mean_damage: 1.0,
normal_attack_stdev_damage: 2.0,
normal_attack_skill_scaling: vec!()
normal_attack: Default::default(),
}
}
}
@ -120,9 +146,17 @@ impl Default for UseData {
}
}
pub struct SoakData {
pub min_soak: f64,
pub max_soak: f64,
pub damage_probability_per_soak: f64,
}
pub struct WearData {
pub covers_parts: Vec<BodyPart>,
pub thickness: f64,
pub dodge_penalty: f64,
pub soaks: BTreeMap<DamageType, SoakData>,
}
#[async_trait]
@ -185,22 +219,22 @@ impl Default for PossessionData {
}
}
impl WeaponData {
pub fn normal_attack_start_message(
impl WeaponAttackData {
pub fn start_message(
&self,
attacker: &Item, victim: &Item, explicit_ok: bool) -> String {
let mut rng = rand::thread_rng();
self.normal_attack_start_messages[..].choose(&mut rng).map(
self.start_messages[..].choose(&mut rng).map(
|f| f(attacker, victim, explicit_ok)).unwrap_or(
"No message defined yet".to_owned())
}
pub fn normal_attack_success_message(
pub fn success_message(
&self, attacker: &Item, victim: &Item,
part: &BodyPart, explicit_ok: bool
) -> String {
let mut rng = rand::thread_rng();
self.normal_attack_success_messages[..].choose(&mut rng).map(
self.success_messages[..].choose(&mut rng).map(
|f| f(attacker, victim, part, explicit_ok)).unwrap_or(
"No message defined yet".to_owned())
}
@ -268,24 +302,27 @@ pub fn fist() -> &'static WeaponData {
uses_skill: SkillType::Fists,
raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0,
normal_attack_start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} swings at {} with {} fists",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&attacker.pronouns.possessive
)
)
),
normal_attack_success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s fists smash into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} swings at {} with {} fists",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&attacker.pronouns.possessive
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s fists smash into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
..Default::default()
},
..Default::default()
}
})
@ -319,3 +356,18 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn other_damage_types_add_to_less_than_one() {
for (_pt, pd) in possession_data().iter() {
if let Some(weapon_data) = pd.weapon_data.as_ref() {
let tot: f64 = weapon_data.normal_attack.other_damage_types.iter().map(|v|v.0).sum();
assert!(tot >= 0.0);
assert!(tot < 1.0);
}
}
}
}

View File

@ -1,4 +1,4 @@
use super::{PossessionData, WeaponData, PossessionType};
use super::{PossessionData, WeaponData, WeaponAttackData, PossessionType};
use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
@ -17,26 +17,29 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
uses_skill: SkillType::Blades,
raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0,
normal_attack_start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} raises {} butcher knife menancingly, preparing to attack {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
normal_attack_success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s butcher knife cuts into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
normal_attack_mean_damage: 2.0,
normal_attack_stdev_damage: 2.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} raises {} butcher knife menancingly, preparing to attack {}",
&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 butcher knife cuts into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 2.0,
stdev_damage: 2.0,
..Default::default()
},
..Default::default()
}),
..Default::default()

View File

@ -1,4 +1,4 @@
use super::{PossessionData, WeaponData};
use super::{PossessionData, WeaponData, WeaponAttackData};
use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
@ -11,24 +11,27 @@ pub fn data() -> &'static PossessionData {
uses_skill: SkillType::Fists,
raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0,
normal_attack_start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} bares {} teeth and lunges at {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
normal_attack_success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s teeth connect and tear at the flesh of {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} bares {} teeth and lunges at {}",
&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 teeth connect and tear at the flesh of {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
..Default::default()
},
..Default::default()
}),
..Default::default()

View File

@ -1,4 +1,4 @@
use super::{PossessionData, PossessionType, WearData};
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell;
@ -15,6 +15,30 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Head),
thickness: 4.0,
dodge_penalty: 0.25,
soaks: vec!(
(DamageType::Beat,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Slash,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Pierce,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
).into_iter().collect(),
}),
..Default::default()
}
@ -28,6 +52,30 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Face),
thickness: 4.0,
dodge_penalty: 0.25,
soaks: vec!(
(DamageType::Beat,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Slash,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Pierce,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
).into_iter().collect()
}),
..Default::default()
}

View File

@ -1,4 +1,4 @@
use super::{PossessionData, PossessionType, WearData};
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell;
@ -15,6 +15,30 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Groin, BodyPart::Legs),
thickness: 4.0,
dodge_penalty: 0.5,
soaks: vec!(
(DamageType::Beat,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Slash,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Pierce,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
).into_iter().collect(),
}),
..Default::default()
}

View File

@ -1,4 +1,4 @@
use super::{PossessionData, PossessionType, WearData};
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell;
@ -17,6 +17,30 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
BodyPart::Chest,
BodyPart::Back),
thickness: 4.0,
dodge_penalty: 0.5,
soaks: vec!(
(DamageType::Beat,
SoakData {
min_soak: 1.0,
max_soak: 2.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Slash,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
(DamageType::Pierce,
SoakData {
min_soak: 2.0,
max_soak: 3.0,
damage_probability_per_soak: 0.1
}
),
).into_iter().collect(),
}),
..Default::default()
}

View File

@ -1,4 +1,4 @@
use super::{PossessionData, PossessionType, WeaponData};
use super::{PossessionData, PossessionType, WeaponData, WeaponAttackData};
use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
@ -16,26 +16,29 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
uses_skill: SkillType::Whips,
raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0,
normal_attack_start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} lines up {} antenna whip for a strike on {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
normal_attack_success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s antenna 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())
)
)
),
normal_attack_mean_damage: 3.0,
normal_attack_stdev_damage: 3.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} lines up {} antenna 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 antenna 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: 3.0,
stdev_damage: 3.0,
..Default::default()
},
..Default::default()
}),
..Default::default()