Allow renting apartments at CoK building.

This commit is contained in:
Condorra 2023-04-08 23:51:18 +10:00
parent ff5e80398a
commit 0ccf9a3adc
14 changed files with 1002 additions and 120 deletions

View File

@ -9,6 +9,7 @@ use tokio_postgres::NoTls;
use crate::message_handler::ListenerSession; use crate::message_handler::ListenerSession;
use crate::DResult; use crate::DResult;
use crate::message_handler::user_commands::parsing::parse_offset; use crate::message_handler::user_commands::parsing::parse_offset;
use crate::static_content::room::Direction;
use crate::models::{ use crate::models::{
session::Session, session::Session,
user::User, user::User,
@ -313,6 +314,17 @@ impl DBTrans {
.get("item_id")) .get("item_id"))
} }
pub async fn find_exact_dyn_exit<'a>(self: &'a Self, source: &'a str, direction: &'a Direction) -> DResult<Option<Item>> {
if let Some(details_json) = self.pg_trans()?
.query_opt("SELECT details FROM items WHERE \
details->'dynamic_entrance'->>'source_item' = $1 AND \
LOWER(details->'dynamic_entrance'->>'direction') = $2",
&[&source, &direction.describe().to_lowercase()]).await? {
return Ok(Some(serde_json::from_value(details_json.get("details"))?))
}
Ok(None)
}
pub async fn limited_update_static_item<'a>(self: &'a Self, item: &'a Item) -> DResult<()> { pub async fn limited_update_static_item<'a>(self: &'a Self, item: &'a Item) -> DResult<()> {
let value = serde_json::to_value(item)?; let value = serde_json::to_value(item)?;
let obj_map = value.as_object() let obj_map = value.as_object()
@ -482,6 +494,18 @@ impl DBTrans {
.collect()) .collect())
} }
pub async fn find_item_by_location_dynroom_code<'a>(self: &'a Self, location: &'a str,
dynroom_code: &'a str) -> DResult<Option<Item>> {
match self.pg_trans()?.query_opt(
"SELECT details FROM items WHERE details->>'location' = $1 \
AND details->'special_data'->'DynroomData'->>'dynroom_code' = $2 LIMIT 1",
&[&location, &dynroom_code]
).await? {
None => Ok(None),
Some(v) => Ok(Some(serde_json::from_value(v.get("details"))?))
}
}
pub async fn save_item_model(self: &Self, details: &Item) pub async fn save_item_model(self: &Self, details: &Item)
-> DResult<()> { -> DResult<()> {
self.pg_trans()? self.pg_trans()?

View File

@ -33,6 +33,7 @@ mod page;
pub mod parsing; pub mod parsing;
mod quit; mod quit;
pub mod register; pub mod register;
pub mod rent;
pub mod say; pub mod say;
mod score; mod score;
mod sign; mod sign;
@ -113,6 +114,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"nw" => movement::VERB, "nw" => movement::VERB,
"up" => movement::VERB, "up" => movement::VERB,
"down" => movement::VERB, "down" => movement::VERB,
"in" => movement::VERB,
// Other commands (alphabetical except aliases grouped): // Other commands (alphabetical except aliases grouped):
"allow" => allow::VERB, "allow" => allow::VERB,
@ -145,6 +147,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"repl" => page::VERB, "repl" => page::VERB,
"reply" => page::VERB, "reply" => page::VERB,
"rent" => rent::VERB,
"\'" => say::VERB, "\'" => say::VERB,
"say" => say::VERB, "say" => say::VERB,

View File

@ -4,9 +4,12 @@ use async_trait::async_trait;
use ansi::{ansi, flow_around, word_wrap}; use ansi::{ansi, flow_around, word_wrap};
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
models::{item::{Item, LocationActionType, Subattack, ItemFlag}}, models::{item::{
Item, LocationActionType, Subattack, ItemFlag, ItemSpecialData
}},
static_content::{ static_content::{
room::{self, Direction}, room::{self, Direction, GridCoords},
dynzone::self,
possession_type::possession_data, possession_type::possession_data,
}, },
language, language,
@ -14,6 +17,8 @@ use crate::{
}; };
use itertools::Itertools; use itertools::Itertools;
use std::sync::Arc; use std::sync::Arc;
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String { pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
let mut buf = String::new(); let mut buf = String::new();
@ -45,6 +50,54 @@ pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
buf buf
} }
pub fn render_map_dyn(dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom,
width: usize, height: usize) -> String {
let mut buf = String::new();
let my_loc = &dynroom.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
let max_x = min_x + (width as i64);
let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64);
let main_exit: Option<GridCoords> = dynzone.dyn_rooms
.iter()
.flat_map(|(_, dr)|
dr.exits.iter()
.filter(|ex| match ex.target {
dynzone::ExitTarget::ExitZone => true,
_ => false
})
.map(|ex| dr.grid_coords.apply(&ex.direction))
).next();
for y in min_y..max_y {
for x in min_x..max_x {
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else {
buf.push_str(dynzone.dyn_rooms.iter()
.find(
|(_, dr)| dr.grid_coords.x == x &&
dr.grid_coords.y == y &&
dr.grid_coords.z == my_loc.z)
.map(|(_, r)| r.short)
.or_else(|| main_exit.as_ref().and_then(
|ex_pos|
if ex_pos.x == x && ex_pos.y == y &&
ex_pos.z == my_loc.z {
Some("<<")
} else {
None
}))
.unwrap_or(" "));
}
}
buf.push('\n');
}
buf
}
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> { pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> {
let mut contents_desc = String::new(); let mut contents_desc = String::new();
@ -156,6 +209,13 @@ fn exits_for(room: &room::Room) -> String {
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" ")) format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
} }
fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String {
let exit_text: Vec<String> =
dynroom.exits.iter().map(|ex| format!(ansi!("<yellow>{}"),
ex.direction.describe())).collect();
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
}
pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item, pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item,
room: &room::Room, contents: &str) -> UResult<()> { room: &room::Room, contents: &str) -> UResult<()> {
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time"); let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
@ -173,6 +233,25 @@ pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item,
Ok(()) Ok(())
} }
pub async fn describe_dynroom(ctx: &VerbContext<'_>,
item: &Item,
dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom,
contents: &str) -> UResult<()> {
ctx.trans.queue_for_session(
ctx.session,
Some(&flow_around(&render_map_dyn(dynzone, dynroom, 5, 5), 10, ansi!("<reset> "),
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat),
dynzone.zonename,
item.details_for_session(
&ctx.session_dat).unwrap_or(""),
contents, exits_for_dyn(dynroom)),
|row| if row >= 5 { 80 } else { 68 }), 68))
).await?;
Ok(())
}
async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> { async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> {
if item.flags.contains(&ItemFlag::NoSeeContents) { if item.flags.contains(&ItemFlag::NoSeeContents) {
return Ok(" It is too foggy to see who or what else is here.".to_owned()); return Ok(" It is too foggy to see who or what else is here.".to_owned());
@ -238,6 +317,71 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
Ok(buf) Ok(buf)
} }
async fn direction_to_item(
trans: &DBTrans,
use_location: &str,
direction: &Direction,
) -> UResult<Option<Arc<Item>>> {
// Firstly check dynamic exits, since they apply to rooms and dynrooms...
if let Some(dynroom_result) = trans.find_exact_dyn_exit(use_location, direction).await? {
return Ok(Some(Arc::new(dynroom_result)));
}
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
if heretype == "dynroom" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v
};
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?
};
let dynzone = dynzone::dynzone_by_type()
.get(&dynzone::DynzoneType::from_str(dynzone_code)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?;
let dynroom = dynzone.dyn_rooms.get(dynroom_code.as_str())
.ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target {
dynzone::ExitTarget::ExitZone => {
let (zonetype, zonecode) = old_dynroom_item.location.split_once("/")
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await?
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) =>
user_error("That exit doesn't seem to go anywhere".to_owned())?,
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit,
_ => user_error("The zone you are in has invalid data associated with it".to_owned())?,
};
let (zone_exit_type, zone_exit_code) = zone_exit.split_once("/").ok_or_else(
|| UserError("Oops, that way out seems to be broken.".to_owned()))?;
Ok(trans.find_item_by_type_code(zone_exit_type, zone_exit_code).await?)
},
dynzone::ExitTarget::Intrazone { subcode } => {
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await?
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?;
Ok(Some(Arc::new(to_item)))
}
}
}
if heretype != "room" {
user_error("Navigating outside rooms not yet supported.".to_owned())?
}
let room = room::room_map_by_code().get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
let new_room =
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok(trans.find_item_by_type_code("room", new_room.code).await?)
}
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
@ -253,27 +397,8 @@ impl UserVerb for Verb {
ctx.trans.find_item_by_type_code(heretype, herecode).await? ctx.trans.find_item_by_type_code(heretype, herecode).await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))? .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
} else if let Some(dir) = Direction::parse(&rem_trim) { } else if let Some(dir) = Direction::parse(&rem_trim) {
if heretype != "room" { direction_to_item(&ctx.trans, use_location, &dir).await?
// Fix this when we have planes / boats / roomkits. .ok_or_else(|| UserError("There's nothing in that direction".to_owned()))?
user_error("Navigating outside rooms not yet supported.".to_owned())?
} else {
if let Some(room) = room::room_map_by_code().get(herecode) {
match room.exits.iter().find(|ex| ex.direction == *dir) {
None => user_error("There is nothing in that direction".to_owned())?,
Some(exit) => {
match room::resolve_exit(room, exit) {
None => user_error("There is nothing in that direction".to_owned())?,
Some(room2) =>
ctx.trans.find_item_by_type_code("room", room2.code).await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
}
}
}
} else {
user_error("Can't find your current location".to_owned())?
}
}
} else if rem_trim == "me" || rem_trim == "self" { } else if rem_trim == "me" || rem_trim == "self" {
player_item.clone() player_item.clone()
} else { } else {
@ -287,13 +412,26 @@ impl UserVerb for Verb {
} }
).await? ).await?
}; };
if item.item_type != "room" { if item.item_type == "room" {
describe_normal_item(ctx, &item).await?;
} else {
let room = let room =
room::room_map_by_code().get(item.item_code.as_str()) room::room_map_by_code().get(item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?; .ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?; describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?;
} else if item.item_type == "dynroom" {
let (dynzone, dynroom) = match &item.special_data {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => {
dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t|
dynzone::dynzone_by_type().get(&dz_t))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?
},
_ => user_error("Expected dynroom to have DynroomData".to_owned())?
};
describe_dynroom(ctx, &item, &dynzone, &dynroom,
&list_room_contents(ctx, &item).await?).await?;
} else {
describe_normal_item(ctx, &item).await?;
} }
Ok(()) Ok(())
} }

View File

@ -12,9 +12,13 @@ use crate::{
QueueCommand, QueueCommand,
queue_command queue_command
}, },
static_content::room::{self, Direction, ExitType}, static_content::{
room::{self, Direction, ExitType},
dynzone::{dynzone_by_type, ExitTarget as DynExitTarget, DynzoneType},
},
models::item::{ models::item::{
Item, Item,
ItemSpecialData,
SkillType, SkillType,
LocationActionType LocationActionType
}, },
@ -25,6 +29,7 @@ use crate::{
combat::handle_resurrect, combat::handle_resurrect,
} }
}; };
use std::sync::Arc;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; #[double] use crate::db::DBTrans;
use std::time; use std::time;
@ -53,6 +58,83 @@ pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, ar
Ok(()) Ok(())
} }
async fn move_to_where(
trans: &DBTrans,
use_location: &str,
direction: &Direction,
mover: &mut Item,
player_ctx: &mut Option<&mut VerbContext<'_>>
) -> UResult<(String, Option<Item>)> {
// Firstly check dynamic exits, since they apply to rooms and dynrooms...
if let Some(dynroom_result) = trans.find_exact_dyn_exit(use_location, direction).await? {
return Ok((format!("{}/{}",
&dynroom_result.item_type,
&dynroom_result.item_code), Some(dynroom_result)));
}
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
if heretype == "dynroom" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v
};
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?
};
let dynzone = dynzone_by_type().get(&DynzoneType::from_str(dynzone_code)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?;
let dynroom = dynzone.dyn_rooms.get(dynroom_code.as_str())
.ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target {
DynExitTarget::ExitZone => {
let (zonetype, zonecode) = old_dynroom_item.location.split_once("/")
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await?
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) =>
user_error("That exit doesn't seem to go anywhere".to_owned())?,
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit,
_ => user_error("The zone you are in has invalid data associated with it".to_owned())?,
};
Ok((zone_exit.to_string(), None))
},
DynExitTarget::Intrazone { subcode } => {
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await?
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?;
Ok((format!("{}/{}", &to_item.item_type, &to_item.item_code), Some(to_item)))
}
}
}
if heretype != "room" {
user_error("Navigating outside rooms not yet supported.".to_owned())?
}
let room = room::room_map_by_code().get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
match exit.exit_type {
ExitType::Free => {}
ExitType::Blocked(blocker) => {
if let Some(ctx) = player_ctx {
if !blocker.attempt_exit(*ctx, mover, exit).await? {
user_error("Stopping movement".to_owned())?;
}
}
}
}
let new_room =
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok((format!("room/{}", new_room.code), None))
}
pub async fn attempt_move_immediate( pub async fn attempt_move_immediate(
trans: &DBTrans, trans: &DBTrans,
orig_mover: &Item, orig_mover: &Item,
@ -67,30 +149,9 @@ pub async fn attempt_move_immediate(
} else { } else {
&orig_mover.location &orig_mover.location
}; };
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
// Fix this when we have planes / boats / roomkits.
user_error("Navigating outside rooms not yet supported.".to_owned())?
}
let room = room::room_map_by_code().get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
let mut mover = (*orig_mover).clone(); let mut mover = (*orig_mover).clone();
match exit.exit_type { let (new_loc, new_loc_item) = move_to_where(trans, use_location, direction, &mut mover, &mut player_ctx).await?;
ExitType::Free => {}
ExitType::Blocked(blocker) => {
if let Some(ctx) = player_ctx.as_mut() {
if !blocker.attempt_exit(ctx, &mut mover, exit).await? {
user_error("Stopping movement".to_owned())?;
}
}
}
}
let new_room =
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
match mover.active_combat.as_ref().and_then(|ac| ac.attacking.clone()) { match mover.active_combat.as_ref().and_then(|ac| ac.attacking.clone()) {
None => {} None => {}
@ -150,7 +211,7 @@ pub async fn attempt_move_immediate(
} }
} }
mover.location = format!("{}/{}", "room", new_room.code); mover.location = new_loc.clone();
mover.action_type = LocationActionType::Normal; mover.action_type = LocationActionType::Normal;
mover.active_combat = None; mover.active_combat = None;
@ -160,9 +221,16 @@ pub async fn attempt_move_immediate(
look::VERB.handle(ctx, "look", "").await?; look::VERB.handle(ctx, "look", "").await?;
} }
if let Some(old_room_item) = trans.find_item_by_type_code("room", room.code).await? { if let Some((old_loc_type, old_loc_code)) = use_location.split_once("/") {
if let Some(new_room_item) = trans.find_item_by_type_code("room", new_room.code).await? { if let Some(old_room_item) = trans.find_item_by_type_code(old_loc_type, old_loc_code).await? {
announce_move(&trans, &mover, &old_room_item, &new_room_item).await?; if let Some((new_loc_type, new_loc_code)) = new_loc.split_once("/") {
if let Some(new_room_item) = match new_loc_item {
None => trans.find_item_by_type_code(new_loc_type, new_loc_code).await?,
v => v.map(Arc::new)
} {
announce_move(&trans, &mover, &old_room_item, &new_room_item).await?;
}
}
} }
} }
@ -195,10 +263,9 @@ pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> { async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> {
let dir = Direction::parse(verb).ok_or_else(|| UserError("Unknown direction".to_owned()))?; let dir = Direction::parse(
if remaining.trim() != "" { &(verb.to_owned() + " " + remaining.trim()).trim())
user_error("Movement commands don't take extra data at the end.".to_owned())?; .ok_or_else(|| UserError("Unknown direction".to_owned()))?;
}
queue_command(ctx, &QueueCommand::Movement { direction: dir.clone() }).await queue_command(ctx, &QueueCommand::Movement { direction: dir.clone() }).await
} }
} }

View File

@ -0,0 +1,301 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError,
user_error, get_player_item_or_fail, get_user_or_fail,
};
use crate::{
DResult,
static_content::{
room::{room_map_by_code, Direction},
dynzone::dynzone_by_type,
npc::npc_by_code,
},
models::{
task::{TaskDetails, TaskMeta, Task},
item::{Item, ItemSpecialData},
},
regular_tasks::{
TaskHandler,
TaskRunContext,
},
};
use log::info;
use async_trait::async_trait;
use chrono::{Utc, Duration};
use std::time;
use ansi::ansi;
use async_recursion::async_recursion;
use itertools::Itertools;
#[async_recursion]
async fn recursively_destroy_or_move_item(ctx: &mut TaskRunContext<'_>, item: &Item) -> DResult<()> {
let mut item_mut = item.clone();
match item.item_type.as_str() {
"npc" => {
let npc = match npc_by_code().get(item.item_code.as_str()) {
None => { return Ok(()) },
Some(r) => r
};
item_mut.location = npc.spawn_location.to_owned();
ctx.trans.save_item_model(&item_mut).await?;
return Ok(());
},
"player" => {
let session = ctx.trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() {
Some((listener_sess, _)) => {
ctx.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?;
},
None => {}
}
item_mut.location = "room/melbs_homelessshelter".to_owned();
ctx.trans.save_item_model(&item_mut).await?;
return Ok(());
},
_ => {}
}
ctx.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?;
if result.is_empty() {
return Ok(());
}
for sub_item in result {
recursively_destroy_or_move_item(ctx, &sub_item).await?;
}
}
}
static EVICTION_NOTICE: &'static str = ansi!(". Nailed to the door is a notice: <red>Listen here you lazy bum - you didn't pay your rent on time, and so unless you come down in the next 24 hours and re-rent the place (and pay the setup fee again for wasting my time), I'm having you, and any other deadbeats who might be in there with you, evicted, and I'm just gonna sell anything I find in there<reset>");
pub struct ChargeRoomTaskHandler;
#[async_trait]
impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (zone_item_ref, daily_price) = match &mut ctx.task.details {
TaskDetails::ChargeRoom { zone_item, daily_price } => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?
};
let zone_item_code = match zone_item_ref.split_once("/") {
Some(("dynzone", c)) => c,
_ => Err("Invalid zone item ref when charging room")?
};
let zone_item = match ctx.trans.find_item_by_type_code("dynzone", zone_item_code).await? {
None => {
info!("Can't charge rent for dynzone {}, it's gone", zone_item_code);
return Ok(None);
}
Some(it) => it
};
let vacate_after = match zone_item.special_data {
Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after,
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")?
};
match vacate_after {
Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(ctx, &zone_item).await?;
return Ok(None);
}
_ => ()
}
let bill_player_code = match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) {
Some((player_item_type, player_item_code)) if player_item_type == "player" =>
player_item_code,
_ => {
info!("Can't charge rent for dynzone {}, owner {:?} isn't chargeable", zone_item_code,
&zone_item.owner
);
return Ok(None)
}
};
let mut bill_user = match ctx.trans.find_by_username(bill_player_code).await? {
None => return Ok(None),
Some(user) => user
};
let session = ctx.trans.find_session_for_player(bill_player_code).await?;
// Check if they have enough money.
if bill_user.credits < *daily_price {
let mut zone_item_mut = (*zone_item).clone();
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + Duration::days(1)),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) =>
zone_exit.clone(),
_ => None
}});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx.trans.find_item_by_type_code("dynroom",
&(zone_item.item_code.clone() + "/doorstep")).await? {
None => {},
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some(
doorstep_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE
);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<red>Your wristpad beeps a sad sounding tone as your landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money fast \
and hurry to reception (in the next 24 hours) and sign a new rental \
agreement for the premises, or all your stuff will be gone forever!\n"),
&zone_item.display
)),
).await?
}
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_user.credits -= *daily_price;
ctx.trans.save_user_model(&bill_user).await?;
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${} for rent for {}.<reset>\n"),
&daily_price, &zone_item.display
))
).await?
}
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
}
pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeRoomTaskHandler;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let item_name = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item.location.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" {
user_error("You can't rent anything from here.".to_owned())?;
}
let room = room_map_by_code().get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() {
user_error("You can't rent anything from here.".to_owned())?;
}
let rentinfo = match room.rentable_dynzone.iter().find(|ri| ri.rent_what == item_name) {
None => user_error(format!("Rent must be followed by the specific thing you want to rent: {}",
room.rentable_dynzone.iter()
.map(|ri| ri.rent_what).join(", ")))?,
Some(v) => v
};
let user = get_user_or_fail(ctx)?;
if user.credits < rentinfo.setup_fee {
user_error("The robot rolls its eyes at you derisively. \"I don't think so - you couldn't even afford the setup fee!\"".to_owned())?
}
let zone = dynzone_by_type().get(&rentinfo.dynzone)
.ok_or_else(|| UserError("That seems to no longer exist, so you can't rent it.".to_owned()))?;
match ctx.trans.find_exact_dyn_exit(
&player_item.location,
&Direction::IN { item: player_item.display.clone() })
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => {},
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) =
ctx.trans.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await? {
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: None, .. }) =>
user_error(
"You can only rent one apartment here, and you already have one!".to_owned())?,
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_), zone_exit: ref ex }) => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.save_item_model(
&Item {
special_data:
Some(ItemSpecialData::DynzoneData {
vacate_after: None,
zone_exit: ex.clone()
}), ..(*ex_zone).clone() }
).await?;
match ctx.trans.find_item_by_type_code("dynroom",
&(ex_zone.item_code.clone() + "/doorstep")).await? {
None => {},
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some(
doorstep_mut.details.clone().unwrap_or("".to_owned()).replace(EVICTION_NOTICE, "")
);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(ansi!(
"\"Okay - let's forget this ever happened - and apart from me having a few extra credits for my trouble, your lease will continue as before!\"\n\
<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), rentinfo.setup_fee))
).await?;
return Ok(());
},
_ => {}
}
}
}
}
let zonecode = zone.create_instance(
ctx.trans, &player_item.location,
"You can only rent one apartment here, and you already have one!",
&player_item, &Direction::IN { item: player_item.display.clone() }
).await?;
ctx.trans.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("charge_rent/{}", &zonecode),
is_static: false,
recurrence: None, // Managed by the handler.
next_scheduled: Utc::now() + Duration::days(1),
..Default::default()
},
details: TaskDetails::ChargeRoom {
zone_item: zonecode.clone(),
daily_price: rentinfo.daily_price
}
}).await?;
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee))
).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -4,8 +4,10 @@ use crate::{
language, language,
static_content::species::SpeciesType, static_content::species::SpeciesType,
static_content::possession_type::PossessionType, static_content::possession_type::PossessionType,
static_content::room::Direction,
}; };
use super::session::Session; use super::session::Session;
use chrono::{DateTime, Utc};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum BuffCause { pub enum BuffCause {
@ -288,7 +290,21 @@ impl Default for ActiveCombat {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
pub enum ItemSpecialData { pub enum ItemSpecialData {
ItemWriting { text: String } ItemWriting { text: String },
DynroomData {
dynzone_code: String,
dynroom_code: String,
},
DynzoneData {
zone_exit: Option<String>,
vacate_after: Option<DateTime<Utc>>
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
pub struct DynamicEntrance {
pub direction: Direction,
pub source_item: String,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
@ -320,6 +336,8 @@ pub struct Item {
pub weight: u64, pub weight: u64,
pub charges: u8, pub charges: u8,
pub special_data: Option<ItemSpecialData>, pub special_data: Option<ItemSpecialData>,
pub dynamic_entrance: Option<DynamicEntrance>,
pub owner: Option<String>,
} }
impl Item { impl Item {
@ -397,6 +415,8 @@ impl Default for Item {
weight: 0, weight: 0,
charges: 0, charges: 0,
special_data: None, special_data: None,
dynamic_entrance: None,
owner: None,
} }
} }
} }

View File

@ -37,6 +37,10 @@ pub enum TaskDetails {
ExpireItem { ExpireItem {
item_code: String item_code: String
}, },
ChargeRoom {
zone_item: String,
daily_price: u64
}
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -51,6 +55,8 @@ impl TaskDetails {
RotCorpse { .. } => "RotCorpse", RotCorpse { .. } => "RotCorpse",
DelayedHealth { .. } => "DelayedHealth", DelayedHealth { .. } => "DelayedHealth",
ExpireItem { .. } => "ExpireItem", ExpireItem { .. } => "ExpireItem",
ChargeRoom { .. } => "ChargeRoom",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
} }
} }
} }

View File

@ -7,7 +7,7 @@ use crate::{
listener::{ListenerMap, ListenerSend}, listener::{ListenerMap, ListenerSend},
static_content::npc, static_content::npc,
services::{combat, effect}, services::{combat, effect},
message_handler::user_commands::drop, message_handler::user_commands::{drop, rent},
}; };
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence}; #[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence};
use mockall_double::double; use mockall_double::double;
@ -45,6 +45,7 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()), ("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()),
("DelayedHealth", effect::DELAYED_HEALTH_HANDLER.clone()), ("DelayedHealth", effect::DELAYED_HEALTH_HANDLER.clone()),
("ExpireItem", drop::EXPIRE_ITEM_HANDLER.clone()), ("ExpireItem", drop::EXPIRE_ITEM_HANDLER.clone()),
("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone())
).into_iter().collect() ).into_iter().collect()
) )
} }

View File

@ -5,6 +5,7 @@ use std::collections::{BTreeSet, BTreeMap};
use log::info; use log::info;
pub mod room; pub mod room;
pub mod dynzone;
pub mod npc; pub mod npc;
pub mod possession_type; pub mod possession_type;
pub mod species; pub mod species;

View File

@ -0,0 +1,240 @@
// A dynzone is a template for a set of rooms arranged in a fixed pattern.
// While the layout is static, an arbitrary number of instances can be created
// dynamically. They can dynamically connect to the grid.
// Apartments, planes, and boats are all expected to be specific instances of dynzones.
use super::room::{Direction, GridCoords};
use crate::{
message_handler::user_commands::{user_error, UResult},
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance}
};
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use mockall_double::double;
#[double] use crate::db::DBTrans;
mod cokmurl_apartment;
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
pub enum DynzoneType {
CokMurlApartment
}
impl DynzoneType {
pub fn from_str(i: &str) -> Option<Self> {
match i {
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
_ => None
}
}
pub fn to_str(&self) -> &'static str {
match self {
DynzoneType::CokMurlApartment => "CokMurlApartment"
}
}
}
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
pub struct Dynzone {
pub zonetype: DynzoneType,
pub zonename: &'static str,
pub entrypoint_subcode: &'static str,
pub dyn_rooms: BTreeMap<&'static str, Dynroom>,
}
impl Dynzone {
// Returns None if there is already an instance in the same exit direction.
pub async fn create_instance(&self, trans: &DBTrans, connect_where: &str, dup_message: &str,
new_owner: &Item, new_exit_direction: &Direction) -> UResult<String> {
// Check exit not taken...
if trans.find_exact_dyn_exit(connect_where, new_exit_direction).await?.is_some() {
user_error(dup_message.to_string())?;
}
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
let code = format!("{}", &trans.alloc_item_code().await?);
trans.create_item(
&Item {
item_type: "dynzone".to_owned(),
item_code: code.clone(),
display: self.zonename.to_owned(),
special_data: Some(
ItemSpecialData::DynzoneData {
zone_exit: Some(connect_where.to_owned()),
vacate_after: None
}
),
owner: Some(owner.clone()),
location: format!("dynzone/{}", &code),
..Default::default()
}
).await?;
let mut should_connect = true;
for (_, room) in &self.dyn_rooms {
let roomcode = format!("{}/{}", &code, room.subcode);
let will_connect = should_connect &&
room.exits.iter().any(|r| match r.target {
ExitTarget::ExitZone => true,
_ => false
});
should_connect &= !will_connect;
trans.create_item(
&Item {
item_type: "dynroom".to_owned(),
item_code: roomcode,
display: room.name.to_owned(),
details: Some(room.description.to_owned()),
details_less_explicit: room.description_less_explicit.map(|s| s.to_owned()),
location: format!("dynzone/{}", &code),
special_data: Some(ItemSpecialData::DynroomData {
dynzone_code: self.zonetype.to_str().to_owned(),
dynroom_code: room.subcode.to_owned()
}),
dynamic_entrance: if will_connect {
Some(DynamicEntrance {
direction: new_exit_direction.clone(),
source_item: connect_where.to_owned()
})
} else { None },
flags: room.item_flags.clone(),
owner: Some(owner.clone()),
..Default::default()
}
).await?;
}
Ok(format!("dynzone/{}", &code))
}
}
impl Default for Dynzone {
fn default() -> Self {
Self {
zonename: "undefined",
zonetype: DynzoneType::CokMurlApartment,
entrypoint_subcode: "entry",
dyn_rooms: BTreeMap::new(),
}
}
}
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
pub enum ExitType {
Doorless,
Doored,
}
#[derive(Eq, Ord, Debug, PartialEq, PartialOrd, Clone)]
pub enum ExitTarget {
ExitZone,
Intrazone { subcode: &'static str },
}
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
pub struct Exit {
pub direction: Direction,
pub target: ExitTarget,
pub exit_type: ExitType,
}
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
pub struct Dynroom {
pub subcode: &'static str,
pub name: &'static str,
pub short: &'static str,
pub description: &'static str,
pub description_less_explicit: Option<&'static str>,
pub exits: Vec<Exit>,
pub should_caption: bool,
pub item_flags: Vec<ItemFlag>,
pub grid_coords: GridCoords,
}
impl Default for Dynroom {
fn default() -> Self {
Self {
subcode: "undefined",
name: "Undefined",
short: "XX",
description: "A generic room",
description_less_explicit: None,
exits: vec!(),
should_caption: false,
item_flags: vec!(),
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
}
}
}
pub fn dynzone_list() -> &'static Vec<Dynzone> {
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
CELL.get_or_init(
|| vec!(
cokmurl_apartment::zone()
)
)
}
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
CELL.get_or_init(
|| dynzone_list().iter().map(|z| (&z.zonetype, (*z).clone())).collect()
)
}
#[cfg(test)]
mod test {
use itertools::Itertools;
use super::super::room::{Direction};
use super::{dynzone_list, ExitTarget, DynzoneType};
#[test]
fn dynzone_types_unique() {
let mut sorted_list = dynzone_list().clone();
sorted_list.sort();
assert_eq!(Vec::<(&DynzoneType, usize)>::new(),
sorted_list.iter()
.group_by(|v| &v.zonetype)
.into_iter()
.map(|v| (v.0, v.1.count()))
.filter(|v| v.1 > 1)
.collect::<Vec<(&DynzoneType, usize)>>()
);
}
#[test]
fn dynroom_codes_match_struct() {
for dynzone in dynzone_list() {
assert_eq!(
dynzone.dyn_rooms.iter().filter(|v| *v.0 != v.1.subcode)
.map(|v| *v.0).collect::<Vec<&str>>(),
Vec::<&str>::new()
);
}
}
#[test]
fn dynzone_has_dynroom() {
for dynzone in dynzone_list() {
assert_ne!(0, dynzone.dyn_rooms.len(), "# rooms in zone {}",
dynzone.zonetype.to_str())
}
}
#[test]
fn dynroom_exits_subcodes_exists() {
for dynzone in dynzone_list() {
for dynroom in dynzone.dyn_rooms.iter() {
let exits = dynroom.1.exits.iter().filter(
|ex|
if let ExitTarget::Intrazone { subcode } = ex.target {
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
} else {
false
}).map(|ex| &ex.direction).collect::<Vec<&Direction>>();
assert_eq!(Vec::<&Direction>::new(), exits,
"exits to invalid subcode in room {} in zone {}", dynroom.0,
dynzone.zonetype.to_str());
}
}
}
}

View File

@ -0,0 +1,62 @@
use super::{
Dynzone,
DynzoneType,
Dynroom,
Exit,
ExitTarget,
ExitType,
super::room::GridCoords
};
use crate::static_content::room::Direction;
pub fn zone() -> Dynzone {
Dynzone {
zonetype: DynzoneType::CokMurlApartment,
zonename: "Condos on King",
entrypoint_subcode: "doorstep",
dyn_rooms: vec!(
("doorstep", Dynroom {
subcode: "doorstep",
name: "Door step",
short: "DS",
description: "A sleek hallway, painted white, and lit by futuristic looking bluish-white strip lights that run along the edges of the ceiling. Soft navy blue carpet covers the floor. A beige painted door seems to lead into a private studio apartment",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::WEST,
target: ExitTarget::ExitZone,
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::EAST,
target: ExitTarget::Intrazone { subcode: "studio" },
exit_type: ExitType::Doored
}
),
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
should_caption: false,
item_flags: vec!(),
..Default::default()
}),
("studio", Dynroom {
subcode: "studio",
name: "Studio apartment",
short: "ST",
description: "An oddly comfortable studio apartment, with worn grey carpet covering the floor. A window to the east has spectacular views of Melbs and the bleak and desolate wasteland beyond it",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::WEST,
target: ExitTarget::Intrazone { subcode: "doorstep" },
exit_type: ExitType::Doored
}
),
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
should_caption: false,
item_flags: vec!(),
..Default::default()
})
).into_iter().collect(),
..Default::default()
}
}

View File

@ -82,7 +82,7 @@ pub enum ExitType {
} }
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
pub enum Direction { pub enum Direction {
NORTH, NORTH,
SOUTH, SOUTH,
@ -96,6 +96,21 @@ pub enum Direction {
DOWN, DOWN,
IN { item: String } IN { item: String }
} }
impl Serialize for Direction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
Serialize::serialize(&self.describe(), serializer)
}
}
impl <'de> Deserialize<'de> for Direction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
Deserialize::deserialize(deserializer)
.and_then(|s: String| Self::parse(s.as_str())
.ok_or(
serde::de::Error::custom("Invalid direction")))
}
}
impl Direction { impl Direction {
pub fn describe(self: &Self) -> String { pub fn describe(self: &Self) -> String {
@ -110,22 +125,25 @@ impl Direction {
Direction::SOUTHWEST => "southwest".to_owned(), Direction::SOUTHWEST => "southwest".to_owned(),
Direction::UP => "up".to_owned(), Direction::UP => "up".to_owned(),
Direction::DOWN => "down".to_owned(), Direction::DOWN => "down".to_owned(),
Direction::IN { item } => item.to_owned() Direction::IN { item } => "in ".to_owned() + item
} }
} }
pub fn parse(input: &str) -> Option<&'static Direction> { pub fn parse(input: &str) -> Option<Direction> {
if input.starts_with("in ") {
return Some(Direction::IN { item: input["in ".len()..].trim().to_owned() })
}
match input { match input {
"north" | "n" => Some(&Direction::NORTH), "north" | "n" => Some(Direction::NORTH),
"south" | "s" => Some(&Direction::SOUTH), "south" | "s" => Some(Direction::SOUTH),
"east" | "e" => Some(&Direction::EAST), "east" | "e" => Some(Direction::EAST),
"west" | "w" => Some(&Direction::WEST), "west" | "w" => Some(Direction::WEST),
"northeast" | "ne" => Some(&Direction::NORTHEAST), "northeast" | "ne" => Some(Direction::NORTHEAST),
"southeast" | "se" => Some(&Direction::SOUTHEAST), "southeast" | "se" => Some(Direction::SOUTHEAST),
"northwest" | "nw" => Some(&Direction::NORTHEAST), "northwest" | "nw" => Some(Direction::NORTHWEST),
"southwest" | "sw" => Some(&Direction::SOUTHWEST), "southwest" | "sw" => Some(Direction::SOUTHWEST),
"up" => Some(&Direction::UP), "up" => Some(Direction::UP),
"down" => Some(&Direction::DOWN), "down" => Some(Direction::DOWN),
_ => None _ => None
} }
} }
@ -164,6 +182,13 @@ impl Default for RoomStock {
} }
} }
pub struct RentInfo {
pub rent_what: &'static str,
pub dynzone: super::dynzone::DynzoneType,
pub daily_price: u64,
pub setup_fee: u64,
}
pub struct Room { pub struct Room {
pub zone: &'static str, pub zone: &'static str,
// Other zones where it can be seen on the map and accessed. // Other zones where it can be seen on the map and accessed.
@ -180,6 +205,8 @@ pub struct Room {
pub item_flags: Vec<ItemFlag>, pub item_flags: Vec<ItemFlag>,
// Empty means not a shop. // Empty means not a shop.
pub stock_list: Vec<RoomStock>, pub stock_list: Vec<RoomStock>,
// What can be rented here...
pub rentable_dynzone: Vec<RentInfo>
} }
impl Default for Room { impl Default for Room {
@ -198,6 +225,7 @@ impl Default for Room {
repel_npc: false, repel_npc: false,
item_flags: vec!(), item_flags: vec!(),
stock_list: vec!(), stock_list: vec!(),
rentable_dynzone: vec!()
} }
} }
@ -303,4 +331,30 @@ mod test {
Vec::<Vec<(&str, &GridCoords, &str)>>::new()); Vec::<Vec<(&str, &GridCoords, &str)>>::new());
} }
#[test]
fn parse_direction_should_work() {
assert_eq!(Direction::parse("southeast"), Some(Direction::SOUTHEAST));
}
#[test]
fn parse_and_describe_direction_should_roundtrip() {
let examples = vec!(
"north",
"south",
"east",
"west",
"northeast",
"southeast",
"northwest",
"southwest",
"up",
"down",
"in there"
);
for example in examples {
assert_eq!(Direction::parse(example).map(|v| v.describe()), Some(example.to_owned()));
}
}
} }

View File

@ -1,7 +1,8 @@
use super::{ use super::{
Room, GridCoords, Exit, Direction, ExitTarget, ExitType, Room, GridCoords, Exit, Direction, ExitTarget, ExitType,
SecondaryZoneRecord SecondaryZoneRecord, RentInfo,
}; };
use crate::static_content::dynzone::DynzoneType;
use ansi::ansi; use ansi::ansi;
pub fn room_list() -> Vec<Room> { pub fn room_list() -> Vec<Room> {
vec!( vec!(
@ -19,7 +20,7 @@ pub fn room_list() -> Vec<Room> {
code: "cok_lobby", code: "cok_lobby",
name: "Residential Lobby", name: "Residential Lobby",
short: ansi!("<bgyellow><black>RE<reset>"), short: ansi!("<bgyellow><black>RE<reset>"),
description: "A sizeable lobby that looks like it is serves the dual purpose as the entrance to the residential condos and as a grand entrance to the linked Murlison Suites commercial building. It is tiled with sparkling clean bluestone tiles. Light green tinted tempered glass panels line the walls. You notice a set of sleek lifts to the south, stairs to the north, and a passage to the attached Murlison commercial building to the east", description: ansi!("A sizeable lobby that looks like it is serves the dual purpose as the entrance to the residential condos and as a grand entrance to the linked Murlison Suites commercial building. It is tiled with sparkling clean bluestone tiles. Light green tinted tempered glass panels line the walls. You notice a set of sleek lifts, supervised by a friendly robot, and a passage to the attached Murlison commercial building to the east.\n\n\"Welcome to Condos on King!\", intones the bot, \"say <bold>in<reset> name\" with the name of the person you are here to see, and I'll guide you to their apartment. Or try <bold>rent studio<reset> to rent a studio apartment for $20 a day ($40 setup fee), and <bold>vacate studio<reset> to give notice to vacate"),
description_less_explicit: None, description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 0, z: 0 }, grid_coords: GridCoords { x: 0, y: 0, z: 0 },
exits: vec!( exits: vec!(
@ -28,16 +29,6 @@ pub fn room_list() -> Vec<Room> {
target: ExitTarget::Custom("room/melbs_kingst_80"), target: ExitTarget::Custom("room/melbs_kingst_80"),
exit_type: ExitType::Free exit_type: ExitType::Free
}, },
Exit {
direction: Direction::NORTH,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
Exit {
direction: Direction::SOUTH,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
Exit { Exit {
direction: Direction::EAST, direction: Direction::EAST,
target: ExitTarget::UseGPS, target: ExitTarget::UseGPS,
@ -45,42 +36,12 @@ pub fn room_list() -> Vec<Room> {
}, },
), ),
should_caption: true, should_caption: true,
..Default::default() rentable_dynzone: vec!(RentInfo {
}, rent_what: "studio",
Room { dynzone: DynzoneType::CokMurlApartment,
zone: "cok_murl", daily_price: 20,
code: "cok_gf_lift", setup_fee: 40,
name: "Residential Lifts", }),
short: ansi!("<bgyellow><black>LI<reset>"),
description: "A set of lifts leading up to various floors",
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: -1, z: 0 },
exits: vec!(
Exit {
direction: Direction::SOUTH,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
),
should_caption: true,
..Default::default()
},
Room {
zone: "cok_murl",
code: "cok_gf_stairs",
name: "Residential Stairs",
short: ansi!("<bgyellow><black>LI<reset>"),
description: ansi!("A set of lifts leading up to various floors. It looks like it is also possible to go down to the basement by stepping down through a trapdoor covered with tape that says <bgwhite><red>EXTREME DANGER - DO NOT ENTER<reset>"),
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 1, z: 0 },
exits: vec!(
Exit {
direction: Direction::NORTH,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
),
should_caption: true,
..Default::default() ..Default::default()
}, },

View File

@ -28,6 +28,9 @@ CREATE INDEX item_by_loc ON items ((details->>'location'));
CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean))); CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
CREATE INDEX item_by_display ON items (lower(details->>'display')); CREATE INDEX item_by_display ON items (lower(details->>'display'));
CREATE INDEX item_by_display_less_explicit ON items (lower(details->>'display_less_explicit')); CREATE INDEX item_by_display_less_explicit ON items (lower(details->>'display_less_explicit'));
CREATE UNIQUE INDEX item_dynamic_entrance ON items (
(details->'dynamic_entrance'->>'source_item'),
(LOWER(details->'dynamic_entrance'->>'direction')));
CREATE TABLE users ( CREATE TABLE users (
-- Username here is all lower case, but details has correct case version. -- Username here is all lower case, but details has correct case version.