blastmud/blastmud_game/src/static_content/npc.rs

420 lines
15 KiB
Rust
Raw Normal View History

2023-01-22 22:43:44 +11:00
use super::{
StaticItem,
StaticTask,
possession_type::PossessionType,
species::SpeciesType,
room::{
room_map_by_code,
resolve_exit
}
2023-01-22 22:43:44 +11:00
};
use crate::models::{
2023-01-22 01:16:00 +11:00
item::{Item, Pronouns, SkillType},
task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
};
2023-01-23 22:52:01 +11:00
use crate::services::{
combat::{
max_health,
corpsify_item,
start_attack,
2023-01-23 22:52:01 +11:00
}
};
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;
2023-01-11 22:51:17 +11:00
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 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)>,
2023-01-20 23:38:57 +11:00
pub aliases: Vec<&'static str>,
2023-01-15 17:30:23 +11:00
pub says: Vec<NPCSayInfo>,
2023-01-22 01:16:00 +11:00
pub attackable: bool,
pub aggression: u64,
2023-01-22 01:16:00 +11:00
pub intrinsic_weapon: Option<PossessionType>,
pub total_xp: u64,
2023-01-22 01:16:00 +11:00
pub total_skills: BTreeMap<SkillType, f64>,
2023-01-22 22:43:44 +11:00
pub species: SpeciesType,
pub wander_zones: Vec<&'static str>
}
2023-01-11 22:51:17 +11:00
impl Default for NPC {
fn default() -> Self {
Self {
code: "DEFAULT",
name: "default",
pronouns: Pronouns::default_animate(),
2023-01-11 22:51:17 +11:00
description: "default",
spawn_location: "default",
message_handler: None,
2023-01-20 23:38:57 +11:00
aliases: vec!(),
2023-01-15 17:30:23 +11:00
says: vec!(),
total_xp: 1000,
total_skills: SkillType::values().into_iter().map(|sk| (sk, 10.0)).collect(),
2023-01-22 01:16:00 +11:00
attackable: false,
aggression: 0,
2023-01-22 22:43:44 +11:00
intrinsic_weapon: None,
species: SpeciesType::Human,
wander_zones: vec!()
2023-01-11 22:51:17 +11:00
}
}
}
pub fn npc_list() -> &'static Vec<NPC> {
static NPC_LIST: OnceCell<Vec<NPC>> = OnceCell::new();
NPC_LIST.get_or_init(
2023-01-11 22:51:17 +11:00
|| {
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),
2023-01-11 22:51:17 +11:00
says: vec!(),
..Default::default()
},
2023-01-11 22:51:17 +11:00
);
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(),
2023-01-15 17:30:23 +11:00
is_challenge_attack_only: !c.attackable,
2023-01-22 01:16:00 +11:00
total_skills: c.total_skills.clone(),
species: c.species.clone(),
2023-01-20 23:38:57 +11:00
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(100..150) as u32 }),
next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..30) 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(100..150) 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
};
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;
2023-01-23 22:52:01 +11:00
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.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<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.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;
2023-01-23 22:52:01 +11:00
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.is_dead {
return Ok(None);
}
corpsify_item(ctx.trans, &npc_item).await?;
npc_item.is_dead = false;
npc_item.health = max_health(&npc_item);
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;