use super::{ StaticItem, StaticTask, possession_type::PossessionType, species::SpeciesType, room::{ room_map_by_code, resolve_exit } }; use crate::models::{ item::{Item, Pronouns, SkillType}, task::{Task, TaskMeta, TaskRecurrence, TaskDetails}, consent::{ConsentType}, }; use crate::services::{ combat::{ corpsify_item, start_attack, } }; use once_cell::sync::OnceCell; use std::collections::BTreeMap; use crate::message_handler::user_commands::{ VerbContext, UResult, CommandHandlingError, say::say_to_room, movement::attempt_move_immediate, }; use crate::DResult; use async_trait::async_trait; use chrono::Utc; use rand::{thread_rng, Rng, prelude::*}; use crate::regular_tasks::{TaskHandler, TaskRunContext}; use log::info; use std::time; pub mod statbot; mod melbs_citizen; mod melbs_dog; #[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, } pub struct NPC { pub code: &'static str, pub name: &'static str, pub pronouns: Pronouns, pub description: &'static str, pub spawn_location: &'static str, pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, pub aliases: Vec<&'static str>, pub says: Vec, pub aggression: u64, pub max_health: u64, pub intrinsic_weapon: Option, pub total_xp: u64, pub total_skills: BTreeMap, pub species: SpeciesType, pub wander_zones: Vec<&'static str>, pub kill_bonus: Option, pub player_consents: Vec, } impl Default for NPC { fn default() -> Self { Self { code: "DEFAULT", name: "default", pronouns: Pronouns::default_animate(), description: "default", spawn_location: "default", 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(), aggression: 0, max_health: 24, intrinsic_weapon: None, species: SpeciesType::Human, wander_zones: vec!(), kill_bonus: None, player_consents: vec!(), } } } 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 }) } 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(|| 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(), species: c.species.clone(), health: c.max_health.clone(), aliases: c.aliases.iter().map(|a| (*a).to_owned()).collect::>(), ..Item::default() }) })) } 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(), }, } }) })) } 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; 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.is_dead { 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.is_dead { 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 }; 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 { match attempt_move_immediate(ctx.trans, &item, &dir, None).await { Ok(()) | Err(CommandHandlingError::UserError(_)) => {}, Err(CommandHandlingError::SystemError(e)) => Err(e)? } } 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 }; if item.is_dead || 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" || it.item_type == "npc") && !it.is_dead && (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 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.is_dead { return Ok(None); } corpsify_item(ctx.trans, &npc_item).await?; npc_item.is_dead = false; npc_item.health = npc.max_health; npc_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&npc_item).await?; return Ok(None); } } pub static RECLONE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCRecloneTaskHandler;