Implement more combat capability.

This commit is contained in:
Condorra 2023-01-22 01:16:00 +11:00
parent c26a4768c5
commit 09db1a6ed9
7 changed files with 328 additions and 14 deletions

View File

@ -4,22 +4,87 @@ use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use std::time; use std::time;
use crate::{ use crate::{
services::broadcast_to_room, services::{broadcast_to_room, skills::skill_check_and_grind},
db::{DBTrans, ItemSearchParams}, 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}, regular_tasks::{TaskRunContext, TaskHandler},
DResult DResult
}; };
use async_recursion::async_recursion; use async_recursion::async_recursion;
use chrono::Utc;
#[derive(Clone)]
pub struct AttackTaskHandler; pub struct AttackTaskHandler;
#[async_trait] #[async_trait]
impl TaskHandler for AttackTaskHandler { impl TaskHandler for AttackTaskHandler {
async fn do_task(&self, _ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
todo!("AttackTaskHandler"); 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<()> { pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
let mut new_to_whom = (*to_whom).clone(); let mut new_to_whom = (*to_whom).clone();
if let Some(ac) = new_to_whom.active_combat.as_mut() { 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(()) 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] #[async_recursion]
pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> { pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
let mut msg_exp = String::new(); 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!( msg_exp.push_str(&format!(
ansi!("<red>{} {} {}.<reset>\n"), ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(true, 1, true), &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, verb,
&to_whom.display_for_sentence(false, 1, false)) &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?; 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(); 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) &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(&by_whom_for_update).await?;
trans.save_item_model(&to_whom_for_update).await?; trans.save_item_model(&to_whom_for_update).await?;
// Auto-counterattack if victim isn't busy. // Auto-counterattack if victim isn't busy.
@ -120,7 +217,7 @@ impl UserVerb for Verb {
if attack_whom.is_challenge_attack_only { if attack_whom.is_challenge_attack_only {
// Add challenge check here. // 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 very much functional. [Try <bold>help challenge<reset>]").to_string())? 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())?
} }
start_attack(&ctx.trans, &player_item, &attack_whom).await start_attack(&ctx.trans, &player_item, &attack_whom).await

View File

@ -14,7 +14,8 @@ pub enum TaskDetails {
NPCSay { NPCSay {
npc_code: String, npc_code: String,
say_code: String say_code: String
} },
AttackTick
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -22,6 +23,7 @@ impl TaskDetails {
match self { match self {
RunQueuedCommand => "RunQueuedCommand", RunQueuedCommand => "RunQueuedCommand",
NPCSay { .. } => "NPCSay", NPCSay { .. } => "NPCSay",
AttackTick => "AttackTick"
} }
} }
} }

View File

@ -1,14 +1,19 @@
use tokio::{task, time, sync::oneshot}; use tokio::{task, time, sync::oneshot};
use async_trait::async_trait; use async_trait::async_trait;
use crate::{DResult, db, models::task::{Task, TaskParse, TaskRecurrence}}; use crate::{
use crate::listener::{ListenerMap, ListenerSend}; DResult,
db,
models::task::{Task, TaskParse, TaskRecurrence},
listener::{ListenerMap, ListenerSend},
static_content::npc,
message_handler::user_commands::attack,
};
use blastmud_interfaces::MessageToListener; use blastmud_interfaces::MessageToListener;
use log::warn; use log::warn;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::ops::AddAssign; use std::ops::AddAssign;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use chrono::Utc; use chrono::Utc;
use crate::static_content::npc;
pub mod queued_command; pub mod queued_command;
@ -29,6 +34,7 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
|| vec!( || vec!(
("RunQueuedCommand", queued_command::HANDLER.clone()), ("RunQueuedCommand", queued_command::HANDLER.clone()),
("NPCSay", npc::SAY_HANDLER.clone()), ("NPCSay", npc::SAY_HANDLER.clone()),
("AttackTick", attack::TASK_HANDLER.clone())
).into_iter().collect() ).into_iter().collect()
) )
} }

View File

@ -6,6 +6,7 @@ use log::info;
pub mod room; pub mod room;
pub mod npc; pub mod npc;
pub mod possession_type;
mod fixed_item; mod fixed_item;
pub struct StaticItem { pub struct StaticItem {

View File

@ -1,6 +1,6 @@
use super::{StaticItem, StaticTask}; use super::{StaticItem, StaticTask, possession_type::PossessionType};
use crate::models::{ use crate::models::{
item::{Item, Pronouns}, item::{Item, Pronouns, SkillType},
task::{Task, TaskMeta, TaskRecurrence, TaskDetails} task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -54,7 +54,9 @@ pub struct NPC {
pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>,
pub aliases: Vec<&'static str>, pub aliases: Vec<&'static str>,
pub says: Vec<NPCSayInfo>, pub says: Vec<NPCSayInfo>,
pub attackable: bool pub attackable: bool,
pub intrinsic_weapon: Option<PossessionType>,
pub total_skills: BTreeMap<SkillType, f64>,
} }
impl Default for NPC { impl Default for NPC {
@ -68,7 +70,9 @@ impl Default for NPC {
message_handler: None, message_handler: None,
aliases: vec!(), aliases: vec!(),
says: 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<dyn Iterator<Item = StaticItem>> {
is_static: true, is_static: true,
pronouns: c.pronouns.clone(), pronouns: c.pronouns.clone(),
is_challenge_attack_only: !c.attackable, is_challenge_attack_only: !c.attackable,
total_skills: c.total_skills.clone(),
aliases: c.aliases.iter().map(|a| (*a).to_owned()).collect::<Vec<String>>(), aliases: c.aliases.iter().map(|a| (*a).to_owned()).collect::<Vec<String>>(),
..Item::default() ..Item::default()
}) })

View File

@ -1,5 +1,6 @@
use super::NPC; use super::NPC;
use crate::models::item::Pronouns; use crate::models::item::Pronouns;
use crate::static_content::possession_type::PossessionType;
macro_rules! dog { macro_rules! dog {
($code:expr, $adj:expr, $spawn: expr) => { ($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.", 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"), aliases: vec!("dog"),
spawn_location: concat!("room/", $spawn), spawn_location: concat!("room/", $spawn),
intrinsic_weapon: Some(PossessionType::Fangs),
..Default::default() ..Default::default()
} }
} }

View File

@ -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<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 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<WeaponData>
}
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<WeaponData> = 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<PossessionType, PossessionData> {
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, PossessionData>> = 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()
})
}