diff --git a/blastmud_game/src/message_handler/user_commands/attack.rs b/blastmud_game/src/message_handler/user_commands/attack.rs index d638f449..49f5995f 100644 --- a/blastmud_game/src/message_handler/user_commands/attack.rs +++ b/blastmud_game/src/message_handler/user_commands/attack.rs @@ -4,22 +4,87 @@ use async_trait::async_trait; use ansi::ansi; use std::time; use crate::{ - services::broadcast_to_room, + services::{broadcast_to_room, skills::skill_check_and_grind}, db::{DBTrans, ItemSearchParams}, - models::{item::{Item, LocationActionType, Subattack}}, + models::{ + item::{Item, LocationActionType, Subattack, SkillType}, + task::{Task, TaskMeta, TaskDetails} + }, + static_content::{ + possession_type::{WeaponData, BodyPart, possession_data, fist}, + npc::npc_by_code, + }, regular_tasks::{TaskRunContext, TaskHandler}, DResult }; 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> { - todo!("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() { @@ -37,6 +102,23 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> 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(); @@ -63,6 +145,7 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR }, _ => {} } + msg_exp.push_str(&format!( ansi!("{} {} {}.\n"), &by_whom.display_for_sentence(true, 1, true), @@ -75,6 +158,11 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR 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(); @@ -88,6 +176,15 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR &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. @@ -95,7 +192,7 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR start_attack(trans, &to_whom_for_update, &by_whom_for_update).await?; } - Ok(()) + Ok(()) } pub struct Verb; @@ -120,7 +217,7 @@ impl UserVerb for Verb { if attack_whom.is_challenge_attack_only { // 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 very much functional. [Try help challenge]").to_string())? + 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())? } start_attack(&ctx.trans, &player_item, &attack_whom).await diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index 59127763..dbfd2a19 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -14,7 +14,8 @@ pub enum TaskDetails { NPCSay { npc_code: String, say_code: String - } + }, + AttackTick } impl TaskDetails { pub fn name(self: &Self) -> &'static str { @@ -22,6 +23,7 @@ impl TaskDetails { match self { RunQueuedCommand => "RunQueuedCommand", NPCSay { .. } => "NPCSay", + AttackTick => "AttackTick" } } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index 0581536c..0adeefe7 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -1,14 +1,19 @@ use tokio::{task, time, sync::oneshot}; use async_trait::async_trait; -use crate::{DResult, db, models::task::{Task, TaskParse, TaskRecurrence}}; -use crate::listener::{ListenerMap, ListenerSend}; +use crate::{ + DResult, + db, + models::task::{Task, TaskParse, TaskRecurrence}, + listener::{ListenerMap, ListenerSend}, + static_content::npc, + message_handler::user_commands::attack, +}; use blastmud_interfaces::MessageToListener; use log::warn; use once_cell::sync::OnceCell; use std::ops::AddAssign; use std::collections::BTreeMap; use chrono::Utc; -use crate::static_content::npc; pub mod queued_command; @@ -29,6 +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()) ).into_iter().collect() ) } diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs index f6fbd2c3..bda48d40 100644 --- a/blastmud_game/src/static_content.rs +++ b/blastmud_game/src/static_content.rs @@ -6,6 +6,7 @@ use log::info; pub mod room; pub mod npc; +pub mod possession_type; 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 4dd65322..2864bbb6 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -1,6 +1,6 @@ -use super::{StaticItem, StaticTask}; +use super::{StaticItem, StaticTask, possession_type::PossessionType}; use crate::models::{ - item::{Item, Pronouns}, + item::{Item, Pronouns, SkillType}, task::{Task, TaskMeta, TaskRecurrence, TaskDetails} }; use once_cell::sync::OnceCell; @@ -54,7 +54,9 @@ pub struct NPC { pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, pub aliases: Vec<&'static str>, pub says: Vec, - pub attackable: bool + pub attackable: bool, + pub intrinsic_weapon: Option, + pub total_skills: BTreeMap, } impl Default for NPC { @@ -68,7 +70,9 @@ impl Default for NPC { message_handler: None, aliases: vec!(), says: vec!(), - attackable: false + total_skills: SkillType::values().into_iter().map(|sk| (sk, 8.0)).collect(), + attackable: false, + intrinsic_weapon: None } } } @@ -126,6 +130,7 @@ pub fn npc_static_items() -> Box> { is_static: true, pronouns: c.pronouns.clone(), is_challenge_attack_only: !c.attackable, + total_skills: c.total_skills.clone(), aliases: c.aliases.iter().map(|a| (*a).to_owned()).collect::>(), ..Item::default() }) diff --git a/blastmud_game/src/static_content/npc/melbs_dog.rs b/blastmud_game/src/static_content/npc/melbs_dog.rs index 8da76301..c0d4c6f6 100644 --- a/blastmud_game/src/static_content/npc/melbs_dog.rs +++ b/blastmud_game/src/static_content/npc/melbs_dog.rs @@ -1,5 +1,6 @@ use super::NPC; use crate::models::item::Pronouns; +use crate::static_content::possession_type::PossessionType; macro_rules! dog { ($code:expr, $adj:expr, $spawn: expr) => { @@ -11,6 +12,7 @@ macro_rules! dog { description: "A malnourished looking dog. Its skeleton is visible through its thin and patchy fur. It smells terrible, and certainly doesn't look tame.", aliases: vec!("dog"), spawn_location: concat!("room/", $spawn), + intrinsic_weapon: Some(PossessionType::Fangs), ..Default::default() } } diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs new file mode 100644 index 00000000..8133cd7d --- /dev/null +++ b/blastmud_game/src/static_content/possession_type.rs @@ -0,0 +1,201 @@ +use serde::{Serialize, Deserialize}; +use crate::{ + models::item::{SkillType, Item, Sex} +}; +use once_cell::sync::OnceCell; +use std::collections::BTreeMap; +use rand::seq::SliceRandom; +use rand::seq::IteratorRandom; + +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 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, +} + +impl Default for WeaponData { + fn default() -> Self { + Self { + 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()) + ))), + } + } +} + +pub struct PossessionData { + pub weapon_data: Option +} + +impl Default for PossessionData { + fn default() -> Self { + Self { + weapon_data: None + } + } +} + +impl WeaponData { + pub fn normal_attack_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( + |f| f(attacker, victim, explicit_ok)).unwrap_or( + "No message defined yet".to_owned()) + } + + pub fn normal_attack_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( + |f| f(attacker, victim, part, explicit_ok)).unwrap_or( + "No message defined yet".to_owned()) + } + + +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum PossessionType { + // Special values that substitute for possessions. + Fangs, // Default weapon for certain animals + // Real possessions from here on: +} + +pub fn fist() -> &'static WeaponData { + static FIST_WEAPON: OnceCell = OnceCell::new(); + FIST_WEAPON.get_or_init(|| { + 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()) + ) + ) + ), + ..Default::default() + } + }) +} + +pub fn possession_data() -> &'static BTreeMap { + static POSSESSION_DATA: OnceCell> = OnceCell::new(); + use PossessionType::*; + &POSSESSION_DATA.get_or_init(|| { + vec!( + (Fangs, PossessionData { + weapon_data: Some(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!("{} 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()) + ) + ) + ), + ..Default::default() + }), + ..Default::default() + }) + ).into_iter().collect() + }) +}