Implement health and death.
This commit is contained in:
parent
09db1a6ed9
commit
165f5671ac
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -139,6 +139,7 @@ dependencies = [
|
||||
"ouroboros",
|
||||
"phf",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -815,6 +816,12 @@ version = "0.2.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.8"
|
||||
@ -977,6 +984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1246,6 +1254,16 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
|
@ -36,3 +36,4 @@ itertools = "0.10.5"
|
||||
once_cell = "1.16.0"
|
||||
rand = "0.8.5"
|
||||
async-recursion = "1.0.0"
|
||||
rand_distr = "0.4.3"
|
||||
|
@ -286,7 +286,7 @@ impl DBTrans {
|
||||
// be reset on restart.
|
||||
for to_copy in ["display", "display_less_explicit", "details", "details_less_explicit",
|
||||
"total_xp", "total_stats", "total_skills", "pronouns", "flags",
|
||||
"sex", "is_challenge_attack_only", "aliases"] {
|
||||
"sex", "is_challenge_attack_only", "aliases", "species"] {
|
||||
det_ex = format!("jsonb_set({}, '{{{}}}', ${})", det_ex, to_copy, var_id);
|
||||
params.push(obj_map.get(to_copy).unwrap_or(&Value::Null));
|
||||
var_id += 1;
|
||||
|
@ -2,198 +2,13 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error,
|
||||
get_player_item_or_fail, search_item_for_user};
|
||||
use async_trait::async_trait;
|
||||
use ansi::ansi;
|
||||
use std::time;
|
||||
use crate::{
|
||||
services::{broadcast_to_room, skills::skill_check_and_grind},
|
||||
db::{DBTrans, ItemSearchParams},
|
||||
models::{
|
||||
item::{Item, LocationActionType, Subattack, SkillType},
|
||||
task::{Task, TaskMeta, TaskDetails}
|
||||
services::{
|
||||
combat::start_attack,
|
||||
},
|
||||
static_content::{
|
||||
possession_type::{WeaponData, BodyPart, possession_data, fist},
|
||||
npc::npc_by_code,
|
||||
},
|
||||
regular_tasks::{TaskRunContext, TaskHandler},
|
||||
DResult
|
||||
|
||||
db::ItemSearchParams,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use chrono::Utc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AttackTaskHandler;
|
||||
#[async_trait]
|
||||
impl TaskHandler for AttackTaskHandler {
|
||||
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||
let (ctype, ccode) = ctx.task.meta.task_code.split_once("/")
|
||||
.ok_or("Invalid AttackTick task code")?;
|
||||
let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
|
||||
None => { return Ok(None); } // Player is gone
|
||||
Some(item) => (*item).clone()
|
||||
};
|
||||
|
||||
let (vtype, vcode) =
|
||||
match attacker_item.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()).and_then(|v| v.split_once("/")) {
|
||||
None => return Ok(None),
|
||||
Some(x) => x
|
||||
};
|
||||
let mut victim_item = match ctx.trans.find_item_by_type_code(vtype, vcode).await? {
|
||||
None => { return Ok(None); }
|
||||
Some(item) => (*item).clone()
|
||||
};
|
||||
|
||||
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 attack_result = skill_check_and_grind(ctx.trans, &mut attacker_item, &weapon.uses_skill,
|
||||
victim_dodge_skill).await?;
|
||||
|
||||
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?;
|
||||
} else {
|
||||
// TODO: Parry system of some kind?
|
||||
|
||||
// Determine body part...
|
||||
let part = BodyPart::sample();
|
||||
|
||||
// TODO: Armour / soaks
|
||||
|
||||
// TODO: Calculate damage etc... and display health impact.
|
||||
let msg_exp = weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, true) + ".\n";
|
||||
let msg_nonexp = weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, false) + ".\n";
|
||||
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||
}
|
||||
|
||||
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?;
|
||||
ctx.trans.save_item_model(&attacker_item).await?;
|
||||
ctx.trans.save_item_model(&victim_item).await?;
|
||||
Ok(Some(attack_speed(&attacker_item)))
|
||||
}
|
||||
}
|
||||
|
||||
pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler;
|
||||
|
||||
pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
|
||||
let mut new_to_whom = (*to_whom).clone();
|
||||
if let Some(ac) = new_to_whom.active_combat.as_mut() {
|
||||
let old_attacker = format!("{}/{}", by_whom.item_type, by_whom.item_code);
|
||||
ac.attacked_by.retain(|v| v != &old_attacker);
|
||||
trans.save_item_model(&new_to_whom).await?;
|
||||
}
|
||||
let mut new_by_whom = (*by_whom).clone();
|
||||
if let Some(ac) = new_by_whom.active_combat.as_mut() {
|
||||
ac.attacking = None;
|
||||
}
|
||||
new_by_whom.action_type = LocationActionType::Normal;
|
||||
trans.save_item_model(&new_by_whom).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn what_wielded(_trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> {
|
||||
// TODO: Search inventory for wielded item first.
|
||||
if who.item_type == "npc" {
|
||||
if let Some(intrinsic) = npc_by_code().get(who.item_code.as_str())
|
||||
.and_then(|npc| npc.intrinsic_weapon.as_ref()) {
|
||||
if let Some(weapon) = possession_data().get(intrinsic).and_then(|p| p.weapon_data.as_ref()) {
|
||||
return Ok(weapon)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fist())
|
||||
}
|
||||
|
||||
fn attack_speed(_who: &Item) -> time::Duration {
|
||||
time::Duration::from_secs(5)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
|
||||
let mut msg_exp = String::new();
|
||||
let mut msg_nonexp = String::new();
|
||||
let mut verb: String = "attacks".to_string();
|
||||
match by_whom.action_type {
|
||||
LocationActionType::Sitting | LocationActionType::Reclining => {
|
||||
msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display));
|
||||
msg_nonexp.push_str(&format!(ansi!("{} stands up.\n"),
|
||||
by_whom.display_less_explicit.as_ref().unwrap_or(&by_whom.display)));
|
||||
},
|
||||
LocationActionType::Attacking(_) => {
|
||||
match by_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref().and_then(|s| s.split_once("/"))) {
|
||||
Some((cur_type, cur_code)) if cur_type == to_whom.item_type && cur_code == to_whom.item_code =>
|
||||
user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?,
|
||||
Some((cur_type, cur_code)) => {
|
||||
if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? {
|
||||
stop_attacking(trans, by_whom, &cur_item_arc).await?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on";
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
msg_exp.push_str(&format!(
|
||||
ansi!("<red>{} {} {}.<reset>\n"),
|
||||
&by_whom.display_for_sentence(true, 1, true),
|
||||
verb,
|
||||
&to_whom.display_for_sentence(true, 1, false))
|
||||
);
|
||||
msg_nonexp.push_str(&format!(
|
||||
ansi!("<red>{} {} {}.<reset>\n"),
|
||||
&by_whom.display_for_sentence(false, 1, true),
|
||||
verb,
|
||||
&to_whom.display_for_sentence(false, 1, false))
|
||||
);
|
||||
|
||||
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"));
|
||||
|
||||
broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?;
|
||||
|
||||
let mut by_whom_for_update = by_whom.clone();
|
||||
by_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacking =
|
||||
Some(format!("{}/{}",
|
||||
&to_whom.item_type, &to_whom.item_code));
|
||||
by_whom_for_update.action_type = LocationActionType::Attacking(Subattack::Normal);
|
||||
let mut to_whom_for_update = to_whom.clone();
|
||||
to_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacked_by.push(
|
||||
format!("{}/{}",
|
||||
&by_whom.item_type, &by_whom.item_code)
|
||||
);
|
||||
|
||||
trans.upsert_task(&Task {
|
||||
meta: TaskMeta {
|
||||
task_code: format!("{}/{}", by_whom.item_type, by_whom.item_code),
|
||||
next_scheduled: Utc::now() + chrono::Duration::milliseconds(
|
||||
attack_speed(by_whom).as_millis() as i64),
|
||||
..Default::default()
|
||||
},
|
||||
details: TaskDetails::AttackTick
|
||||
}).await?;
|
||||
trans.save_item_model(&by_whom_for_update).await?;
|
||||
trans.save_item_model(&to_whom_for_update).await?;
|
||||
// Auto-counterattack if victim isn't busy.
|
||||
if to_whom_for_update.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()) == None {
|
||||
start_attack(trans, &to_whom_for_update, &by_whom_for_update).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Verb;
|
||||
#[async_trait]
|
||||
@ -208,7 +23,7 @@ impl UserVerb for Verb {
|
||||
match attack_whom.item_type.as_str() {
|
||||
"npc" => {}
|
||||
"player" => {},
|
||||
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
|
||||
_ => user_error("Only characters (players / NPCs) can be attacked".to_string())?
|
||||
}
|
||||
|
||||
if attack_whom.item_code == player_item.item_code && attack_whom.item_type == player_item.item_type {
|
||||
@ -219,6 +34,10 @@ impl UserVerb for Verb {
|
||||
// Add challenge check here.
|
||||
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an accepted challenge is very much functional. [Try <bold>help challenge<reset>]").to_string())?
|
||||
}
|
||||
|
||||
if attack_whom.is_dead {
|
||||
user_error("There's no point attacking the dead!".to_string())?
|
||||
}
|
||||
|
||||
start_attack(&ctx.trans, &player_item, &attack_whom).await
|
||||
}
|
||||
|
@ -99,8 +99,13 @@ async fn list_item_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
|
||||
match head.action_type {
|
||||
LocationActionType::Sitting => buf.push_str("sitting "),
|
||||
LocationActionType::Reclining => buf.push_str("reclining "),
|
||||
LocationActionType::Normal | LocationActionType::Attacking(_) if is_creature =>
|
||||
buf.push_str("standing "),
|
||||
LocationActionType::Normal | LocationActionType::Attacking(_) if is_creature => {
|
||||
if head.is_dead {
|
||||
buf.push_str("lying ");
|
||||
} else {
|
||||
buf.push_str("standing ");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
buf.push_str("here");
|
||||
|
@ -2,7 +2,6 @@ use super::{
|
||||
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
|
||||
get_player_item_or_fail,
|
||||
look,
|
||||
attack::stop_attacking,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
@ -22,7 +21,8 @@ use crate::{
|
||||
},
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
skills::skill_check_and_grind
|
||||
skills::skill_check_and_grind,
|
||||
combat::stop_attacking,
|
||||
}
|
||||
};
|
||||
use std::time;
|
||||
|
@ -1,6 +1,9 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::BTreeMap;
|
||||
use crate::language;
|
||||
use crate::{
|
||||
language,
|
||||
static_content::species::SpeciesType,
|
||||
};
|
||||
use super::session::Session;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
@ -286,7 +289,8 @@ pub struct Item {
|
||||
pub is_static: bool,
|
||||
pub is_dead: bool,
|
||||
pub is_challenge_attack_only: bool,
|
||||
|
||||
pub species: SpeciesType,
|
||||
pub health: u64,
|
||||
pub total_xp: u64,
|
||||
pub total_stats: BTreeMap<StatType, f64>,
|
||||
pub total_skills: BTreeMap<SkillType, f64>,
|
||||
@ -358,6 +362,8 @@ impl Default for Item {
|
||||
is_static: false,
|
||||
is_dead: false,
|
||||
is_challenge_attack_only: true,
|
||||
species: SpeciesType::Human,
|
||||
health: 40,
|
||||
total_xp: 0,
|
||||
total_stats: BTreeMap::new(),
|
||||
total_skills: BTreeMap::new(),
|
||||
|
@ -6,7 +6,7 @@ use crate::{
|
||||
models::task::{Task, TaskParse, TaskRecurrence},
|
||||
listener::{ListenerMap, ListenerSend},
|
||||
static_content::npc,
|
||||
message_handler::user_commands::attack,
|
||||
services::combat,
|
||||
};
|
||||
use blastmud_interfaces::MessageToListener;
|
||||
use log::warn;
|
||||
@ -34,7 +34,7 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
|
||||
|| vec!(
|
||||
("RunQueuedCommand", queued_command::HANDLER.clone()),
|
||||
("NPCSay", npc::SAY_HANDLER.clone()),
|
||||
("AttackTick", attack::TASK_HANDLER.clone())
|
||||
("AttackTick", combat::TASK_HANDLER.clone())
|
||||
).into_iter().collect()
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
db::DBTrans,
|
||||
DResult,
|
||||
models::item::Item
|
||||
models::item::Item,
|
||||
};
|
||||
|
||||
pub mod skills;
|
||||
pub mod combat;
|
||||
|
||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
||||
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
||||
|
291
blastmud_game/src/services/combat.rs
Normal file
291
blastmud_game/src/services/combat.rs
Normal file
@ -0,0 +1,291 @@
|
||||
use crate::{
|
||||
services::{
|
||||
broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
skills::skill_check_only,
|
||||
},
|
||||
models::{
|
||||
item::{Item, LocationActionType, Subattack, SkillType},
|
||||
task::{Task, TaskMeta, TaskDetails}
|
||||
},
|
||||
static_content::{
|
||||
possession_type::{WeaponData, possession_data, fist},
|
||||
npc::npc_by_code,
|
||||
},
|
||||
message_handler::user_commands::{user_error, UResult},
|
||||
regular_tasks::{TaskRunContext, TaskHandler},
|
||||
DResult,
|
||||
db::DBTrans,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use async_recursion::async_recursion;
|
||||
use std::time;
|
||||
use ansi::ansi;
|
||||
use rand_distr::{Normal, Distribution};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AttackTaskHandler;
|
||||
#[async_trait]
|
||||
impl TaskHandler for AttackTaskHandler {
|
||||
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||
let (ctype, ccode) = ctx.task.meta.task_code.split_once("/")
|
||||
.ok_or("Invalid AttackTick task code")?;
|
||||
let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
|
||||
None => { return Ok(None); } // Player is gone
|
||||
Some(item) => (*item).clone()
|
||||
};
|
||||
|
||||
let (vtype, vcode) =
|
||||
match attacker_item.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()).and_then(|v| v.split_once("/")) {
|
||||
None => return Ok(None),
|
||||
Some(x) => x
|
||||
};
|
||||
let mut victim_item = match ctx.trans.find_item_by_type_code(vtype, vcode).await? {
|
||||
None => { return Ok(None); }
|
||||
Some(item) => (*item).clone()
|
||||
};
|
||||
|
||||
if attacker_item.is_dead || victim_item.is_dead {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} 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?;
|
||||
} 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 u64;
|
||||
let new_health = if actual_damage > victim_item.health { 0 } else { victim_item.health - actual_damage };
|
||||
let msg_exp = format!(ansi!("[ <red>{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
||||
actual_damage,
|
||||
new_health,
|
||||
max_health(&victim_item),
|
||||
weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, true));
|
||||
let msg_nonexp =
|
||||
format!(ansi!("[ <red>{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
||||
actual_damage,
|
||||
new_health,
|
||||
max_health(&victim_item),
|
||||
weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, false));
|
||||
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||
victim_item.health = new_health;
|
||||
if new_health == 0 {
|
||||
handle_death(ctx.trans, &mut victim_item).await?;
|
||||
ctx.trans.save_item_model(&attacker_item).await?;
|
||||
ctx.trans.save_item_model(&victim_item).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
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?;
|
||||
ctx.trans.save_item_model(&attacker_item).await?;
|
||||
ctx.trans.save_item_model(&victim_item).await?;
|
||||
Ok(Some(attack_speed(&attacker_item)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
whom.is_dead = true;
|
||||
let msg_exp = format!(
|
||||
ansi!("<red>{} stiffens and collapses dead.<reset>\n"),
|
||||
&whom.display_for_sentence(true, 1, true)
|
||||
);
|
||||
let msg_nonexp = format!(
|
||||
ansi!("<red>{} stiffens and collapses dead.<reset>\n"),
|
||||
&whom.display_for_sentence(false, 1, true)
|
||||
);
|
||||
|
||||
if let Some(ac) = &whom.active_combat {
|
||||
let at_str = ac.attacking.clone();
|
||||
for attacker in ac.attacked_by.clone().iter() {
|
||||
if let Some((atype, acode)) = attacker.split_once("/") {
|
||||
if let Some(aitem) = trans.find_item_by_type_code(atype, acode).await? {
|
||||
let mut new_aitem = (*aitem).clone();
|
||||
stop_attacking_mut(trans, &mut new_aitem, whom).await?;
|
||||
trans.save_item_model(&new_aitem).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((vtype, vcode)) = at_str.as_ref().and_then(|a| a.split_once("/")) {
|
||||
if let Some(vitem) = trans.find_item_by_type_code(vtype, vcode).await? {
|
||||
let mut new_vitem = (*vitem).clone();
|
||||
stop_attacking_mut(trans, whom, &mut new_vitem).await?;
|
||||
trans.save_item_model(&new_vitem).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcast_to_room(trans, &whom.location, None, &msg_exp, Some(&msg_nonexp)).await
|
||||
}
|
||||
|
||||
pub fn max_health(_whom: &Item) -> u64 {
|
||||
24
|
||||
}
|
||||
|
||||
pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler;
|
||||
|
||||
pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_whom: &mut Item) ->
|
||||
DResult<()>
|
||||
{
|
||||
trans.delete_task("AttackTick",
|
||||
&format!("{}/{}",
|
||||
new_by_whom.item_type,
|
||||
new_by_whom.item_code)).await?;
|
||||
if let Some(ac) = new_to_whom.active_combat.as_mut() {
|
||||
let old_attacker = format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code);
|
||||
ac.attacked_by.retain(|v| v != &old_attacker);
|
||||
}
|
||||
if let Some(ac) = new_by_whom.active_combat.as_mut() {
|
||||
ac.attacking = None;
|
||||
}
|
||||
new_by_whom.action_type = LocationActionType::Normal;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> DResult<()> {
|
||||
let mut new_by_whom = (*by_whom).clone();
|
||||
let mut new_to_whom = (*to_whom).clone();
|
||||
stop_attacking_mut(trans, &mut new_by_whom, &mut new_to_whom).await?;
|
||||
trans.save_item_model(&new_by_whom).await?;
|
||||
trans.save_item_model(&new_to_whom).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn what_wielded(_trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> {
|
||||
// TODO: Search inventory for wielded item first.
|
||||
if who.item_type == "npc" {
|
||||
if let Some(intrinsic) = npc_by_code().get(who.item_code.as_str())
|
||||
.and_then(|npc| npc.intrinsic_weapon.as_ref()) {
|
||||
if let Some(weapon) = possession_data().get(intrinsic).and_then(|p| p.weapon_data.as_ref()) {
|
||||
return Ok(weapon)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fist())
|
||||
}
|
||||
|
||||
fn attack_speed(_who: &Item) -> time::Duration {
|
||||
time::Duration::from_secs(5)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
|
||||
let mut msg_exp = String::new();
|
||||
let mut msg_nonexp = String::new();
|
||||
let mut verb: String = "attacks".to_string();
|
||||
match by_whom.action_type {
|
||||
LocationActionType::Sitting | LocationActionType::Reclining => {
|
||||
msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display));
|
||||
msg_nonexp.push_str(&format!(ansi!("{} stands up.\n"),
|
||||
by_whom.display_less_explicit.as_ref().unwrap_or(&by_whom.display)));
|
||||
},
|
||||
LocationActionType::Attacking(_) => {
|
||||
match by_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref().and_then(|s| s.split_once("/"))) {
|
||||
Some((cur_type, cur_code)) if cur_type == to_whom.item_type && cur_code == to_whom.item_code =>
|
||||
user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?,
|
||||
Some((cur_type, cur_code)) => {
|
||||
if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? {
|
||||
stop_attacking(trans, by_whom, &cur_item_arc).await?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on";
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
msg_exp.push_str(&format!(
|
||||
ansi!("<red>{} {} {}.<reset>\n"),
|
||||
&by_whom.display_for_sentence(true, 1, true),
|
||||
verb,
|
||||
&to_whom.display_for_sentence(true, 1, false))
|
||||
);
|
||||
msg_nonexp.push_str(&format!(
|
||||
ansi!("<red>{} {} {}.<reset>\n"),
|
||||
&by_whom.display_for_sentence(false, 1, true),
|
||||
verb,
|
||||
&to_whom.display_for_sentence(false, 1, false))
|
||||
);
|
||||
|
||||
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"));
|
||||
|
||||
broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?;
|
||||
|
||||
let mut by_whom_for_update = by_whom.clone();
|
||||
by_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacking =
|
||||
Some(format!("{}/{}",
|
||||
&to_whom.item_type, &to_whom.item_code));
|
||||
by_whom_for_update.action_type = LocationActionType::Attacking(Subattack::Normal);
|
||||
let mut to_whom_for_update = to_whom.clone();
|
||||
to_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacked_by.push(
|
||||
format!("{}/{}",
|
||||
&by_whom.item_type, &by_whom.item_code)
|
||||
);
|
||||
|
||||
trans.upsert_task(&Task {
|
||||
meta: TaskMeta {
|
||||
task_code: format!("{}/{}", by_whom.item_type, by_whom.item_code),
|
||||
next_scheduled: Utc::now() + chrono::Duration::milliseconds(
|
||||
attack_speed(by_whom).as_millis() as i64),
|
||||
..Default::default()
|
||||
},
|
||||
details: TaskDetails::AttackTick
|
||||
}).await?;
|
||||
trans.save_item_model(&by_whom_for_update).await?;
|
||||
trans.save_item_model(&to_whom_for_update).await?;
|
||||
// Auto-counterattack if victim isn't busy.
|
||||
if to_whom_for_update.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()) == None {
|
||||
start_attack(trans, &to_whom_for_update, &by_whom_for_update).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -206,7 +206,7 @@ pub async fn skill_check_and_grind(trans: &DBTrans, who: &mut Item, skill: &Skil
|
||||
user.raw_skills.entry(skill.clone()).and_modify(|raw| *raw += 0.01).or_insert(0.01);
|
||||
|
||||
trans.queue_for_session(&sess,
|
||||
Some(&format!("Your raw {} is now {:2}\n",
|
||||
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?;
|
||||
|
@ -7,6 +7,7 @@ use log::info;
|
||||
pub mod room;
|
||||
pub mod npc;
|
||||
pub mod possession_type;
|
||||
pub mod species;
|
||||
mod fixed_item;
|
||||
|
||||
pub struct StaticItem {
|
||||
|
@ -1,4 +1,9 @@
|
||||
use super::{StaticItem, StaticTask, possession_type::PossessionType};
|
||||
use super::{
|
||||
StaticItem,
|
||||
StaticTask,
|
||||
possession_type::PossessionType,
|
||||
species::SpeciesType
|
||||
};
|
||||
use crate::models::{
|
||||
item::{Item, Pronouns, SkillType},
|
||||
task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
|
||||
@ -57,6 +62,7 @@ pub struct NPC {
|
||||
pub attackable: bool,
|
||||
pub intrinsic_weapon: Option<PossessionType>,
|
||||
pub total_skills: BTreeMap<SkillType, f64>,
|
||||
pub species: SpeciesType,
|
||||
}
|
||||
|
||||
impl Default for NPC {
|
||||
@ -72,7 +78,8 @@ impl Default for NPC {
|
||||
says: vec!(),
|
||||
total_skills: SkillType::values().into_iter().map(|sk| (sk, 8.0)).collect(),
|
||||
attackable: false,
|
||||
intrinsic_weapon: None
|
||||
intrinsic_weapon: None,
|
||||
species: SpeciesType::Human,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
use super::NPC;
|
||||
use crate::models::item::Pronouns;
|
||||
use crate::static_content::possession_type::PossessionType;
|
||||
use crate::static_content::{
|
||||
possession_type::PossessionType,
|
||||
species::SpeciesType
|
||||
};
|
||||
|
||||
macro_rules! dog {
|
||||
($code:expr, $adj:expr, $spawn: expr) => {
|
||||
@ -13,6 +16,7 @@ macro_rules! dog {
|
||||
aliases: vec!("dog"),
|
||||
spawn_location: concat!("room/", $spawn),
|
||||
intrinsic_weapon: Some(PossessionType::Fangs),
|
||||
species: SpeciesType::Dog,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +1,19 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::{
|
||||
models::item::{SkillType, Item, Sex}
|
||||
models::item::{SkillType, Item}
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::BTreeMap;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::seq::IteratorRandom;
|
||||
use super::species::BodyPart;
|
||||
|
||||
pub type AttackMessageChoice = Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
|
||||
pub type AttackMessageChoicePart = Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
|
||||
|
||||
#[derive(Eq, Ord, Clone, PartialEq, PartialOrd, Debug)]
|
||||
pub enum BodyPart {
|
||||
Head,
|
||||
Face,
|
||||
Chest,
|
||||
Back,
|
||||
Groin,
|
||||
Arms,
|
||||
Feet
|
||||
}
|
||||
|
||||
impl BodyPart {
|
||||
pub fn display(&self, sex: Option<Sex>) -> &'static str {
|
||||
use BodyPart::*;
|
||||
match self {
|
||||
Head => "head",
|
||||
Face => "face",
|
||||
Chest => match sex {
|
||||
Some(Sex::Female) => "breasts",
|
||||
_ => "chest",
|
||||
},
|
||||
Back => "back",
|
||||
Groin => match sex {
|
||||
Some(Sex::Male) => "penis",
|
||||
Some(Sex::Female) => "vagina",
|
||||
_ => "groin"
|
||||
},
|
||||
Arms => "arms",
|
||||
Feet => "feet"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items() -> Vec<Self> {
|
||||
use BodyPart::*;
|
||||
vec!(
|
||||
Head,
|
||||
Face,
|
||||
Chest,
|
||||
Back,
|
||||
Groin,
|
||||
Arms,
|
||||
Feet
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sample() -> Self {
|
||||
let mut rng = rand::thread_rng();
|
||||
Self::items().into_iter().choose(&mut rng).unwrap_or(BodyPart::Head)
|
||||
}
|
||||
pub struct SkillScaling {
|
||||
pub skill: SkillType,
|
||||
pub min_skill: f64,
|
||||
pub mean_damage_per_point_over_min: f64
|
||||
}
|
||||
|
||||
pub struct WeaponData {
|
||||
@ -67,6 +22,9 @@ pub struct WeaponData {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Default for WeaponData {
|
||||
@ -88,6 +46,9 @@ impl Default for WeaponData {
|
||||
&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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
97
blastmud_game/src/static_content/species.rs
Normal file
97
blastmud_game/src/static_content/species.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::BTreeMap;
|
||||
use crate::{
|
||||
models::item::Sex
|
||||
};
|
||||
use rand::seq::IteratorRandom;
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, Ord, Clone, PartialEq, PartialOrd, Debug)]
|
||||
pub enum SpeciesType {
|
||||
Human,
|
||||
Dog
|
||||
}
|
||||
|
||||
impl SpeciesType {
|
||||
pub fn sample_body_part(&self) -> BodyPart {
|
||||
let mut rng = rand::thread_rng();
|
||||
species_info_map().get(&self)
|
||||
.and_then(|sp| sp.body_parts.iter().choose(&mut rng))
|
||||
.unwrap_or(&BodyPart::Head).clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, Ord, Clone, PartialEq, PartialOrd, Debug)]
|
||||
pub enum BodyPart {
|
||||
Head,
|
||||
Face,
|
||||
Chest,
|
||||
Back,
|
||||
Groin,
|
||||
Arms,
|
||||
Legs,
|
||||
Feet
|
||||
}
|
||||
|
||||
impl BodyPart {
|
||||
pub fn display(&self, sex: Option<Sex>) -> &'static str {
|
||||
use BodyPart::*;
|
||||
match self {
|
||||
Head => "head",
|
||||
Face => "face",
|
||||
Chest => match sex {
|
||||
Some(Sex::Female) => "breasts",
|
||||
_ => "chest",
|
||||
},
|
||||
Back => "back",
|
||||
Groin => match sex {
|
||||
Some(Sex::Male) => "penis",
|
||||
Some(Sex::Female) => "vagina",
|
||||
_ => "groin"
|
||||
},
|
||||
Arms => "arms",
|
||||
Legs => "legs",
|
||||
Feet => "feet"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpeciesInfo {
|
||||
body_parts: Vec<BodyPart>,
|
||||
}
|
||||
|
||||
|
||||
pub fn species_info_map() -> &'static BTreeMap<SpeciesType, SpeciesInfo> {
|
||||
static INFOMAP: OnceCell<BTreeMap<SpeciesType, SpeciesInfo>> = OnceCell::new();
|
||||
INFOMAP.get_or_init(|| {
|
||||
vec!(
|
||||
(SpeciesType::Human,
|
||||
SpeciesInfo {
|
||||
body_parts: vec!(
|
||||
BodyPart::Head,
|
||||
BodyPart::Face,
|
||||
BodyPart::Chest,
|
||||
BodyPart::Back,
|
||||
BodyPart::Groin,
|
||||
BodyPart::Arms,
|
||||
BodyPart::Legs,
|
||||
BodyPart::Feet
|
||||
)
|
||||
}
|
||||
),
|
||||
(SpeciesType::Dog,
|
||||
SpeciesInfo {
|
||||
body_parts: vec!(
|
||||
BodyPart::Head,
|
||||
BodyPart::Face,
|
||||
BodyPart::Chest,
|
||||
BodyPart::Back,
|
||||
BodyPart::Groin,
|
||||
BodyPart::Legs,
|
||||
BodyPart::Feet
|
||||
)
|
||||
}
|
||||
),
|
||||
).into_iter().collect()
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user