blastmud/blastmud_game/src/static_content/npc.rs

647 lines
22 KiB
Rust

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 serde::{Deserialize, Serialize};
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.
}
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
pub enum NPCPronounType {
Inanimate,
Neutral,
Male,
Female,
}
pub fn npc_pronoun_type_to_pronouns(t: &NPCPronounType) -> Pronouns {
use NPCPronounType::*;
match t {
Inanimate => Pronouns::default_inanimate(),
Neutral => Pronouns::default_animate(),
Male => Pronouns::default_male(),
Female => Pronouns::default_female(),
}
}
pub struct NPC {
pub code: String,
pub name: String,
pub pronouns: Pronouns,
pub description: String,
pub spawn_location: String,
pub spawn_possessions: Vec<NPCSpawnPossession>,
pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>,
pub aliases: Vec<String>,
pub says: Vec<NPCSayInfo>,
pub aggression: u64, // 1-20 sets aggro time, >= 21 = instant aggro.
pub aggro_pc_only: bool,
pub max_health: u64,
pub intrinsic_weapon: Option<PossessionType>,
pub total_xp: u64,
pub total_skills: BTreeMap<SkillType, f64>,
pub total_stats: BTreeMap<StatType, f64>,
pub species: SpeciesType,
pub wander_zones: Vec<String>,
pub kill_bonus: Option<KillBonus>,
pub player_consents: Vec<ConsentType>,
pub hire_data: Option<HireData>,
pub extra_flags: Vec<ItemFlag>,
pub has_urges: bool,
}
impl Default for NPC {
fn default() -> Self {
Self {
code: "DEFAULT".to_owned(),
name: "default".to_owned(),
pronouns: Pronouns::default_animate(),
description: "default".to_owned(),
spawn_location: "default".to_owned(),
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<NPC> {
static NPC_LIST: OnceCell<Vec<NPC>> = OnceCell::new();
NPC_LIST.get_or_init(|| {
let mut npcs = vec![NPC {
code: "repro_xv_chargen_statbot".to_owned(),
name: "Statbot".to_owned(),
description:
"A silvery shiny metal mechanical being. It lets out a whirring sound as it moves."
.to_owned(),
spawn_location: "room/repro_xv_chargen".to_owned(),
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<BTreeMap<&'static str, &'static NPC>> = OnceCell::new();
NPC_CODE_MAP.get_or_init(|| {
npc_list()
.iter()
.map(|npc| (npc.code.as_str(), 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<BTreeMap<(&'static str, &'static str), &'static NPCSayInfo>> =
OnceCell::new();
NPC_SAYINFO_MAP.get_or_init(|| {
npc_list()
.iter()
.flat_map(|npc| {
npc.says
.iter()
.map(|says| ((npc.code.as_str(), says.say_code), says))
})
.collect()
})
}
pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
Box::new(npc_list().iter().map(|c| {
StaticItem {
item_code: c.code.clone(),
initial_item: Box::new(|| {
let mut flags: Vec<ItemFlag> = 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::<Vec<String>>(),
..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<dyn Iterator<Item = StaticTask>> {
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<dyn Iterator<Item = StaticTask>> {
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<dyn Iterator<Item = StaticTask>> {
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<Option<time::Duration>> {
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<Option<time::Duration>> {
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<Option<time::Duration>> {
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<Option<time::Duration>> {
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;