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} }, 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> { 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] 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. 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 } } static VERB_INT: Verb = Verb; pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;