Implement more combat capability.
This commit is contained in:
parent
c26a4768c5
commit
09db1a6ed9
@ -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<Option<time::Duration>> {
|
||||
todo!("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() {
|
||||
@ -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!("<red>{} {} {}.<reset>\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!("<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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use log::info;
|
||||
|
||||
pub mod room;
|
||||
pub mod npc;
|
||||
pub mod possession_type;
|
||||
mod fixed_item;
|
||||
|
||||
pub struct StaticItem {
|
||||
|
@ -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<NPCSayInfo>,
|
||||
pub attackable: bool
|
||||
pub attackable: bool,
|
||||
pub intrinsic_weapon: Option<PossessionType>,
|
||||
pub total_skills: BTreeMap<SkillType, f64>,
|
||||
}
|
||||
|
||||
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<dyn Iterator<Item = StaticItem>> {
|
||||
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::<Vec<String>>(),
|
||||
..Item::default()
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
201
blastmud_game/src/static_content/possession_type.rs
Normal file
201
blastmud_game/src/static_content/possession_type.rs
Normal 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()
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user