Allow hiring NPCs (roboporters) to carry heavy stuff!

Note that I still have to implement carrying weight calculation, so it
isn't yet as useful as it will be eventually!
This commit is contained in:
Condorra 2023-07-06 22:34:01 +10:00
parent 4b524fda96
commit e83cc19698
15 changed files with 4413 additions and 3694 deletions

View File

@ -822,6 +822,15 @@ impl DBTrans {
query = query[6..].trim();
dead_only = true;
}
if query.ends_with("ies") {
query = &query[0..(query.len() - 3)];
} else if query.ends_with("es") {
query = &query[0..(query.len() - 2)];
} else if query.ends_with("s") {
query = &query[0..(query.len() - 1)];
}
if dead_only {
extra_where.push_str(" AND COALESCE(details->>'death_data' IS NOT NULL, false) = true");
} else if search.dead_first {
@ -1597,6 +1606,30 @@ impl DBTrans {
.collect())
}
pub async fn count_staff_by_hirer<'a>(self: &'a Self, hirer: &str) -> DResult<i64> {
Ok(self
.pg_trans()?
.query_one(
"SELECT COUNT(*) FROM items WHERE details->'special_data'->'HireData'->>'hired_by' = $1",
&[&hirer],
)
.await?
.get(0))
}
pub async fn find_staff_by_hirer<'a>(self: &'a Self, hirer: &str) -> DResult<Vec<Arc<Item>>> {
Ok(self
.pg_trans()?
.query(
"SELECT details FROM items WHERE details->'special_data'->'HireData'->>'hired_by' = $1",
&[&hirer],
)
.await?
.into_iter()
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
.map(Arc::new)
.collect())
}
pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
if let Some(trans) = trans_opt {

View File

@ -26,16 +26,19 @@ pub mod cut;
pub mod delete;
mod describe;
pub mod drop;
mod fire;
pub mod follow;
mod gear;
pub mod get;
mod help;
pub mod hire;
mod ignore;
pub mod improvise;
mod install;
mod inventory;
mod less_explicit_mode;
mod list;
pub mod load;
mod login;
mod look;
mod map;
@ -148,12 +151,16 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"delete" => delete::VERB,
"drop" => drop::VERB,
"fire" => fire::VERB,
"follow" => follow::VERB,
"unfollow" => follow::VERB,
"gear" => gear::VERB,
"get" => get::VERB,
"hire" => hire::VERB,
"improv" => improvise::VERB,
"improvise" => improvise::VERB,
"improvize" => improvise::VERB,
@ -172,6 +179,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"list" => list::VERB,
"load" => load::VERB,
"unload" => load::VERB,
"lm" => map::VERB,
"lmap" => map::VERB,
"gm" => map::VERB,

View File

@ -127,7 +127,7 @@ impl TaskHandler for DestroyUserHandler {
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
destroy_container(&ctx.trans, &player_item).await?;
for dynzone in ctx.trans.find_dynzone_for_user(&username).await? {
recursively_destroy_or_move_item(ctx, &dynzone).await?;
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_user(&username).await?;

View File

@ -0,0 +1,75 @@
use super::{get_player_item_or_fail, UResult, UserError, UserVerb, UserVerbRef, VerbContext};
use crate::{
models::item::{Item, ItemSpecialData},
static_content::npc::npc_by_code,
};
use ansi::ansi;
use async_trait::async_trait;
use std::sync::Arc;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let npc_name = remaining.trim().to_lowercase();
let player_item = get_player_item_or_fail(ctx).await?;
let mut match_staff: Vec<Arc<Item>> = ctx
.trans
.find_staff_by_hirer(&player_item.item_code)
.await?
.into_iter()
.filter(|it| {
it.display.to_lowercase().starts_with(&npc_name)
|| it
.display_less_explicit
.as_ref()
.map(|v| v.to_lowercase().starts_with(&npc_name))
.unwrap_or(false)
|| it
.aliases
.iter()
.any(|al| al.to_lowercase().starts_with(&npc_name))
})
.collect();
match_staff.sort_by_key(|it| (it.display.len() as i64 - npc_name.len() as i64).abs());
let npc: Arc<Item> = match_staff.first().ok_or_else(|| UserError(ansi!("You don't have any matching employees. Try <bold>hire<reset> to list your staff.").to_owned()))?.clone();
let hire_dat = npc_by_code()
.get(npc.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
.ok_or_else(|| {
UserError(
"Sorry, I've forgotten how to fire them! Ask the game staff for help."
.to_owned(),
)
})?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"A hushed silence falls over the room as you fire {}.\n",
npc.display_for_session(&ctx.session_dat),
)),
)
.await?;
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.fire_handler(&ctx.trans, &player_item, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
ctx.trans.save_item_model(&npc_mut).await?;
ctx.trans.delete_task("ChargeWages", &npc.item_code).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -0,0 +1,245 @@
use super::{
get_player_item_or_fail, get_user_or_fail, get_user_or_fail_mut, search_items_for_user,
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::{
item::{Item, ItemFlag, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::npc::npc_by_code,
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use std::sync::Arc;
use std::time;
static RESIGNATION_NOTICE: &'static str = ansi!("You're a <red>terrible employer<reset>, you <bold>HAVEN'T PAID MY WAGES!<reset> Do you even have the credits? I don't work for free, so you can forget it! I quit, effective immediately!");
pub struct ChargeWagesTaskHandler;
#[async_trait]
impl TaskHandler for ChargeWagesTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let npc_code = match &mut ctx.task.details {
TaskDetails::ChargeWages { npc } => npc,
_ => Err("Expected ChargeWages type")?,
};
let hire_dat = match npc_by_code()
.get(npc_code.as_str())
.and_then(|npci| npci.hire_data.as_ref())
{
None => return Ok(None), // I guess it is free until fixed in the db?
Some(d) => d,
};
let npc: Arc<Item> = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? {
None => return Ok(None),
Some(d) => d,
};
let hired_by = match npc.special_data.as_ref() {
Some(ItemSpecialData::HireData {
hired_by: Some(hired_by),
}) => hired_by,
_ => return Ok(None),
};
let mut bill_user = match ctx.trans.find_by_username(&hired_by).await? {
None => {
// Happens if the user is deleted while hiring an NPC.
let mut npc_mut: Item = (*npc).clone();
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
// No player, so the NPC fires itself.
hire_dat
.handler
.fire_handler(&ctx.trans, &npc, &mut npc_mut)
.await?;
ctx.trans.save_item_model(&npc_mut).await?;
return Ok(None);
}
Some(user) => user,
};
let bill_player = ctx
.trans
.find_item_by_type_code("player", &hired_by)
.await?
.ok_or_else(|| "Player hiring NPC missing but user still there.")?;
let sess_and_dat = ctx.trans.find_session_for_player(&hired_by).await?;
if hire_dat.price > bill_user.credits {
if let Some((sess, sess_dat)) = sess_and_dat {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"<yellow>{} says: <reset><bold>\"{}\"<reset>\n",
&npc.display_for_session(&sess_dat),
RESIGNATION_NOTICE
)),
)
.await?;
}
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.fire_handler(&ctx.trans, &bill_player, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
ctx.trans.save_item_model(&npc_mut).await?;
return Ok(None);
}
bill_user.credits -= hire_dat.price;
ctx.trans.save_user_model(&bill_user).await?;
match sess_and_dat.as_ref() {
None => {},
Some((sess, sess_dat)) => ctx.trans.queue_for_session(
sess, Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${} for wages for {}.<reset>\n"),
&hire_dat.price, &npc.display_for_session(&sess_dat)
))
).await?
}
Ok(Some(time::Duration::from_secs(hire_dat.frequency_secs)))
}
}
pub static CHARGE_WAGES_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeWagesTaskHandler;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let npc_name = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
if npc_name == "" {
let staff = ctx
.trans
.find_staff_by_hirer(&player_item.item_code)
.await?;
let mut msg: String = String::new();
if staff.is_empty() {
msg.push_str("You aren't currently employing anyone.\n");
} else {
msg.push_str("You are currently employing:\n");
for emp in staff {
if let Some(hire_dat) = npc_by_code()
.get(emp.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
{
msg.push_str(&format!(
"* {} @ ${} / {}\n",
emp.display_for_session(&ctx.session_dat),
hire_dat.price,
humantime::format_duration(std::time::Duration::from_secs(
hire_dat.frequency_secs,
)),
));
}
}
}
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
return Ok(());
}
let to_hire_if_free = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
item_type_only: Some("npc"),
..ItemSearchParams::base(&player_item, npc_name)
},
)
.await?;
let npc = to_hire_if_free
.into_iter()
.find(|it| {
it.flags.contains(&ItemFlag::Hireable)
&& match it.special_data {
Some(ItemSpecialData::HireData { hired_by: Some(_) }) => false,
_ => true,
}
})
.ok_or_else(|| UserError("Nothing here is available for hire right now.".to_owned()))?;
let hire_dat = npc_by_code()
.get(npc.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
.ok_or_else(|| UserError("Sorry, I've forgotten how to hire that out!".to_owned()))?;
let user = get_user_or_fail(ctx)?;
if user.credits < hire_dat.price {
user_error(format!(
ansi!(
"<yellow>{} says: <reset><bold>\"You wouldn't be able to afford me.\"<reset>"
),
npc.display_for_session(&ctx.session_dat)
))?;
}
if ctx
.trans
.count_staff_by_hirer(&player_item.item_code)
.await?
>= 5
{
user_error(
"You don't think you could supervise that many employees at once!".to_owned(),
)?;
}
let mut user_mut = get_user_or_fail_mut(ctx)?;
user_mut.credits -= hire_dat.price;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You hire {}, and your wristpad beeps for a deduction of ${}.\n",
npc.display_for_session(&ctx.session_dat),
hire_dat.price
)),
)
.await?;
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.hire_handler(&ctx.trans, &ctx.session, &player_item, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData {
hired_by: Some(player_item.item_code.clone()),
});
ctx.trans.save_item_model(&npc_mut).await?;
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: npc.item_code.clone(),
is_static: false,
recurrence: None, // Managed by the handler.
next_scheduled: Utc::now() + Duration::seconds(hire_dat.frequency_secs as i64),
..Default::default()
},
details: TaskDetails::ChargeWages {
npc: npc.item_code.clone(),
},
})
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -0,0 +1,130 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, UResult, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::item::{ItemFlag, ItemSpecialData},
services::{
capacity::{check_item_capacity, CapacityLevel},
comms::broadcast_to_room,
},
};
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
verb: &str,
remaining: &str,
) -> UResult<()> {
let reverse: bool = verb == "unload";
let sep = if reverse { "from" } else { "onto" };
let (mut item_name, npc_name) = match remaining.split_once(sep) {
None => user_error(format!(
ansi!("I couldn't understand that. Try <bold>{}<reset> item {} npc"),
verb, sep
))?,
Some(v) => v,
};
let player_item = get_player_item_or_fail(ctx).await?;
let npc = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, npc_name.trim())
},
)
.await?;
if !npc.flags.contains(&ItemFlag::CanLoad) {
user_error(format!("You can't {} things {} that!", verb, sep))?
}
match npc.special_data.as_ref() {
Some(ItemSpecialData::HireData {
hired_by: Some(hired_by),
}) if hired_by == &player_item.item_code => {}
_ => user_error(format!(
"{} doesn't seem to be letting you do that. Try hiring {} first!",
npc.display_for_session(&ctx.session_dat),
npc.pronouns.object
))?,
}
let mut item_limit = Some(1);
if item_name == "all" || item_name.starts_with("all ") {
item_name = item_name[3..].trim();
item_limit = None;
} else if let (Some(n), remaining2) = parse_count(item_name) {
item_limit = Some(n);
item_name = remaining2;
}
let items = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: reverse,
include_loc_contents: !reverse,
item_type_only: Some("possession"),
limit: item_limit.unwrap_or(100),
..ItemSearchParams::base(&npc, item_name.trim())
},
)
.await?;
for item in items {
if reverse {
match check_item_capacity(&ctx.trans, &player_item.location, item.weight).await? {
CapacityLevel::AboveItemLimit => user_error(
"There is not enough space here to unload another item.".to_owned(),
)?,
_ => {}
}
} else {
match check_item_capacity(&ctx.trans, &npc.refstr(), item.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("There is not enough capacity to load that item.".to_owned())?
}
_ => {}
}
}
let mut item_mut = (*item).clone();
item_mut.location = if reverse {
npc.location.clone()
} else {
npc.refstr()
};
ctx.trans.save_item_model(&item_mut).await?;
broadcast_to_room(
&ctx.trans,
&npc.location,
None,
&format!(
"{} {}s {} {} {}\n",
&npc.display_for_sentence(true, 1, true),
verb,
&item.display_for_sentence(true, 1, false),
sep,
&npc.pronouns.intensive
),
Some(&format!(
"{} {}s {} {} {}\n",
&npc.display_for_sentence(false, 1, true),
verb,
&item.display_for_sentence(false, 1, false),
sep,
&npc.pronouns.intensive
)),
)
.await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -2,6 +2,8 @@ use super::{
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
item::{Item, ItemSpecialData},
@ -21,13 +23,11 @@ use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools;
use log::info;
use mockall_double::double;
use std::time;
#[async_recursion]
pub async fn recursively_destroy_or_move_item(
ctx: &mut TaskRunContext<'_>,
item: &Item,
) -> DResult<()> {
pub async fn recursively_destroy_or_move_item(trans: &DBTrans, item: &Item) -> DResult<()> {
let mut item_mut = item.clone();
match item.item_type.as_str() {
"npc" => {
@ -36,14 +36,14 @@ pub async fn recursively_destroy_or_move_item(
Some(r) => r,
};
item_mut.location = npc.spawn_location.to_owned();
ctx.trans.save_item_model(&item_mut).await?;
trans.save_item_model(&item_mut).await?;
return Ok(());
}
"player" => {
let session = ctx.trans.find_session_for_player(&item.item_code).await?;
let session = trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() {
Some((listener_sess, _)) => {
ctx.trans.queue_for_session(
trans.queue_for_session(
&listener_sess,
Some(ansi!("<red>The landlord barges in with a bunch of very big hired goons, who stuff you in a sack, while the landlord mumbles something about vacant possession.<reset> After what seems like an eternity being jostled along while stuffed in the sack, they dump you out into a room that seems to be some kind of homeless shelter, and beat a hasty retreat.\n"))
).await?;
@ -51,24 +51,22 @@ pub async fn recursively_destroy_or_move_item(
None => {}
}
item_mut.location = "room/melbs_homelessshelter".to_owned();
ctx.trans.save_item_model(&item_mut).await?;
trans.save_item_model(&item_mut).await?;
return Ok(());
}
_ => {}
}
ctx.trans
.delete_item(&item.item_type, &item.item_code)
.await?;
trans.delete_item(&item.item_type, &item.item_code).await?;
let loc = format!("{}/{}", &item.item_type, &item.item_code);
// It's paginated so we loop to get everything...
loop {
let result = ctx.trans.find_items_by_location(&loc).await?;
let result = trans.find_items_by_location(&loc).await?;
if result.is_empty() {
return Ok(());
}
for sub_item in result {
recursively_destroy_or_move_item(ctx, &sub_item).await?;
recursively_destroy_or_move_item(trans, &sub_item).await?;
}
}
}
@ -111,7 +109,7 @@ impl TaskHandler for ChargeRoomTaskHandler {
};
match vacate_after {
Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(ctx, &zone_item).await?;
recursively_destroy_or_move_item(&ctx.trans, &zone_item).await?;
return Ok(None);
}
_ => (),

View File

@ -84,22 +84,22 @@ impl QueueCommandHandler for QueueHandler {
"{} prepares to use {} {} on {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&ctx.item.pronouns.possessive,
&item.display_for_sentence(true, 1, false),
&item.display_for_sentence(true, 0, false),
&if is_self_use {
ctx.item.pronouns.intensive.clone()
} else {
ctx.item.display_for_sentence(true, 1, false)
target.display_for_sentence(true, 1, false)
}
);
let msg_nonexp = format!(
"{} prepares to use {} {} on {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&ctx.item.pronouns.possessive,
&item.display_for_sentence(false, 1, false),
&item.display_for_sentence(false, 0, false),
&if is_self_use {
ctx.item.pronouns.intensive.clone()
} else {
ctx.item.display_for_sentence(true, 1, false)
target.display_for_sentence(true, 1, false)
}
);
broadcast_to_room(

View File

@ -256,6 +256,9 @@ pub enum ItemFlag {
NoSeeContents,
DroppedItemsDontExpire,
PrivatePlace,
Hireable,
NPCsDontAttack,
CanLoad,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -299,6 +302,9 @@ pub enum ItemSpecialData {
zone_exit: Option<String>,
vacate_after: Option<DateTime<Utc>>,
},
HireData {
hired_by: Option<String>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]

View File

@ -49,6 +49,9 @@ pub enum TaskDetails {
DestroyUser {
username: String,
},
ChargeWages {
npc: String,
},
}
impl TaskDetails {
pub fn name(self: &Self) -> &'static str {
@ -66,6 +69,7 @@ impl TaskDetails {
ChargeRoom { .. } => "ChargeRoom",
SwingShut { .. } => "SwingShut",
DestroyUser { .. } => "DestroyUser",
ChargeWages { .. } => "ChargeWages",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
}
}

View File

@ -5,7 +5,7 @@ use crate::models::task::{TaskParse, TaskRecurrence};
use crate::{
db,
listener::{ListenerMap, ListenerSend},
message_handler::user_commands::{delete, drop, open, rent},
message_handler::user_commands::{delete, drop, hire, open, rent},
models::task::Task,
services::{combat, effect},
static_content::npc,
@ -54,6 +54,7 @@ fn task_handler_registry(
("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone()),
("SwingShut", open::SWING_SHUT_HANDLER.clone()),
("DestroyUser", delete::DESTROY_USER_HANDLER.clone()),
("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()),
]
.into_iter()
.collect()

View File

@ -4,13 +4,16 @@ use super::{
species::SpeciesType,
StaticItem, StaticTask,
};
#[double]
use crate::db::DBTrans;
use crate::{
message_handler::user_commands::{
say::say_to_room, CommandHandlingError, UResult, VerbContext,
message_handler::{
user_commands::{say::say_to_room, CommandHandlingError, UResult, VerbContext},
ListenerSession,
},
models::{
consent::ConsentType,
item::{Item, Pronouns, SkillType},
item::{Item, ItemFlag, Pronouns, SkillType},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
},
regular_tasks::{
@ -23,6 +26,7 @@ use crate::{
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;
@ -31,6 +35,7 @@ use uuid::Uuid;
mod melbs_citizen;
mod melbs_dog;
mod roboporter;
pub mod statbot;
#[async_trait]
@ -62,6 +67,24 @@ pub struct KillBonus {
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 NPC {
pub code: &'static str,
pub name: &'static str,
@ -80,6 +103,8 @@ pub struct NPC {
pub wander_zones: Vec<&'static str>,
pub kill_bonus: Option<KillBonus>,
pub player_consents: Vec<ConsentType>,
pub hire_data: Option<HireData>,
pub extra_flags: Vec<ItemFlag>,
}
impl Default for NPC {
@ -110,6 +135,8 @@ impl Default for NPC {
wander_zones: vec![],
kill_bonus: None,
player_consents: vec![],
hire_data: None,
extra_flags: vec![],
}
}
}
@ -129,6 +156,7 @@ pub fn npc_list() -> &'static Vec<NPC> {
}];
npcs.append(&mut melbs_citizen::npc_list());
npcs.append(&mut melbs_dog::npc_list());
npcs.append(&mut roboporter::npc_list());
npcs
})
}
@ -158,6 +186,12 @@ 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(|| {
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(),
@ -170,6 +204,7 @@ pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
total_skills: c.total_skills.clone(),
species: c.species.clone(),
health: c.max_health.clone(),
flags,
aliases: c
.aliases
.iter()
@ -452,6 +487,7 @@ impl TaskHandler for NPCAggroTaskHandler {
.filter(|it| {
(it.item_type == "player" || it.item_type == "npc")
&& it.death_data.is_none()
&& !it.flags.contains(&ItemFlag::NPCsDontAttack)
&& (it.item_type != item.item_type || it.item_code != item.item_code)
})
.choose(&mut thread_rng());

View File

@ -0,0 +1,141 @@
use super::{npc_by_code, HireData, HireHandler, NPC};
#[double]
use crate::db::DBTrans;
use crate::{
message_handler::{
user_commands::{rent::recursively_destroy_or_move_item, UResult},
ListenerSession,
},
models::item::{FollowData, FollowState, Item, ItemFlag, Pronouns},
services::comms::broadcast_to_room,
static_content::{possession_type::PossessionType, species::SpeciesType},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use mockall_double::double;
struct RoboporterHandler;
#[async_trait]
impl HireHandler for RoboporterHandler {
async fn hire_handler(
&self,
trans: &DBTrans,
session: &ListenerSession,
hirer: &Item,
target: &mut Item,
) -> UResult<()> {
target.following = Some(FollowData {
follow_whom: hirer.refstr(),
state: FollowState::Active,
});
trans.queue_for_session(session,
Some(ansi!("You hear a metalic voice croaking: \"Thanks for hiring a <blue>Roboporter<reset> brand transportation robot! Just <bold>load<reset> me up with heavy things you can't lift. I'll try to follow your every move as long as you keep me hired and paid. Simply <bold>unload<reset> me when you reach your destination. When you are done with me, just fire me! I'll unload everything if I can, or if not, for your convenience, I'll auto-burn it in my internal furnace, and then return home as fast as you can blink!\"\n"))).await?;
Ok(())
}
async fn fire_handler(&self, trans: &DBTrans, _firer: &Item, target: &mut Item) -> DResult<()> {
target.following = None;
let stats = trans.get_location_stats(&target.location).await?;
let mut remaining_space = if stats.total_count >= 50 {
0
} else {
50 - stats.total_count
};
let mut msg_exp = String::new();
let mut msg_nonexp = String::new();
let desc_exp = target.display_for_sentence(true, 1, true);
let desc_nonexp = target.display_for_sentence(false, 1, true);
for item in trans.find_items_by_location(&target.refstr()).await? {
if remaining_space > 0 {
msg_exp.push_str(&format!(
"{} unloads {} from {}\n",
&desc_exp,
&item.display_for_sentence(true, 1, false),
&target.pronouns.intensive
));
msg_nonexp.push_str(&format!(
"{} unloads {} from {}\n",
&desc_nonexp,
&item.display_for_sentence(false, 1, false),
&target.pronouns.intensive
));
let mut item_mut = (*item).clone();
item_mut.location = target.location.clone();
trans.save_item_model(&item_mut).await?;
remaining_space -= 1;
} else {
msg_exp.push_str(&format!("{} unloads {} - but since there isn't enough space to put it down, flicks it into the Roboporter's onboard furnace compartment!\n",
&desc_exp, &item.display_for_sentence(true, 1, false),
));
msg_nonexp.push_str(&format!("{} unloads {} - but since there isn't enough space to put it down, flicks it into the Roboporter's onboard furnace compartment!\n",
&desc_exp, &item.display_for_sentence(false, 1, false),
));
recursively_destroy_or_move_item(trans, &item).await?;
}
}
let old_location = target.location.clone();
if let Some(return_to) = npc_by_code()
.get(target.item_code.as_str())
.map(|npc| npc.spawn_location)
{
if return_to != &target.location {
target.location = return_to.to_owned();
msg_exp.push_str(
&format!("By some marvel of modern engineering, {} disappears in a puff of smoke and is gone.\n",
desc_exp));
msg_nonexp.push_str(
&format!("By some marvel of modern engineering, {} disappears in a puff of smoke and is gone.\n",
desc_nonexp));
}
}
broadcast_to_room(
trans,
&old_location,
None,
msg_exp.as_str(),
Some(msg_nonexp.as_str()),
)
.await?;
Ok(())
}
}
static ROBOPORTER_HANDLER: RoboporterHandler = RoboporterHandler;
macro_rules! roboporter {
($code:expr, $spawn: expr) => {
NPC {
code: concat!("roboporter_", $code),
name: concat!("Roboporter ", $code),
pronouns: Pronouns { is_proper: true, ..Pronouns::default_inanimate() },
description: "Standing at an imposing height of over 5 metres, and as wide as a truck, the Roboporter is a marvel of mechanical engineering. Its sturdy metallic frame is built for strength and endurance, with hydraulic joints that allow for precise movements. The body is covered in scuffs and scratches, evidence of its relentless hauling duties.\n\nEquipped with a plethora of massive storage compartments, the Roboporter was clearly designed to carry large and heavy loads with ease. Its reinforced chassis and powerful motors enable it to traverse all terrains, from rubble-strewn streets to treacherous wastelands. It seems to have hinges all over it so it can dynamically transform its shape to sqeeze through doors and haul cargo into indoor spaces too. The faint hum of its internal machinery is ever-present, a testament to its unwavering efficiency.\n\nThe Roboporter's front panel displays a digital interface, showcasing real-time diagnostics and its current operational status. A pair of bright LED lights function as its eyes, pulsating with a soft glow. It lacks any human-like features, emphasizing its purely functional design.\n\nDespite its lack of emotions, the Roboporter exudes an aura of dependability and reliability. It stands as a symbol of resilience in a world ravaged by chaos, ready to assist survivors in their quest for survival.",
aliases: vec!("roboporter"),
spawn_location: concat!("room/", $spawn),
intrinsic_weapon: Some(PossessionType::Fangs),
species: SpeciesType::Robot,
hire_data: Some(HireData {
handler: &ROBOPORTER_HANDLER,
frequency_secs: 600,
price: 100,
}),
extra_flags: vec![ItemFlag::CanLoad],
..Default::default()
}
}
}
pub fn npc_list() -> Vec<NPC> {
vec![
roboporter!("1", "melbs_roboporter_rentals"),
roboporter!("2", "melbs_roboporter_rentals"),
roboporter!("3", "melbs_roboporter_rentals"),
roboporter!("4", "melbs_roboporter_rentals"),
roboporter!("5", "melbs_roboporter_rentals"),
roboporter!("6", "melbs_roboporter_rentals"),
roboporter!("7", "melbs_roboporter_rentals"),
roboporter!("8", "melbs_roboporter_rentals"),
roboporter!("9", "melbs_roboporter_rentals"),
roboporter!("10", "melbs_roboporter_rentals"),
]
}

View File

@ -373,10 +373,33 @@ pub fn room_list() -> Vec<Room> {
direction: Direction::SOUTH,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_roboporter_rentals",
name: "Roboporter Rentals",
short: ansi!("<cyan>RP<reset>"),
description: ansi!("A spacious room with metal walls and a faint smell of oil. The room is filled with rows of charging stations and maintenance bays for massive robots designed to carry heavy things. At the centre of the room stands a counter where you can rent Roboporters. A large sign overhead reads 'Roboporter Rentals - Your Heavy Lifting Solution!'. [You can use the <bold>hire roboporter<reset> command, to hire a Roboporter (if any are available) for $100 / 10 minute block]"),
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 8, z: 0 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
),
stock_list: vec!(),
should_caption: true,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),

View File

@ -1,24 +1,24 @@
use crate::{models::item::Sex, static_content::possession_type::PossessionType};
use once_cell::sync::OnceCell;
use serde::{Serialize, Deserialize};
use std::collections::BTreeMap;
use crate::{
models::item::Sex,
static_content::possession_type::PossessionType,
};
use rand::seq::IteratorRandom;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Eq, Ord, Clone, PartialEq, PartialOrd, Debug)]
pub enum SpeciesType {
Human,
Dog
Dog,
Robot,
}
impl SpeciesType {
pub fn sample_body_part(&self) -> BodyPart {
let mut rng = rand::thread_rng();
species_info_map().get(&self)
species_info_map()
.get(&self)
.and_then(|sp| sp.body_parts.iter().choose(&mut rng))
.unwrap_or(&BodyPart::Head).clone()
.unwrap_or(&BodyPart::Head)
.clone()
}
}
@ -32,7 +32,7 @@ pub enum BodyPart {
Arms,
Hands,
Legs,
Feet
Feet,
}
impl BodyPart {
@ -49,12 +49,12 @@ impl BodyPart {
Groin => match sex {
Some(Sex::Male) => "penis",
Some(Sex::Female) => "vagina",
_ => "groin"
_ => "groin",
},
Arms => "arms",
Hands => "hands",
Legs => "legs",
Feet => "feet"
Feet => "feet",
}
}
@ -72,7 +72,7 @@ impl BodyPart {
Arms => "are",
Hands => "are",
Legs => "are",
Feet => "are"
Feet => "are",
}
}
}
@ -82,14 +82,14 @@ pub struct SpeciesInfo {
pub corpse_butchers_into: Vec<PossessionType>,
}
pub fn species_info_map() -> &'static BTreeMap<SpeciesType, SpeciesInfo> {
static INFOMAP: OnceCell<BTreeMap<SpeciesType, SpeciesInfo>> = OnceCell::new();
INFOMAP.get_or_init(|| {
vec!(
(SpeciesType::Human,
vec![
(
SpeciesType::Human,
SpeciesInfo {
body_parts: vec!(
body_parts: vec![
BodyPart::Head,
BodyPart::Face,
BodyPart::Chest,
@ -98,34 +98,51 @@ pub fn species_info_map() -> &'static BTreeMap<SpeciesType, SpeciesInfo> {
BodyPart::Arms,
BodyPart::Hands,
BodyPart::Legs,
BodyPart::Feet
),
corpse_butchers_into: vec!(
BodyPart::Feet,
],
corpse_butchers_into: vec![
PossessionType::Steak,
PossessionType::Steak,
PossessionType::AnimalSkin,
PossessionType::SeveredHead,
],
},
),
}
),
(SpeciesType::Dog,
(
SpeciesType::Dog,
SpeciesInfo {
body_parts: vec!(
body_parts: vec![
BodyPart::Head,
BodyPart::Face,
BodyPart::Chest,
BodyPart::Back,
BodyPart::Groin,
BodyPart::Legs,
BodyPart::Feet
),
corpse_butchers_into: vec!(
BodyPart::Feet,
],
corpse_butchers_into: vec![
PossessionType::Steak,
PossessionType::AnimalSkin,
PossessionType::SeveredHead,
],
},
),
}
(
SpeciesType::Robot,
SpeciesInfo {
body_parts: vec![
BodyPart::Head,
BodyPart::Face,
BodyPart::Chest,
BodyPart::Back,
BodyPart::Legs,
BodyPart::Feet,
],
corpse_butchers_into: vec![],
},
),
).into_iter().collect()
]
.into_iter()
.collect()
})
}