diff --git a/Cargo.lock b/Cargo.lock index 334f969b..97e13968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index 07e38e6b..ee8249ac 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -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" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index c84a2518..b648900e 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -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; diff --git a/blastmud_game/src/message_handler/user_commands/attack.rs b/blastmud_game/src/message_handler/user_commands/attack.rs index 49f5995f..9c900e2a 100644 --- a/blastmud_game/src/message_handler/user_commands/attack.rs +++ b/blastmud_game/src/message_handler/user_commands/attack.rs @@ -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> { - 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!("{} {} {}.\n"), - &by_whom.display_for_sentence(true, 1, true), - verb, - &to_whom.display_for_sentence(true, 1, false)) - ); - msg_nonexp.push_str(&format!( - ansi!("{} {} {}.\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!("Your wristpad vibrates and blocks you from doing that. 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 help challenge]").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 } diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index 97e42050..2493c34a 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -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"); diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 639063e0..1a1c57a3 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -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; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 0a62efb4..6f171aa5 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -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, pub total_skills: BTreeMap, @@ -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(), diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index 0adeefe7..6b9e07e0 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -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() ) } diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index 2881f564..95396f39 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -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<()> { diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs new file mode 100644 index 00000000..dc6d75ca --- /dev/null +++ b/blastmud_game/src/services/combat.rs @@ -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> { + 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!("[ {} {}/{} ] {}.\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!("[ {} {}/{} ] {}.\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!("{} stiffens and collapses dead.\n"), + &whom.display_for_sentence(true, 1, true) + ); + let msg_nonexp = format!( + ansi!("{} stiffens and collapses dead.\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!("{} {} {}.\n"), + &by_whom.display_for_sentence(true, 1, true), + verb, + &to_whom.display_for_sentence(true, 1, false)) + ); + msg_nonexp.push_str(&format!( + ansi!("{} {} {}.\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(()) +} diff --git a/blastmud_game/src/services/skills.rs b/blastmud_game/src/services/skills.rs index 0bb42c40..327030b4 100644 --- a/blastmud_game/src/services/skills.rs +++ b/blastmud_game/src/services/skills.rs @@ -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?; diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs index bda48d40..c2fe90ea 100644 --- a/blastmud_game/src/static_content.rs +++ b/blastmud_game/src/static_content.rs @@ -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 { diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 2864bbb6..7ebffcd0 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -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, pub total_skills: BTreeMap, + 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, } } } diff --git a/blastmud_game/src/static_content/npc/melbs_dog.rs b/blastmud_game/src/static_content/npc/melbs_dog.rs index c0d4c6f6..66159cf5 100644 --- a/blastmud_game/src/static_content/npc/melbs_dog.rs +++ b/blastmud_game/src/static_content/npc/melbs_dog.rs @@ -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() } } diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 8133cd7d..fdaa5b51 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -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 String + 'static + Sync + Send>>; pub type AttackMessageChoicePart = Vec 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) -> &'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 { - 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, } 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!() } } } diff --git a/blastmud_game/src/static_content/species.rs b/blastmud_game/src/static_content/species.rs new file mode 100644 index 00000000..2826d3f8 --- /dev/null +++ b/blastmud_game/src/static_content/species.rs @@ -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) -> &'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, +} + + +pub fn species_info_map() -> &'static BTreeMap { + static INFOMAP: OnceCell> = 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() + }) +}