#[double] use crate::db::DBTrans; use crate::{ message_handler::user_commands::{ follow::cancel_follow_by_leader, stand::stand_if_needed, user_error, CommandHandlingError, UResult, VerbContext, }, models::{ corp::CorpCommType, effect::EffectType, item::{ AttackMode, DeathData, Item, ItemFlag, LocationActionType, SkillType, StatType, Subattack, }, journal::JournalType, task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{TaskHandler, TaskRunContext}, services::{ comms::broadcast_to_room, destroy_container, skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only}, urges::{change_stress_considering_cool, recalculate_urge_growth}, }, static_content::{ journals::{award_journal_if_needed, check_journal_for_kill}, npc::npc_by_code, possession_type::{ fist, possession_data, DamageDistribution, DamageType, WeaponAttackData, WeaponData, }, species::{species_info_map, BodyPart, SpeciesType}, }, DResult, }; use ansi::ansi; use async_recursion::async_recursion; use async_trait::async_trait; use chrono::{TimeDelta, Utc}; use mockall_double::double; use rand::{prelude::IteratorRandom, thread_rng, Rng}; use rand_distr::{Distribution, Normal}; use std::{sync::Arc, time}; use super::{ effect::{cancel_effect, default_effects_for_type, run_effects}, sharing::stop_conversation_mut, }; pub async fn soak_damage( trans: &DBTrans, attack: &DamageDist, victim: &Item, presoak_amount: f64, part: &BodyPart, ) -> DResult { let damage_by_type: Vec<(&DamageType, f64)> = attack.distribute_damage(presoak_amount); let mut clothes: Vec = trans .find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn) .await? .iter() .map(|cl| (*cl.as_ref()).clone()) .collect(); clothes.sort_unstable_by(|c1, c2| c2.action_type_started.cmp(&c1.action_type_started)); let mut total_damage = 0.0; for (damage_type, mut damage_amount) in &damage_by_type { for clothing in &mut clothes { if let Some(soak) = clothing .possession_type .as_ref() .and_then(|pt| possession_data().get(pt)) .and_then(|pd| pd.wear_data.as_ref()) .filter(|wd| wd.covers_parts.contains(part)) .and_then(|wd| wd.soaks.get(&damage_type)) { if damage_amount <= 0.0 { break; } let soak_amount: f64 = ((soak.max_soak - soak.min_soak) * rand::thread_rng().gen::()) .min(damage_amount); damage_amount -= soak_amount; let clothes_damage = ((0..(soak_amount as i64)) .filter(|_| rand::thread_rng().gen::() < soak.damage_probability_per_soak) .count() as u64) .min(clothing.health); if clothes_damage > 0 { clothing.health -= clothes_damage; if victim.item_type == "player" { if let Some((vic_sess, _sess_dat)) = trans.find_session_for_player(&victim.item_code).await? { trans .queue_for_session( &vic_sess, Some(&format!( "A few bits and pieces fly off your {}.\n", clothing.display_for_sentence(1, false) )), ) .await?; } } } } } total_damage += damage_amount; } for clothing in &clothes { if clothing.health <= 0 { trans .delete_item(&clothing.item_type, &clothing.item_code) .await?; if victim.item_type == "player" { if let Some((vic_sess, _sess_dat)) = trans.find_session_for_player(&victim.item_code).await? { trans .queue_for_session( &vic_sess, Some(&format!( "Your {} is completely destroyed; it crumbles away to nothing.\n", clothing.display_for_sentence(1, false) )), ) .await?; } } } } Ok(total_damage) } async fn start_next_attack( ctx: &mut TaskRunContext<'_>, attacker_item: &Item, victim_item: &Item, weapon: &WeaponData, ) -> DResult<()> { let msg = &(weapon .normal_attack .start_message(&attacker_item, victim_item) + ".\n"); broadcast_to_room(ctx.trans, &attacker_item.location, None, msg).await?; Ok(()) } async fn process_attack( ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, weapon_item: &Item, victim_item: &mut Item, attack: &WeaponAttackData, weapon: &WeaponData, ) -> DResult { if attacker_item .urges .as_ref() .map(|u| u.stress.value) .unwrap_or(0) > 8000 && !attacker_item.flags.contains(&ItemFlag::Invincible) { let msg = format!( "{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n", attacker_item.display_for_sentence(1, true), attacker_item.pronouns.subject, victim_item.display_for_sentence(1, false), ); broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg).await?; match attacker_item.active_combat.as_mut() { Some(ac) if ac.attack_mode != AttackMode::NORMAL => { ac.attack_mode = AttackMode::NORMAL; ctx.trans.save_item_model(&attacker_item).await?; } _ => {} } return Ok(false); } 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, victim_item, &SkillType::Dodge, attack_skill).await?; let user_opt = if attacker_item.item_type == "player" { ctx.trans.find_by_username(&attacker_item.item_code).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, 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) }; change_stress_considering_cool(&ctx.trans, attacker_item, 100).await?; if (dodge_result > attack_result && !attacker_item.flags.contains(&ItemFlag::Invincible)) || victim_item.flags.contains(&ItemFlag::Invincible) { let msg = format!( "{} dodges out of the way of {}'s attack.\n", victim_item.display_for_sentence(1, true), attacker_item.display_for_sentence(1, false) ); broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg).await?; match attacker_item.active_combat.as_mut() { Some(ac) => ac.attack_mode = AttackMode::NORMAL, None => {} } 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(); let mut mean_damage: f64 = attack.mean_damage; for scaling in 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_presoak = Normal::new(mean_damage, attack.stdev_damage)? .sample(&mut rand::thread_rng()) .floor() .max(1.0) as i64; match attacker_item.active_combat.as_mut() { Some(ac) => ac.attack_mode = AttackMode::NORMAL, None => {} } ctx.trans.save_item_model(&attacker_item).await?; let actual_damage = soak_damage( &ctx.trans, attack, victim_item, actual_damage_presoak as f64, &part, ) .await? as i64; let msg = attack.success_message(&attacker_item, victim_item, &part); if actual_damage == 0 { let msg = format!( "{}'s attack bounces off {}'s {}.\n", &attacker_item.display_for_sentence(1, true), &victim_item.display_for_sentence(1, false), &part.display(victim_item.sex.clone()) ); broadcast_to_room(&ctx.trans, &victim_item.location, None, &msg).await?; } else if change_health(ctx.trans, -actual_damage, victim_item, &msg).await? { ctx.trans.save_item_model(victim_item).await?; return Ok(true); } // Consider applying a crit effect... let mut crit_rand = rand::thread_rng().gen::(); for (p, eff_type) in &attack.crit_effects { if victim_item.active_effects.iter().any(|e| e.0 == *eff_type) { continue; } if *p >= crit_rand { if let Some(effect_set) = default_effects_for_type().get(eff_type) { run_effects( ctx.trans, &effect_set, attacker_item, weapon_item, Some(victim_item), 0.0, ) .await?; } } else { crit_rand -= *p; } } ctx.trans.save_item_model(victim_item).await?; } start_next_attack(ctx, &attacker_item, victim_item, weapon).await?; Ok(false) } async fn process_feint( ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, victim_item: &mut Item, ) -> DResult { if attacker_item .urges .as_ref() .map(|u| u.stress.value) .unwrap_or(0) > 8000 { let msg = format!( "{} looks like {} wanted to feint {}, but was too tired and stressed to do it.\n", attacker_item.display_for_sentence(1, true), attacker_item.pronouns.subject, victim_item.display_for_sentence(1, false), ); broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg).await?; match attacker_item.active_combat.as_mut() { Some(ac) => { ac.attack_mode = AttackMode::NORMAL; ctx.trans.save_item_model(&attacker_item).await?; } _ => {} } return Ok(false); } let attacker_brn = *attacker_item .total_stats .get(&StatType::Brains) .unwrap_or(&8.0); let victim_brn = *victim_item .total_stats .get(&StatType::Brains) .unwrap_or(&8.0); let fuzzed_diff = Normal::new(attacker_brn - victim_brn, 2.0)?.sample(&mut rand::thread_rng()); if fuzzed_diff <= -1.0 { broadcast_to_room( &ctx.trans, &attacker_item.location, None, &format!( "{} seems to have pulled off the feint so poorly {} confused {}!\n", &attacker_item.display_for_sentence(1, true), &attacker_item.pronouns.object, &attacker_item.pronouns.intensive, ), ) .await?; run_effects( &ctx.trans, default_effects_for_type() .get(&EffectType::Stunned) .unwrap(), attacker_item, &attacker_item.clone(), None, 0.0, ) .await?; } else if fuzzed_diff >= 1.0 && !victim_item .active_effects .iter() .any(|v| v.0 == EffectType::Stunned) { broadcast_to_room( &ctx.trans, &attacker_item.location, None, &format!( "{} is confused by {}'s antics!\n", &victim_item.display_for_sentence(1, true), &attacker_item.display_for_sentence(1, false), ), ) .await?; run_effects( &ctx.trans, default_effects_for_type() .get(&EffectType::Stunned) .unwrap(), victim_item, attacker_item, None, 0.0, ) .await?; } else { broadcast_to_room( &ctx.trans, &attacker_item.location, None, &format!( "{} doesn't seem to have fallen for {}'s unenlightened attempt at a feint.\n", &victim_item.display_for_sentence(1, true), &attacker_item.display_for_sentence(1, false) ), ) .await?; return Ok(false); } match attacker_item.active_combat.as_mut() { Some(ac) => { ac.attack_mode = AttackMode::NORMAL; } _ => {} } ctx.trans.save_item_model(&attacker_item).await?; ctx.trans.save_item_model(&victim_item).await?; Ok(false) } #[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 attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? { None => { return Ok(None); } // Player is gone Some(item) => item, }; 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_it, weapon) = what_wielded(ctx.trans, &attacker_item).await?; let mut attacker_item_mut = (*attacker_item).clone(); if attacker_item_mut .active_effects .iter() .any(|v| v.0 == EffectType::Stunned) { match attacker_item_mut.active_combat.as_mut() { Some(ac) if ac.attack_mode != AttackMode::NORMAL => { ac.attack_mode = AttackMode::NORMAL; ctx.trans.save_item_model(&attacker_item_mut).await?; } _ => {} } return Ok(Some(attack_speed(&attacker_item_mut))); } let mode = attacker_item .active_combat .as_ref() .map(|ac| ac.attack_mode.clone()) .unwrap_or(AttackMode::NORMAL); if mode == AttackMode::FEINT { if process_feint(ctx, &mut attacker_item_mut, &mut victim_item).await? { start_next_attack(ctx, &mut attacker_item_mut, &mut victim_item, &weapon).await?; } } else { process_attack( ctx, &mut attacker_item_mut, &weapon_it, &mut victim_item, if mode == AttackMode::POWER { if let Some(pow) = weapon.power_attack.as_ref() { pow } else { &weapon.normal_attack } } else { &weapon.normal_attack }, &weapon, ) .await?; } // We re-check this on the next tick, rather than going off if the victim // died. That prevents a bug when re-focusing where we re-schedule and then // re-delete the task. Ok(Some(attack_speed(&attacker_item_mut))) } } pub async fn change_health( trans: &DBTrans, change: i64, victim: &mut Item, reason: &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 = format!( ansi!("[ {}{} {}/{} ] {}.\n"), colour, change, new_health, max_health(&victim), reason ); broadcast_to_room(trans, &victim.location, None, &msg).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" || by_item.flags.contains(&ItemFlag::Invincible) { return Ok(()); } let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? { None => return Ok(()), Some(r) => r, }; let mut user = match trans.find_by_username(&by_item.item_code).await? { None => return Ok(()), Some(r) => r, }; let xp_gain = if by_item.total_xp >= for_item.total_xp { 0 } else { let xp_gain = (((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as i64) .min(100); user.adjust_xp_for_reroll(by_item, xp_gain, trans, &session) .await?; xp_gain }; // Not intended to be saved, just a convenience to describe it before it was dead. let mut for_item_sim_notdead = for_item.clone(); for_item_sim_notdead.death_data = None; // 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 { let mut user_payment = bonus.payment; for (corpid, mut corp) in trans.get_corps_for_user(&by_item.item_code).await? { let actual_tax = (corp.tax as f64 * 0.0001 * (bonus.payment as f64)) .min(user_payment as f64) as u64; if actual_tax > 0 { user_payment -= actual_tax; corp.credits += actual_tax; trans.update_corp_details(&corpid, &corp).await?; trans .broadcast_to_corp( &corpid, &CorpCommType::Reward, None, &format!( "{} earned ${} (of which ${} went to {}) for killing {}.\n", by_item.display, bonus.payment, actual_tax, corp.name, &for_item_sim_notdead.display_for_sentence(1, false) ), ) .await?; } // Consider a notice to corp that missed out on tax due to other corps, // so they can consider a chat with the member about the situation? } user.credits += user_payment; trans .queue_for_session( &session, Some(&format!( "{}\nYour wristpad beeps for a credit of ${}.\n", bonus.msg, user_payment )), ) .await?; } } } trans.save_user_model(&user).await?; if xp_gain == 0 { trans .queue_for_session(&session, Some("[You didn't gain any experience points]\n")) .await?; } else { 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 = format!( ansi!("{} stiffens and collapses dead.\n"), &whom.display_for_sentence(1, true) ); broadcast_to_room(trans, &whom.location, None, &msg).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() }); whom.following = None; cancel_follow_by_leader(trans, &whom.refstr()).await?; let vic_is_npc = whom.item_type == "npc"; 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?; if vic_is_npc { check_journal_for_kill(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?; } } } match whom.urges.as_mut() { None => {} Some(urges) => { urges.hunger = Default::default(); urges.thirst = Default::default(); urges.stress = Default::default(); } } if vic_is_npc { trans .upsert_task(&Task { meta: TaskMeta { task_code: whom.item_code.clone(), next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(600).unwrap(), ..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?; match trans.find_by_username(&whom.item_code).await? { None => {} Some(mut user) => { if award_journal_if_needed(trans, &mut user, whom, JournalType::Died).await? { trans.save_user_model(&user).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.temporary_buffs = vec![]; player.flags.push(ItemFlag::HasUrges); for effect in &player.active_effects { cancel_effect(trans, player, effect).await?; } player.active_climb = None; if player.active_conversation.is_some() { stop_conversation_mut(trans, player, "stops talking on account of being dead").await?; } player.active_effects = vec![]; calculate_total_stats_skills_for_user(player, &user); recalculate_urge_growth(trans, player).await?; player.health = max_health(&player); player.active_climb = None; 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(); match start_attack_mut(trans, new_by_whom, &mut new_vic_item).await { Err(CommandHandlingError::UserError(_)) | Ok(()) => {} Err(CommandHandlingError::SystemError(e)) => return Err(e), } trans.save_item_model(&new_vic_item).await?; } } } else { new_by_whom.action_type = LocationActionType::Normal; } } else { 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: &Arc, ) -> DResult<(Arc, &'static WeaponData)> { if let Some(item) = trans .find_by_action_and_location(&who.refstr(), &LocationActionType::Wielded) .await? .first() { 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((item.clone(), 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((who.clone(), weapon)); } } } Ok((who.clone(), fist())) } fn attack_speed(who: &Item) -> time::Duration { let base_time = 5; let time_multiplier = who .active_combat .as_ref() .map(|ac| match ac.attack_mode { AttackMode::NORMAL => 1, AttackMode::POWER => 2, AttackMode::FEINT => 1, }) .unwrap_or(1); time::Duration::from_secs(base_time * time_multiplier) } #[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 = String::new(); let mut verb: String = "attacks".to_string(); match by_whom.action_type { LocationActionType::Sitting { .. } | LocationActionType::Reclining { .. } => { stand_if_needed(trans, by_whom).await?; } 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.push_str(&format!( ansi!("{} {} {}.\n"), &by_whom.display_for_sentence(1, true), verb, &to_whom.display_for_sentence(1, false) )); let (_, wielded) = what_wielded(trans, &Arc::new(by_whom.clone())).await?; msg.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom) + ".\n")); broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg).await?; let ac = by_whom .active_combat .get_or_insert_with(|| Default::default()); ac.attacking = Some(format!("{}/{}", &to_whom.item_type, &to_whom.item_code)); ac.attack_mode = AttackMode::NORMAL; 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::TimeDelta::try_milliseconds(attack_speed(by_whom).as_millis() as i64) .unwrap(), ..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::TimeDelta::try_minutes(5).unwrap(), ..Default::default() }, details: TaskDetails::RotCorpse { corpse_code: new_item.item_code.clone(), }, }) .await?; trans.transfer_all_possessions(base_item, &new_item).await?; Ok(new_item) } pub async fn switch_to_power_attack(ctx: &VerbContext<'_>, who: &Arc) -> UResult<()> { let (wield_it, wielded) = what_wielded(&ctx.trans, who).await?; let pow_att = match wielded.power_attack.as_ref() { None => user_error(format!( "{} is unsuitable for powerattacking", wield_it.display_for_sentence(1, true) ))?, Some(v) => v, }; let (attacking_type, attacking_code) = match who .active_combat .as_ref() .and_then(|ac| ac.attacking.as_ref()) .and_then(|a| a.split_once("/")) { None => user_error("Calm down satan, you're not currently attacking anyone!".to_owned())?, Some(v) => v, }; let pow_delay: i64 = 30; match who .tactic_use .last_pow .map(|lp| (lp + TimeDelta::try_seconds(pow_delay).unwrap()) - Utc::now()) { None => {} Some(d) if d < TimeDelta::try_seconds(0).unwrap() || who.flags.contains(&ItemFlag::Invincible) => {} Some(d) => user_error(format!( "You can't powerattack again for another {} seconds.", d.num_seconds() ))?, } let to_whom = match ctx .trans .find_item_by_type_code(attacking_type, attacking_code) .await? { None => user_error("They seem to be gone!".to_owned())?, Some(v) => v, }; let msg = pow_att.start_message(who, &to_whom) + ".\n"; broadcast_to_room(ctx.trans, &who.location, None, &msg).await?; let mut who_mut = (**who).clone(); who_mut.active_combat.as_mut().map(|ac| { ac.attack_mode = AttackMode::POWER; }); who_mut.tactic_use.last_pow = Some(Utc::now()); ctx.trans.save_item_model(&who_mut).await?; ctx.trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", &who.item_type, &who.item_code), next_scheduled: Utc::now() + chrono::TimeDelta::try_milliseconds( attack_speed(&who_mut).as_millis() as i64 ) .unwrap(), ..Default::default() }, details: TaskDetails::AttackTick, }) .await?; Ok(()) } pub async fn switch_to_feint(ctx: &VerbContext<'_>, who: &Arc) -> UResult<()> { let (attacking_type, attacking_code) = match who .active_combat .as_ref() .and_then(|ac| ac.attacking.as_ref()) .and_then(|a| a.split_once("/")) { None => user_error("Calm down satan, you're not currently attacking anyone!".to_owned())?, Some(v) => v, }; let feint_delay: i64 = 30; match who .tactic_use .last_feint .map(|lp| (lp + TimeDelta::try_seconds(feint_delay).unwrap()) - Utc::now()) { None => {} Some(d) if d < TimeDelta::try_seconds(0).unwrap() => {} Some(d) => user_error(format!( "You can't feint again for another {} seconds.", d.num_seconds() ))?, } let to_whom = match ctx .trans .find_item_by_type_code(attacking_type, attacking_code) .await? { None => user_error("They seem to be gone!".to_owned())?, Some(v) => v, }; if to_whom.species != SpeciesType::Human && to_whom.species != SpeciesType::Robot { user_error(format!( "You don't think {} will pay any attention to a feint.", to_whom.display_for_sentence(1, false) ))?; } let feints = vec![ |p: &str, v: &str| { format!( "{} flicks sand in {}'s eyes and shouts \"pocket sand\".\n", &p, &v ) }, |p: &str, v: &str| { format!( "{} waves a piece of string at {} and asks \"how long is a piece of string?\".\n", &p, &v ) }, |p: &str, v: &str| format!("{} points out the futility of violence to {}.\n", &p, &v), |p: &str, v: &str| format!("{} references {}'s mother in a pejorative way.\n", &p, &v), |p: &str, v: &str| { format!( "{} asks if {} suffers from pneumonoultramicroscopicsilicovolcanoconiosis.\n", &p, &v ) }, |p: &str, v: &str| { format!( "{} notes that consciousness is an illusion, so {} might as well not bother.\n", &p, &v ) }, ]; let feint = feints.iter().choose(&mut thread_rng()).unwrap(); let msg = feint( &who.display_for_sentence(1, true), &to_whom.display_for_sentence(1, false), ); broadcast_to_room(ctx.trans, &who.location, None, &msg).await?; let mut who_mut = (**who).clone(); who_mut.active_combat.as_mut().map(|ac| { ac.attack_mode = AttackMode::FEINT; }); who_mut.tactic_use.last_feint = Some(Utc::now()); ctx.trans.save_item_model(&who_mut).await?; ctx.trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", &who.item_type, &who.item_code), next_scheduled: Utc::now() + chrono::TimeDelta::try_milliseconds( attack_speed(&who_mut).as_millis() as i64 ) .unwrap(), ..Default::default() }, details: TaskDetails::AttackTick, }) .await?; Ok(()) } 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, }; destroy_container(ctx.trans, &corpse).await?; let msg = format!( "{} rots away to nothing.\n", corpse.display_for_sentence(1, true) ); broadcast_to_room(ctx.trans, &corpse.location, None, &msg).await?; Ok(None) } } pub static ROT_CORPSE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &RotCorpseTaskHandler; #[cfg(test)] mod tests { use crate::{models::effect::EffectType, services::effect::default_effects_for_type}; #[test] fn default_for_stunned() { assert_eq!( default_effects_for_type() .get(&EffectType::Stunned) .is_some(), true ) } }