use crate::{ services::{ comms::broadcast_to_room, skills::skill_check_and_grind, skills::skill_check_only, }, models::{ item::{Item, LocationActionType, Subattack, SkillType, DeathData}, task::{Task, TaskMeta, TaskDetails}, }, static_content::{ possession_type::{WeaponData, possession_data, fist}, npc::npc_by_code, species::species_info_map, }, message_handler::user_commands::{user_error, UResult, drop::consider_expire_job_for_item}, regular_tasks::{TaskRunContext, TaskHandler}, DResult, }; use mockall_double::double; #[double] use crate::db::DBTrans; use async_trait::async_trait; use chrono::Utc; use async_recursion::async_recursion; use std::time; use ansi::ansi; use rand::prelude::IteratorRandom; use rand_distr::{Normal, Distribution}; #[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() }; if attacker_item.death_data.is_some() || victim_item.death_data.is_some() { return Ok(None) } 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 user_opt = if ctype == "player" { ctx.trans.find_by_username(ccode).await? } else { None }; let attack_result = if let Some(user) = user_opt { let raw_skill = *user.raw_skills.get(&weapon.uses_skill).unwrap_or(&0.0); if raw_skill >= weapon.raw_min_to_learn && raw_skill <= weapon.raw_max_to_learn { skill_check_and_grind(ctx.trans, &mut attacker_item, &weapon.uses_skill, victim_dodge_skill).await? } else { skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill) } } else { skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill) }; 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?; ctx.trans.save_item_model(&attacker_item).await?; ctx.trans.save_item_model(&victim_item).await?; } else { // TODO: Parry system of some kind? // Determine body part... let part = victim_item.species.sample_body_part(); // TODO: Armour / soaks // TODO: Calculate damage etc... and display health impact. let mut mean_damage: f64 = weapon.normal_attack_mean_damage; for scaling in weapon.normal_attack_skill_scaling.iter() { let skill = *attacker_item.total_skills.get(&scaling.skill).unwrap_or(&0.0); if skill >= scaling.min_skill { mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min; } } let actual_damage = Normal::new(mean_damage, weapon.normal_attack_stdev_damage)? .sample(&mut rand::thread_rng()).floor().max(1.0) as i64; ctx.trans.save_item_model(&attacker_item).await?; let msg_exp = weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, true); let msg_nonexp = weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, false); if change_health(ctx.trans, -actual_damage, &mut victim_item, &msg_exp, &msg_nonexp).await? { ctx.trans.save_item_model(&victim_item).await?; return Ok(None); } ctx.trans.save_item_model(&victim_item).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?; Ok(Some(attack_speed(&attacker_item))) } } pub async fn change_health(trans: &DBTrans, change: i64, victim: &mut Item, reason_exp: &str, reason_nonexp: &str) -> DResult { let maxh = max_health(victim); let new_health = ((victim.health as i64 + change).max(0) as u64).min(maxh); if change >= 0 && new_health == victim.health { return Ok(false); } let colour = if change > 0 { ansi!("") } else { ansi!("") }; let msg_exp = format!(ansi!("[ {}{} {}/{} ] {}.\n"), colour, change, new_health, max_health(&victim), reason_exp); let msg_nonexp = format!(ansi!("[ {}{} {}/{} ] {}.\n"), colour, change, new_health, maxh, reason_nonexp); broadcast_to_room(trans, &victim.location, None, &msg_exp, Some(&msg_nonexp)).await?; victim.health = new_health; if new_health == 0 { handle_death(trans, victim).await?; Ok(true) } else { Ok(false) } } pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item: &Item) -> DResult<()> { if by_item.item_type != "player" { return Ok(()); } let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? { None => return Ok(()), Some(r) => r }; if by_item.total_xp >= for_item.total_xp { trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?; return Ok(()); } let xp_gain = (((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as u64) .min(100); if xp_gain == 0 { trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?; return Ok(()); } by_item.total_xp += xp_gain; let mut user = match trans.find_by_username(&by_item.item_code).await? { None => return Ok(()), Some(r) => r }; user.experience.xp_change_for_this_reroll += xp_gain as i64; // Now consider kill bonuses... if for_item.item_type == "npc" { if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) { if let Some(bonus) = &npc.kill_bonus { user.credits += bonus.payment; trans.queue_for_session(&session, Some(&format!("{}\nYour wristpad beeps for a credit of {} for that.\n", bonus.msg, bonus.payment))).await?; } } } trans.save_user_model(&user).await?; trans.queue_for_session(&session, Some(&format!("You gained {} experience points!\n", xp_gain))).await?; Ok(()) } pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> { let msg_exp = format!( ansi!("{} stiffens and collapses dead.\n"), &whom.display_for_sentence(true, 1, true) ); let msg_nonexp = format!( ansi!("{} stiffens and collapses dead.\n"), &whom.display_for_sentence(false, 1, true) ); broadcast_to_room(trans, &whom.location, None, &msg_exp, Some(&msg_nonexp)).await?; whom.death_data = Some(DeathData { parts_remaining: species_info_map().get(&whom.species) .map(|sp| sp.corpse_butchers_into.clone()).unwrap_or_else(|| vec!()), ..Default::default() }); if let Some(ac) = &whom.active_combat { let at_str = ac.attacking.clone(); for attacker in ac.attacked_by.clone().iter() { if let Some((atype, acode)) = attacker.split_once("/") { if let Some(aitem) = trans.find_item_by_type_code(atype, acode).await? { let mut new_aitem = (*aitem).clone(); consider_reward_for(trans, &mut new_aitem, &whom).await?; stop_attacking_mut(trans, &mut new_aitem, whom, true).await?; trans.save_item_model(&new_aitem).await?; } } } if let Some((vtype, vcode)) = at_str.as_ref().and_then(|a| a.split_once("/")) { if let Some(vitem) = trans.find_item_by_type_code(vtype, vcode).await? { let mut new_vitem = (*vitem).clone(); stop_attacking_mut(trans, whom, &mut new_vitem, false).await?; trans.save_item_model(&new_vitem).await?; } } } if whom.item_type == "npc" { trans.upsert_task(&Task { meta: TaskMeta { task_code: whom.item_code.clone(), next_scheduled: Utc::now() + chrono::Duration::seconds(120), ..Default::default() }, details: TaskDetails::RecloneNPC { npc_code: whom.item_code.clone() } }).await?; } else if whom.item_type == "player" { trans.revoke_until_death_consent(&whom.item_code).await?; } Ok(()) } pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult { corpsify_item(trans, &player).await?; player.death_data = None; let lost_xp = (player.total_xp / 200).max(10).min(player.total_xp); let (session, _) = match trans.find_session_for_player(&player.item_code).await? { None => return Ok(false), Some(r) => r }; let mut user = match trans.find_by_username(&player.item_code).await? { None => return Ok(false), Some(r) => r }; trans.queue_for_session(&session, Some(&format!("You lost {} experience points by dying.\n", lost_xp))).await?; player.total_xp -= lost_xp; user.experience.xp_change_for_this_reroll -= lost_xp as i64; player.health = max_health(&player); trans.save_user_model(&user).await?; Ok(true) } pub fn max_health(whom: &Item) -> u64 { if whom.item_type == "npc" { npc_by_code().get(whom.item_code.as_str()) .map(|npc| npc.max_health) .unwrap_or(24) } else if whom.item_type == "player" { (22.0 + (whom.total_xp as f64).log(1.4)).min(60.0) as u64 } else if whom.item_type == "possession" { whom.possession_type.as_ref().and_then(|pt| possession_data().get(&pt)) .map(|poss| poss.max_health) .unwrap_or(10) } else { 24 } } pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler; pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_whom: &mut Item, auto_refocus: bool) -> DResult<()> { trans.delete_task("AttackTick", &format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code)).await?; if let Some(ac) = new_to_whom.active_combat.as_mut() { let old_attacker = format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code); ac.attacked_by.retain(|v| v != &old_attacker); } if let Some(ac) = new_by_whom.active_combat.as_mut() { ac.attacking = None; if auto_refocus { let old_vic = format!("{}/{}", new_to_whom.item_type, new_to_whom.item_code); let new_vic_opt = ac.attacked_by.iter().filter(|i| **i != old_vic).choose(&mut rand::thread_rng()); if let Some(new_vic) = new_vic_opt { if let Some((vtype, vcode)) = new_vic.split_once("/") { if let Some(vic_item) = trans.find_item_by_type_code(vtype, vcode).await? { let mut new_vic_item = (*vic_item).clone(); start_attack_mut(trans, new_by_whom, &mut new_vic_item); trans.save_item_model(&new_vic_item).await?; } } } } } new_by_whom.action_type = LocationActionType::Normal; Ok(()) } pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> DResult<()> { let mut new_by_whom = (*by_whom).clone(); let mut new_to_whom = (*to_whom).clone(); stop_attacking_mut(trans, &mut new_by_whom, &mut new_to_whom, false).await?; trans.save_item_model(&new_by_whom).await?; trans.save_item_model(&new_to_whom).await?; Ok(()) } async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { if let Some(item) = trans.find_by_action_and_location( &who.refstr(), &LocationActionType::Wielded).await? { if let Some(dat) = item.possession_type.as_ref() .and_then(|pt| possession_data().get(&pt)) .and_then(|pd| pd.weapon_data.as_ref()) { return Ok(dat); } } // 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 by_whom_for_update = by_whom.clone(); let mut to_whom_for_update = to_whom.clone(); start_attack_mut(trans, &mut by_whom_for_update, &mut to_whom_for_update).await?; trans.save_item_model(&by_whom_for_update).await?; trans.save_item_model(&to_whom_for_update).await?; Ok(()) } #[async_recursion] pub async fn start_attack_mut(trans: &DBTrans, by_whom: &mut Item, to_whom: &mut 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?; by_whom.active_combat.get_or_insert_with(|| Default::default()).attacking = Some(format!("{}/{}", &to_whom.item_type, &to_whom.item_code)); by_whom.action_type = LocationActionType::Attacking(Subattack::Normal); to_whom.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?; // Auto-counterattack if victim isn't busy. if to_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()) == None { start_attack_mut(trans, to_whom, by_whom).await?; } Ok(()) } pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<()> { let mut new_item = base_item.clone(); new_item.item_type = "corpse".to_owned(); new_item.item_code = format!("{}", trans.alloc_item_code().await?); new_item.is_static = false; trans.save_item_model(&new_item).await?; trans.upsert_task(&Task { meta: TaskMeta { task_code: new_item.item_code.clone(), next_scheduled: Utc::now() + chrono::Duration::minutes(5), ..Default::default() }, details: TaskDetails::RotCorpse { corpse_code: new_item.item_code.clone() } }).await?; trans.transfer_all_possessions(base_item, &new_item).await?; Ok(()) } pub struct NPCRecloneTaskHandler; #[async_trait] impl TaskHandler for NPCRecloneTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::RecloneNPC { npc_code } => npc_code.clone(), _ => Err("Expected RecloneNPC type")? }; let mut npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { return Ok(None) }, Some(r) => (*r).clone() }; let npc = match npc_by_code().get(npc_code.as_str()) { None => { return Ok(None) }, Some(r) => r }; if npc_item.death_data.is_none() { return Ok(None); } corpsify_item(ctx.trans, &npc_item).await?; npc_item.death_data = None; npc_item.health = max_health(&npc_item); npc_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&npc_item).await?; return Ok(None); } } pub struct RotCorpseTaskHandler; #[async_trait] impl TaskHandler for RotCorpseTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let corpse_code = match &ctx.task.details { TaskDetails::RotCorpse { corpse_code } => corpse_code.clone(), _ => Err("Expected RotCorpse type")? }; let corpse = match ctx.trans.find_item_by_type_code("corpse", &corpse_code).await? { None => { return Ok(None) } Some(r) => r }; ctx.trans.delete_item("corpse", &corpse_code).await?; let msg_exp = format!("{} rots away to nothing.\n", corpse.display_for_sentence(true, 1, true)); let msg_nonexp = format!("{} rots away to nothing.\n", corpse.display_for_sentence(false, 1, true)); for item in ctx.trans.find_items_by_location( &format!("{}/{}", &corpse.item_type, &corpse.item_code)).await?.into_iter() { let mut item_mut = (*item).clone(); // We only update this to support consider_expire_job - it gets updated in bulk // by transfer_all_possession below. item_mut.location = corpse.location.clone(); consider_expire_job_for_item(ctx.trans, &item_mut).await?; } ctx.trans.transfer_all_possessions_code( &format!("{}/{}", &corpse.item_type, &corpse.item_code), &corpse.location).await?; broadcast_to_room(ctx.trans, &corpse.location, None, &msg_exp, Some(&msg_nonexp)).await?; Ok(None) } } pub static ROT_CORPSE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &RotCorpseTaskHandler;