use super::{ possession_type::PossessionType, room::{resolve_exit, room_map_by_code}, species::SpeciesType, StaticItem, StaticTask, }; #[double] use crate::db::DBTrans; use crate::{ message_handler::{ user_commands::{say::say_to_room, CommandHandlingError, UResult, VerbContext}, ListenerSession, }, models::{ consent::ConsentType, item::{Item, ItemFlag, LocationActionType, Pronouns, SkillType, StatType}, task::{Task, TaskDetails, TaskMeta, TaskRecurrence}, }, regular_tasks::{ queued_command::{queue_command_for_npc_and_save, MovementSource, QueueCommand}, TaskHandler, TaskRunContext, }, services::combat::{corpsify_item, start_attack, start_attack_mut}, DResult, }; use async_trait::async_trait; use chrono::Utc; use log::info; use mockall_double::double; use once_cell::sync::OnceCell; use rand::{prelude::*, thread_rng, Rng}; use std::collections::BTreeMap; use std::time; use uuid::Uuid; pub mod computer_museum_npcs; mod melbs_citizen; mod melbs_dog; mod roboporter; pub mod statbot; #[async_trait] pub trait NPCMessageHandler { async fn handle( self: &Self, ctx: &mut VerbContext, source: &Item, target: &Item, message: &str, ) -> UResult<()>; } #[derive(Clone, Debug)] pub enum NPCSayType { // Bool is true if it should be filtered for less-explicit. FromFixedList(Vec<(bool, &'static str)>), } #[derive(Clone, Debug)] pub struct NPCSayInfo { pub say_code: &'static str, pub frequency_secs: u64, pub talk_type: NPCSayType, } pub struct KillBonus { pub msg: &'static str, pub payment: u64, } #[async_trait] pub trait HireHandler { async fn hire_handler( &self, trans: &DBTrans, session: &ListenerSession, hirer: &Item, target: &mut Item, ) -> UResult<()>; async fn fire_handler(&self, trans: &DBTrans, firer: &Item, target: &mut Item) -> DResult<()>; } pub struct HireData { pub price: u64, pub frequency_secs: u64, pub handler: &'static (dyn HireHandler + Sync + Send), } pub struct NPCSpawnPossession { what: PossessionType, action_type: LocationActionType, wear_layer: i64, // 0 is on top, higher under. } pub struct NPC { pub code: &'static str, pub name: &'static str, pub pronouns: Pronouns, pub description: &'static str, pub spawn_location: &'static str, pub spawn_possessions: Vec, pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, pub aliases: Vec<&'static str>, pub says: Vec, pub aggression: u64, // 1-20 sets aggro time, >= 21 = instant aggro. pub aggro_pc_only: bool, pub max_health: u64, pub intrinsic_weapon: Option, pub total_xp: u64, pub total_skills: BTreeMap, pub total_stats: BTreeMap, pub species: SpeciesType, pub wander_zones: Vec<&'static str>, pub kill_bonus: Option, pub player_consents: Vec, pub hire_data: Option, pub extra_flags: Vec, pub has_urges: bool, } impl Default for NPC { fn default() -> Self { Self { code: "DEFAULT", name: "default", pronouns: Pronouns::default_animate(), description: "default", spawn_location: "default", spawn_possessions: vec![], message_handler: None, aliases: vec![], says: vec![], total_xp: 1000, total_skills: SkillType::values() .into_iter() .map(|sk| { ( sk.clone(), if &sk == &SkillType::Dodge { 8.0 } else { 10.0 }, ) }) .collect(), total_stats: vec![].into_iter().collect(), aggression: 0, aggro_pc_only: false, max_health: 24, intrinsic_weapon: None, species: SpeciesType::Human, wander_zones: vec![], kill_bonus: None, player_consents: vec![], hire_data: None, extra_flags: vec![], has_urges: false, } } } pub fn npc_list() -> &'static Vec { static NPC_LIST: OnceCell> = OnceCell::new(); NPC_LIST.get_or_init(|| { let mut npcs = vec![NPC { code: "repro_xv_chargen_statbot", name: "Statbot", description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.", spawn_location: "room/repro_xv_chargen", message_handler: Some(&statbot::StatbotMessageHandler), says: vec![], ..Default::default() }]; npcs.append(&mut melbs_citizen::npc_list()); npcs.append(&mut melbs_dog::npc_list()); npcs.append(&mut roboporter::npc_list()); npcs.append(&mut computer_museum_npcs::npc_list()); npcs }) } pub fn npc_by_code() -> &'static BTreeMap<&'static str, &'static NPC> { static NPC_CODE_MAP: OnceCell> = OnceCell::new(); NPC_CODE_MAP.get_or_init(|| npc_list().iter().map(|npc| (npc.code, npc)).collect()) } pub fn npc_say_info_by_npc_code_say_code( ) -> &'static BTreeMap<(&'static str, &'static str), &'static NPCSayInfo> { static NPC_SAYINFO_MAP: OnceCell> = OnceCell::new(); NPC_SAYINFO_MAP.get_or_init(|| { npc_list() .iter() .flat_map(|npc| { npc.says .iter() .map(|says| ((npc.code, says.say_code), says)) }) .collect() }) } pub fn npc_static_items() -> Box> { Box::new(npc_list().iter().map(|c| { StaticItem { item_code: c.code, initial_item: Box::new(|| { let mut flags: Vec = c.extra_flags.clone(); if c.hire_data.is_some() { flags.push(ItemFlag::Hireable); // Revise if we ever want NPCs to attack for-hire NPCs. flags.push(ItemFlag::NPCsDontAttack); } Item { item_code: c.code.to_owned(), item_type: "npc".to_owned(), display: c.name.to_owned(), details: Some(c.description.to_owned()), location: c.spawn_location.to_owned(), is_static: true, pronouns: c.pronouns.clone(), total_xp: c.total_xp.clone(), total_skills: c.total_skills.clone(), total_stats: c.total_stats.clone(), species: c.species.clone(), health: c.max_health.clone(), flags, aliases: c .aliases .iter() .map(|a| (*a).to_owned()) .collect::>(), ..Item::default() } }), extra_items_on_create: Box::new(|npc_item| { let npc_ref = npc_item.refstr(); Box::new(c.spawn_possessions.iter().map(move |spawn_item| { let mut pos_it: Item = spawn_item.what.clone().into(); pos_it.location = npc_ref.clone(); pos_it.action_type = spawn_item.action_type.clone(); pos_it.action_type_started = Some(Utc::now() - chrono::Duration::seconds(spawn_item.wear_layer)); pos_it })) }), } })) } pub fn npc_say_tasks() -> Box> { Box::new(npc_list().iter().flat_map(|c| { c.says.iter().map(|say| StaticTask { task_code: c.code.to_owned() + "_" + say.say_code, initial_task: Box::new(|| { let mut rng = thread_rng(); Task { meta: TaskMeta { task_code: c.code.to_owned() + "_" + say.say_code, is_static: true, recurrence: Some(TaskRecurrence::FixedDuration { seconds: say.frequency_secs as u32, }), next_scheduled: Utc::now() + chrono::Duration::seconds( rng.gen_range(0..say.frequency_secs) as i64 ), ..TaskMeta::default() }, details: TaskDetails::NPCSay { npc_code: c.code.to_owned(), say_code: say.say_code.to_owned(), }, } }), }) })) } pub fn npc_wander_tasks() -> Box> { Box::new( npc_list() .iter() .filter(|c| !c.wander_zones.is_empty()) .map(|c| StaticTask { task_code: c.code.to_owned(), initial_task: Box::new(|| { let mut rng = thread_rng(); Task { meta: TaskMeta { task_code: c.code.to_owned(), is_static: true, recurrence: Some(TaskRecurrence::FixedDuration { seconds: rng.gen_range(250..350) as u32, }), next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..300) as i64), ..TaskMeta::default() }, details: TaskDetails::NPCWander { npc_code: c.code.to_owned(), }, } }), }), ) } const NONINSTANT_AGGRESSION: u64 = 20; pub fn npc_aggro_tasks() -> Box> { Box::new( npc_list() .iter() .filter(|c| c.aggression != 0) .map(|c| StaticTask { task_code: c.code.to_owned(), initial_task: Box::new(|| { let mut rng = thread_rng(); let aggro_time = (rng.gen_range(450..550) as u64) / c.aggression.min(NONINSTANT_AGGRESSION); Task { meta: TaskMeta { task_code: c.code.to_owned(), is_static: true, recurrence: Some(TaskRecurrence::FixedDuration { seconds: aggro_time as u32, }), next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..aggro_time) as i64), ..TaskMeta::default() }, details: TaskDetails::NPCAggro { npc_code: c.code.to_owned(), }, } }), }), ) } pub struct NPCSayTaskHandler; #[async_trait] impl TaskHandler for NPCSayTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let (npc_code, say_code) = match &ctx.task.details { TaskDetails::NPCSay { npc_code, say_code } => (npc_code.clone(), say_code.clone()), _ => Err("Expected NPC say task to be NPCSay type")?, }; let say_info = match npc_say_info_by_npc_code_say_code().get(&(&npc_code, &say_code)) { None => { info!( "NPCSayTaskHandler can't find NPCSayInfo for npc {} say_code {}", npc_code, say_code ); return Ok(None); } Some(r) => r, }; let npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { info!("NPCSayTaskHandler can't find NPC {}", npc_code); return Ok(None); } Some(r) => r, }; if npc_item.death_data.is_some() { return Ok(None); } let (is_explicit, say_what) = match &say_info.talk_type { NPCSayType::FromFixedList(l) => { let mut rng = thread_rng(); match l[..].choose(&mut rng) { None => { info!( "NPCSayTaskHandler NPCSayInfo for npc {} say_code {} has no choices", npc_code, say_code ); return Ok(None); } Some(r) => r.clone(), } } }; match say_to_room( ctx.trans, &npc_item, &npc_item.location, say_what, is_explicit, ) .await { Ok(()) => {} Err(CommandHandlingError::UserError(e)) => { info!( "NPCSayHandler couldn't send for npc {} say_code {}: {}", npc_code, say_code, e ); } Err(CommandHandlingError::SystemError(e)) => Err(e)?, } Ok(None) } } pub static SAY_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCSayTaskHandler; pub struct NPCWanderTaskHandler; #[async_trait] impl TaskHandler for NPCWanderTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::NPCWander { npc_code } => npc_code.clone(), _ => Err("Expected NPCWander type")?, }; let npc = match npc_by_code().get(npc_code.as_str()) { None => { info!( "NPC {} is gone / not yet in static items, ignoring in wander handler", &npc_code ); return Ok(None); } Some(r) => r, }; let item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { info!( "NPC {} is gone / not yet in DB, ignoring in wander handler", &npc_code ); return Ok(None); } Some(r) => r, }; if item.death_data.is_some() { return Ok(None); } let (ltype, lcode) = match item.location.split_once("/") { None => return Ok(None), Some(r) => r, }; if ltype != "room" { let mut new_item = (*item).clone(); new_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&new_item).await?; return Ok(None); } let room = match room_map_by_code().get(lcode) { None => { let mut new_item = (*item).clone(); new_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&new_item).await?; return Ok(None); } Some(r) => r, }; if !item.queue.is_empty() { return Ok(None); } let ex_iter = room.exits.iter().filter(|ex| { resolve_exit(room, ex) .map(|new_room| npc.wander_zones.contains(&new_room.zone) && !new_room.repel_npc) .unwrap_or(false) }); let dir_opt = ex_iter .choose(&mut thread_rng()) .map(|ex| ex.direction.clone()) .clone(); if let Some(dir) = dir_opt { queue_command_for_npc_and_save( &ctx.trans, &item, &QueueCommand::Movement { direction: dir.clone(), source: MovementSource::Command { event_id: Uuid::new_v4(), }, }, ) .await?; } Ok(None) } } pub static WANDER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCWanderTaskHandler; pub struct NPCAggroTaskHandler; #[async_trait] impl TaskHandler for NPCAggroTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::NPCAggro { npc_code } => npc_code.clone(), _ => Err("Expected NPCAggro type")?, }; let item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { info!( "NPC {} is gone / not yet in DB, ignoring in aggro handler", &npc_code ); return Ok(None); } Some(r) => r, }; let npc = match npc_by_code().get(npc_code.as_str()) { None => return Ok(None), Some(r) => r, }; if item.death_data.is_some() || item .active_combat .as_ref() .map(|ac| ac.attacking.is_some()) .unwrap_or(false) { return Ok(None); } let items_loc = ctx.trans.find_items_by_location(&item.location).await?; let vic_opt = items_loc .iter() .filter(|it| { (it.item_type == "player" || (!npc.aggro_pc_only && it.item_type == "npc")) && it.death_data.is_none() && !it.flags.contains(&ItemFlag::NPCsDontAttack) && it.active_climb.is_none() && (it.item_type != item.item_type || it.item_code != item.item_code) }) .choose(&mut thread_rng()); if let Some(victim) = vic_opt { match start_attack(ctx.trans, &item, victim).await { Ok(()) | Err(CommandHandlingError::UserError(_)) => {} Err(CommandHandlingError::SystemError(e)) => Err(e)?, } } Ok(None) } } pub static AGGRO_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCAggroTaskHandler; pub async fn check_for_instant_aggro(trans: &DBTrans, player_item: &mut Item) -> DResult<()> { for item in &trans.find_items_by_location(&player_item.location).await? { if item.item_type == player_item.item_type && item.item_code == player_item.item_code { continue; } if item.item_type != "npc" { continue; } let npc = match npc_by_code().get(item.item_code.as_str()) { None => continue, Some(r) => r, }; if npc.aggression <= NONINSTANT_AGGRESSION { continue; } if npc.aggro_pc_only && player_item.item_type != "player" { continue; } if item.death_data.is_some() || item .active_combat .as_ref() .map(|ac| ac.attacking.is_some()) .unwrap_or(false) { continue; } let mut item_mut: Item = (**item).clone(); match start_attack_mut(trans, &mut item_mut, player_item).await { Ok(()) | Err(CommandHandlingError::UserError(_)) => {} Err(CommandHandlingError::SystemError(e)) => Err(e)?, } trans.save_item_model(&item_mut).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 = npc.max_health; npc_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&npc_item).await?; for spawn_item in &npc.spawn_possessions { let mut item: Item = spawn_item.what.clone().into(); item.location = npc_item.refstr(); item.item_code = format!("{}", ctx.trans.alloc_item_code().await?); item.action_type = spawn_item.action_type.clone(); ctx.trans.create_item(&item).await?; } return Ok(None); } } pub static RECLONE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCRecloneTaskHandler;