blastmud/blastmud_game/src/message_handler/user_commands/attack.rs

228 lines
10 KiB
Rust
Raw Normal View History

2023-01-15 17:30:23 +11:00
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;
2023-01-20 23:08:40 +11:00
use std::time;
use crate::{
2023-01-22 01:16:00 +11:00
services::{broadcast_to_room, skills::skill_check_and_grind},
2023-01-20 23:08:40 +11:00
db::{DBTrans, ItemSearchParams},
2023-01-22 01:16:00 +11:00
models::{
item::{Item, LocationActionType, Subattack, SkillType},
task::{Task, TaskMeta, TaskDetails}
},
static_content::{
possession_type::{WeaponData, BodyPart, possession_data, fist},
npc::npc_by_code,
},
2023-01-20 23:08:40 +11:00
regular_tasks::{TaskRunContext, TaskHandler},
DResult
};
2023-01-15 17:30:23 +11:00
use async_recursion::async_recursion;
2023-01-22 01:16:00 +11:00
use chrono::Utc;
2023-01-15 17:30:23 +11:00
2023-01-22 01:16:00 +11:00
#[derive(Clone)]
2023-01-20 23:08:40 +11:00
pub struct AttackTaskHandler;
#[async_trait]
impl TaskHandler for AttackTaskHandler {
2023-01-22 01:16:00 +11:00
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)))
2023-01-20 23:08:40 +11:00
}
}
2023-01-22 01:16:00 +11:00
pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler;
2023-01-15 23:16:02 +11:00
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(())
}
2023-01-22 01:16:00 +11:00
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)
}
2023-01-15 17:30:23 +11:00
#[async_recursion]
2023-01-15 23:16:02 +11:00
pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
2023-01-15 17:30:23 +11:00
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? {
2023-01-15 23:16:02 +11:00
stop_attacking(trans, by_whom, &cur_item_arc).await?;
2023-01-15 17:30:23 +11:00
}
}
_ => {}
}
verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on";
},
_ => {}
}
2023-01-22 01:16:00 +11:00
2023-01-15 17:30:23 +11:00
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))
);
2023-01-22 01:16:00 +11:00
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"));
2023-01-20 23:08:40 +11:00
broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?;
2023-01-15 17:30:23 +11:00
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)
);
2023-01-22 01:16:00 +11:00
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?;
2023-01-15 17:30:23 +11:00
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?;
}
2023-01-22 01:16:00 +11:00
Ok(())
2023-01-15 17:30:23 +11:00
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let attack_whom = search_item_for_user(ctx, &ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, remaining)
}).await?;
match attack_whom.item_type.as_str() {
"npc" => {}
"player" => {},
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
}
if attack_whom.item_code == player_item.item_code && attack_whom.item_type == player_item.item_type {
user_error("That's you, silly!".to_string())?
}
if attack_whom.is_challenge_attack_only {
// Add challenge check here.
2023-01-22 01:16:00 +11:00
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())?
2023-01-15 17:30:23 +11:00
}
start_attack(&ctx.trans, &player_item, &attack_whom).await
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;