forked from blasthavers/blastmud
Also fix some minor combat bugs found, including the refocusing bug, and the dead NPCs talking (mk II) bug.
435 lines
16 KiB
Rust
435 lines
16 KiB
Rust
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<NPCSayInfo>,
|
|
pub aggression: u64,
|
|
pub max_health: u64,
|
|
pub intrinsic_weapon: Option<PossessionType>,
|
|
pub total_xp: u64,
|
|
pub total_skills: BTreeMap<SkillType, f64>,
|
|
pub species: SpeciesType,
|
|
pub wander_zones: Vec<&'static str>,
|
|
pub kill_bonus: Option<KillBonus>,
|
|
pub player_consents: Vec<ConsentType>,
|
|
}
|
|
|
|
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<NPC> {
|
|
static NPC_LIST: OnceCell<Vec<NPC>> = 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<BTreeMap<&'static str, &'static NPC>> = 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<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, 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,
|
|
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::<Vec<String>>(),
|
|
..Item::default()
|
|
})
|
|
}))
|
|
}
|
|
|
|
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(),
|
|
},
|
|
}
|
|
})
|
|
}))
|
|
}
|
|
|
|
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;
|
|
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
|
|
};
|
|
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, &mut 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<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
|
|
};
|
|
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" || it.item_type == "npc") &&
|
|
it.death_data.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 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?;
|
|
return Ok(None);
|
|
}
|
|
}
|
|
pub static RECLONE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCRecloneTaskHandler;
|