Implement corporate HQs

This commit is contained in:
Condorra 2023-10-01 16:34:25 +11:00
parent 546f19d3cb
commit 50f23e1c56
17 changed files with 1110 additions and 152 deletions

View File

@ -529,13 +529,13 @@ impl DBTrans {
Ok(())
}
pub async fn find_dynzone_for_user(&self, username: &str) -> DResult<Vec<Arc<Item>>> {
pub async fn find_dynzone_for_owner(&self, owner: &str) -> DResult<Vec<Arc<Item>>> {
Ok(self
.pg_trans()?
.query(
"SELECT details FROM items WHERE details->>'owner' = $1 \
AND details->>'item_type' = 'dynzone'",
&[&format!("player/{}", username.to_lowercase())],
&[&owner],
)
.await?
.into_iter()
@ -1522,6 +1522,26 @@ impl DBTrans {
.collect())
}
pub async fn get_corps_for_user(&self, username: &str) -> DResult<Vec<(CorpId, Corp)>> {
Ok(self
.pg_trans()?
.query(
"SELECT m.corp_id, c.details FROM corp_membership m \
JOIN corps c ON c.corp_id = m.corp_id WHERE m.member_username = $1 \
AND m.details->>'joined_at' IS NOT NULL \
ORDER BY (m.details->>'priority')::int DESC NULLS LAST, \
(m.details->>'joined_at') :: TIMESTAMPTZ ASC",
&[&username.to_lowercase()],
)
.await?
.into_iter()
.filter_map(|row| match serde_json::from_value(row.get(1)) {
Err(_) => None,
Ok(j) => Some((CorpId(row.get(0)), j)),
})
.collect())
}
pub async fn get_default_corp_for_user(
&self,
username: &str,

View File

@ -54,6 +54,7 @@ pub mod movement;
pub mod open;
mod page;
pub mod parsing;
pub mod pay;
pub mod put;
mod quit;
pub mod recline;
@ -214,6 +215,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"repl" => page::VERB,
"reply" => page::VERB,
"pay" => pay::VERB,
"put" => put::VERB,
"recline" => recline::VERB,
"remove" => remove::VERB,

View File

@ -1,6 +1,7 @@
use super::{
get_player_item_or_fail, get_user_or_fail, parsing::parse_command_name, search_item_for_user,
user_error, UResult, UserVerb, UserVerbRef, VerbContext,
get_player_item_or_fail, get_user_or_fail, parsing::parse_command_name,
rent::recursively_destroy_or_move_item, search_item_for_user, user_error, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
@ -270,6 +271,13 @@ async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
.delete_corp_membership(&corpid, &user.username)
.await?;
if delete_corp {
for dynzone in ctx
.trans
.find_dynzone_for_owner(&format!("corp/{}", &corp.name))
.await?
{
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_corp(&corpid).await?;
}
Ok(())
@ -556,8 +564,8 @@ async fn corp_info(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
(Utc::now() - corp.founded).num_seconds() as u64,
));
msg.push_str(&format!(
ansi!("<bold>{}'s essential information<reset>\nFounded: {} ago\n"),
&corp.name, &founded_ago
ansi!("<bold>{}'s essential information<reset>\nFounded: {} ago\nCredits: ${}\nTax rate: {}%\n"),
&corp.name, &founded_ago, corp.credits, corp.tax as f64 * 1E-2
));
let members = ctx.trans.list_corp_members(&corp_id).await?;
if corp.allow_combat_required {
@ -770,8 +778,43 @@ async fn corp_config(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
)
.await?;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
} else if remaining.starts_with("tax rate ") {
let remaining = remaining[("tax rate ".len())..].trim();
let remaining = match remaining.split_once("%") {
None => remaining,
Some((r, e)) => {
if e != "" {
user_error("Tax rate must be a number".to_owned())?
} else {
user_error("Configurations you can set:\n\tallow combat required\n\tallow combat not required\nbase permissions permission_name permission_name".to_owned())?;
r.trim()
}
}
};
let rate: f32 = match remaining.parse::<f32>() {
Err(_) => user_error("Tax rate must be a number".to_owned())?,
Ok(r) => r,
};
if rate < 0.0 || rate >= 80.0 {
user_error("Tax rate must be 0-80%".to_owned())?;
}
corp.tax = (rate * 100.0) as u16;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
"{} has changed the tax rate for {} to {}%\n",
user.username,
corp.name,
(corp.tax as f64) / 100.0
),
)
.await?;
} else {
user_error("Configurations you can set:\n\tallow combat required\n\tallow combat not required\nbase permissions permission_name permission_name\ntax rate 12.34%".to_owned())?;
}
Ok(())

View File

@ -128,7 +128,11 @@ 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? {
for dynzone in ctx
.trans
.find_dynzone_for_owner(&format!("player/{}", &username.to_lowercase()))
.await?
{
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_user(&username).await?;

View File

@ -1,13 +1,17 @@
use super::allow::{AllowCommand, ConsentDetails, ConsentTarget};
use super::{
allow::{AllowCommand, ConsentDetails, ConsentTarget},
pay::{FinancialAccount, PaymentRequest},
};
use crate::models::consent::ConsentType;
use ansi::{ansi, strip_special_characters};
use nom::{
branch::alt,
bytes::complete::{take_till, take_till1, take_while},
character::complete::{alpha1, char, one_of, space0, space1, u16, u8},
combinator::{eof, fail, recognize},
bytes::complete::tag,
bytes::complete::{take_till, take_till1, take_while, take_while1},
character::complete::{alpha1, char, one_of, space0, space1, u16, u64, u8},
combinator::{cut, eof, fail, map, opt, peek, recognize},
error::{context, VerboseError, VerboseErrorKind},
sequence::terminated,
sequence::{pair, preceded, terminated},
IResult,
};
@ -258,6 +262,67 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
})
}
pub fn parse_payment_request(
input: &str,
my_account: &FinancialAccount,
) -> Result<PaymentRequest<FinancialAccount>, &'static str> {
let input = input.trim_start();
let account_parser = || {
context(
"Expected account to pay to/from to be username, corpname, or me",
cut(terminated(
alt((
map(tag::<&str, &str, VerboseError<&str>>("treasury"), |_| {
FinancialAccount::Treasury
}),
map(tag("me"), |_| my_account.clone()),
map(
take_while1(|c: char| c.is_alphanumeric() || c == '_'),
|n: &str| FinancialAccount::NamedAccount(n.to_owned()),
),
)),
peek(alt((eof, space1))),
)),
)
};
let res = pair(
context(
"No amount to pay found",
preceded(opt(char('$')), terminated(u64, space1)),
),
pair(
opt(preceded(
tag("from"),
preceded(space1, terminated(account_parser(), space1)),
)),
terminated(
context(
"You need to specify who to pay with \"to\"",
preceded(preceded(tag("to"), space1), account_parser()),
),
eof,
),
),
)(input);
const CATCHALL_ERROR: &'static str = "Usage: \"pay $123 from account to account\". Leave off from to default to from you. Account can be \"me\" or a username or corpname";
match res {
Ok((_, (amount, (from, to)))) => Ok(PaymentRequest {
amount,
from: from.unwrap_or_else(|| my_account.clone()),
to,
}),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(e
.errors
.into_iter()
.find_map(|k| match k.1 {
VerboseErrorKind::Context(s) => Some(s),
_ => None,
})
.unwrap_or(CATCHALL_ERROR)),
Err(_) => Err(CATCHALL_ERROR),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -412,4 +477,78 @@ mod tests {
}
}))
}
#[test]
fn parse_payment_request_happy_path_works() {
assert_eq!(
super::parse_payment_request(
"$10 to mafia",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Ok(PaymentRequest {
amount: 10,
from: FinancialAccount::NamedAccount("namey".to_owned()),
to: FinancialAccount::NamedAccount("mafia".to_owned())
})
)
}
#[test]
fn parse_payment_request_explicit_from_works() {
assert_eq!(
super::parse_payment_request(
"10 from mafia to treasury",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Ok(PaymentRequest {
amount: 10,
from: FinancialAccount::NamedAccount("mafia".to_owned()),
to: FinancialAccount::Treasury
})
)
}
#[test]
fn parse_payment_wrong_start_err() {
assert_eq!(
super::parse_payment_request(
"nonsense",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("No amount to pay found")
)
}
#[test]
fn parse_payment_wrong_from_err() {
assert_eq!(
super::parse_payment_request(
"10 from ! to me",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("Expected account to pay to/from to be username, corpname, or me")
)
}
#[test]
fn parse_payment_wrong_to_err() {
assert_eq!(
super::parse_payment_request(
"10 from me for fun",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("You need to specify who to pay with \"to\"")
)
}
#[test]
fn parse_payment_trailing_junk_err() {
assert_eq!(
super::parse_payment_request(
"10 from me to me for lolz",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("Usage: \"pay $123 from account to account\". Leave off from to default to from you. Account can be \"me\" or a username or corpname")
)
}
}

View File

@ -0,0 +1,267 @@
use super::{
corp::check_corp_perm, get_user_or_fail, parsing::parse_payment_request, user_error, UResult,
UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{Corp, CorpCommType, CorpId, CorpPermission},
user::{User, UserFlag},
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use log::info;
use mockall_double::double;
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum FinancialAccount {
NamedAccount(String),
Treasury, // Staff only source of unlimited funds
}
#[derive(Clone, PartialEq, Debug)]
pub enum ResolvedFinancialAccount {
CorpAccount((CorpId, Corp)),
UserAccount(User),
Treasury,
}
#[derive(Clone, PartialEq, Debug)]
pub struct PaymentRequest<AccDetails> {
pub from: AccDetails,
pub to: AccDetails,
pub amount: u64,
}
async fn resolve_account(
trans: &DBTrans,
inp: &FinancialAccount,
) -> UResult<ResolvedFinancialAccount> {
match inp {
FinancialAccount::Treasury => Ok(ResolvedFinancialAccount::Treasury),
FinancialAccount::NamedAccount(ref name) => {
match trans.find_corp_by_name(name.as_str()).await? {
Some(corp) => Ok(ResolvedFinancialAccount::CorpAccount(corp)),
None => match trans.find_by_username(name.as_str()).await? {
Some(user) => Ok(ResolvedFinancialAccount::UserAccount(user)),
None => user_error(format!(
"I couldn't find a user or corp matching \"{}\"",
name.as_str()
)),
},
}
}
}
}
async fn resolve_payment_request(
trans: &DBTrans,
inp: &PaymentRequest<FinancialAccount>,
) -> UResult<PaymentRequest<ResolvedFinancialAccount>> {
Ok(PaymentRequest {
from: resolve_account(trans, &inp.from).await?,
to: resolve_account(trans, &inp.to).await?,
amount: inp.amount.clone(),
})
}
async fn verify_access(
trans: &DBTrans,
user: &User,
inp: &PaymentRequest<ResolvedFinancialAccount>,
) -> UResult<()> {
if inp.from == inp.to {
user_error("That seems a bit like a money-go-round!".to_owned())?;
}
if inp.from == ResolvedFinancialAccount::Treasury
|| inp.to == ResolvedFinancialAccount::Treasury
{
if !user.user_flags.contains(&UserFlag::Staff) {
user_error("I'm sorry Dave, I can't let you do that!".to_owned())?;
}
// Treasury deliberately bypasses all other access checks, but
// even staff have to follow the rules for payments not involving
// treasury.
} else {
match inp.from {
ResolvedFinancialAccount::UserAccount(ref usr) => {
if usr.username != user.username {
user_error("Sadly, it appears the wristpad system won't let you access other people's funds.".to_owned())?;
}
}
ResolvedFinancialAccount::CorpAccount(ref corp) => {
match trans
.match_user_corp_by_name(&corp.1.name, &user.username)
.await?
{
None => user_error(format!("You're not even a member of {}!", &corp.1.name))?,
Some((_, _, mem)) => {
if !check_corp_perm(&CorpPermission::Finance, &mem) {
user_error(format!(
"You lack finance permissions in {}",
&corp.1.name
))?;
}
}
}
}
_ => user_error("Oops, can't verify you have access to pay".to_owned())?,
}
}
Ok(())
}
fn get_name(acc: &ResolvedFinancialAccount) -> String {
match acc {
ResolvedFinancialAccount::Treasury => "Treasury".to_owned(),
ResolvedFinancialAccount::UserAccount(ref usr) => usr.username.clone(),
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.name.clone(),
}
}
fn get_balance(acc: &ResolvedFinancialAccount) -> u64 {
match acc {
ResolvedFinancialAccount::Treasury => 10000000000,
ResolvedFinancialAccount::UserAccount(ref usr) => usr.credits,
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.credits,
}
}
async fn modify_balance<F>(trans: &DBTrans, acc: &mut ResolvedFinancialAccount, f: F) -> DResult<()>
where
F: Fn(u64) -> u64,
{
match acc {
ResolvedFinancialAccount::Treasury => {}
ResolvedFinancialAccount::UserAccount(ref mut usr) => {
usr.credits = f(usr.credits);
trans.save_user_model(&usr).await?;
}
ResolvedFinancialAccount::CorpAccount(ref mut corp) => {
corp.1.credits = f(corp.1.credits);
trans.update_corp_details(&corp.0, &corp.1).await?;
}
}
Ok(())
}
fn involves_treasury(req: &PaymentRequest<ResolvedFinancialAccount>) -> bool {
req.from == ResolvedFinancialAccount::Treasury || req.to == ResolvedFinancialAccount::Treasury
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let user = get_user_or_fail(ctx)?;
let req = match parse_payment_request(
remaining,
&FinancialAccount::NamedAccount(user.username.clone()),
) {
Err(msg) => user_error(msg.to_owned())?,
Ok(r) => r,
};
if req.amount == 0 || req.amount >= 10000000000000 {
user_error(
"The wristpad seems to require a number between $1 and $10 trillion.".to_owned(),
)?;
}
let mut req = resolve_payment_request(&ctx.trans, &req).await?;
verify_access(&ctx.trans, &user, &req).await?;
if get_balance(&req.from) < req.amount {
user_error(ansi!("The wristpad plays a sad sounding beep, and a red cross comes up, with the text: <red>DECLINED Insufficient Funds<reset>. [Hint: Try having rich parents, or not spending all your credits on smashed avo, and you might be able to afford to make your payments]").to_owned())?;
}
modify_balance(&ctx.trans, &mut req.to, |a| a + req.amount).await?;
modify_balance(&ctx.trans, &mut req.from, |a| a - req.amount).await?;
let mut notify_corps: Vec<(CorpId, String)> = vec![];
match req.from {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
match req.to {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
for (corp_id, corp_name) in notify_corps {
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
ansi!("<green>[{}] {} has transferred ${} from {} to {}.<reset>\n"),
&corp_name,
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
&get_name(&req.to),
),
)
.await?;
}
match req.to {
ResolvedFinancialAccount::UserAccount(ref other_usr)
if other_usr.username != user.username =>
{
if let Some((sess, _)) = ctx
.trans
.find_session_for_player(&other_usr.username.to_lowercase())
.await?
{
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
ansi!("<green>{} has transferred ${} from {} to you.<reset>\n"),
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
)),
)
.await?;
}
}
_ => {}
}
if involves_treasury(&req) {
info!(
"{} has transferred ${} from {} to {}.",
&user.username,
req.amount,
&get_name(&req.from),
&get_name(&req.to),
);
}
ctx.trans.queue_for_session(&ctx.session,
Some(ansi!("Your wristpad displays a <green>green<reset> tick, indicating a successful transaction.\n"))).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -30,6 +30,7 @@ pub fn is_invalid_username(name: &str) -> bool {
"on",
"privileges",
"as",
"treasury",
])
});
if invalid_words.contains(name) {

View File

@ -1,11 +1,12 @@
use super::{
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
corp::check_corp_perm, get_player_item_or_fail, get_user_or_fail, user_error, UResult,
UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
@ -13,14 +14,14 @@ use crate::{
static_content::{
dynzone::dynzone_by_type,
npc::npc_by_code,
room::{room_map_by_code, Direction},
room::{room_map_by_code, Direction, RentSuiteType},
},
DResult,
};
use ansi::ansi;
use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use chrono::{DateTime, Duration, Utc};
use itertools::Itertools;
use log::info;
use mockall_double::double;
@ -73,14 +74,163 @@ pub async fn recursively_destroy_or_move_item(trans: &DBTrans, item: &Item) -> D
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>");
async fn bill_residential_room(
ctx: &mut TaskRunContext<'_>,
bill_player_code: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
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 {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
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)))
}
async fn bill_commercial_room(
ctx: &mut TaskRunContext<'_>,
bill_corp: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
let mut bill_corp = match ctx.trans.find_corp_by_name(bill_corp).await? {
None => return Ok(None),
Some(c) => c,
};
// Check if they have enough money.
if bill_corp.1.credits < daily_price {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
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() + "/reception"))
.await?
{
None => {}
Some(reception_room) => {
let mut reception_mut = (*reception_room).clone();
reception_mut.details =
Some(reception_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE);
ctx.trans.save_item_model(&reception_mut).await?;
}
}
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<red>All wristpads of members of {} beep a sad sounding tone as the corp's landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money for the corp \
fast and have your holder hurry to reception (in the next 24 hours) and sign a new \
lease agreement for the premises, or all your corp's stuff will be gone forever!\n"),
&bill_corp.1.name,
&zone_item.display
),
).await?;
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_corp.1.credits -= daily_price;
ctx.trans
.update_corp_details(&bill_corp.0, &bill_corp.1)
.await?;
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<yellow>Your wristpad beeps as a deduction is made from {}'s account of ${} for rent for {}.<reset>\n"),
&bill_corp.1.name,
daily_price, &zone_item.display
)
).await?;
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
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 {
let (zone_item_ref, daily_price) = match ctx.task.details {
TaskDetails::ChargeRoom {
zone_item,
daily_price,
ref zone_item,
ref daily_price,
} => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?,
};
@ -115,9 +265,20 @@ impl TaskHandler for ChargeRoomTaskHandler {
_ => (),
}
let bill_player_code = match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) {
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
bill_residential_room(
ctx,
player_item_code,
daily_price.clone(),
&zone_item,
vacate_after,
)
.await
}
Some((item_type, corpname)) if item_type == "corp" => {
bill_commercial_room(ctx, corpname, daily_price.clone(), &zone_item, vacate_after)
.await
}
_ => {
info!(
@ -126,72 +287,7 @@ impl TaskHandler for ChargeRoomTaskHandler {
);
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 {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
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;
@ -205,7 +301,11 @@ impl UserVerb for Verb {
_verb: &str,
remaining: &str,
) -> UResult<()> {
let item_name = remaining.trim();
let remaining = remaining.trim();
let (item_name, corp_name) = match remaining.split_once(" for ") {
None => (remaining, None),
Some((i, c)) => (i.trim(), Some(c.trim())),
};
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
@ -235,22 +335,43 @@ impl UserVerb for Verb {
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 mut corp = match (&rentinfo.suite_type, corp_name) {
(RentSuiteType::Commercial, None) =>
user_error(format!(
ansi!("This is a commercial suite, you need to rent it in the name of a corp. Try <bold>rent {} for corpname<reset>"),
item_name
))?,
(RentSuiteType::Residential, Some(_)) =>
user_error("This is a residential suite, you can't rent it for a corp. Try finding a commercial suite for your corp, or rent it personally.".to_owned())?,
(RentSuiteType::Residential, None) => None,
(RentSuiteType::Commercial, Some(n)) => match ctx.trans.match_user_corp_by_name(&n, &user.username).await? {
None => user_error("I can't find that corp in your list of corps!".to_owned())?,
Some((_, _, mem)) if !check_corp_perm(&CorpPermission::Holder, &mem) => user_error("You don't have holder permissions in that corp.".to_owned())?,
Some((corp_id, corp, _)) => Some((corp_id, corp))
},
};
match corp.as_ref() {
None 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())?,
Some((_, c)) if c.credits < rentinfo.setup_fee =>
user_error("The robot rolls its eyes at you derisively. \"I don't think so - your corp 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())
})?;
let exit = Direction::IN {
item: corp
.as_ref()
.map(|c| c.1.name.clone())
.unwrap_or_else(|| player_item.display.clone()),
};
match ctx
.trans
.find_exact_dyn_exit(
&player_item.location,
&Direction::IN {
item: player_item.display.clone(),
},
)
.find_exact_dyn_exit(&player_item.location, &exit)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
@ -273,9 +394,39 @@ impl UserVerb for Verb {
vacate_after: Some(_),
zone_exit: ref ex,
}) => {
match corp.as_mut() {
None => {
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?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just cancelled plans to vacate a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
ctx.trans
.save_item_model(&Item {
@ -291,7 +442,7 @@ impl UserVerb for Verb {
.trans
.find_item_by_type_code(
"dynroom",
&(ex_zone.item_code.clone() + "/doorstep"),
&(ex_zone.item_code.clone() + "/" + zone.entrypoint_subcode),
)
.await?
{
@ -312,9 +463,8 @@ impl UserVerb for Verb {
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?;
"\"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")
))).await?;
return Ok(());
}
_ => {}
@ -323,15 +473,17 @@ impl UserVerb for Verb {
}
}
let owner = corp
.as_ref()
.map(|c| format!("corp/{}", c.1.name))
.unwrap_or_else(|| player_item.refstr());
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(),
},
&owner,
&exit,
)
.await?;
@ -351,6 +503,8 @@ impl UserVerb for Verb {
})
.await?;
match corp.as_mut() {
None => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
@ -363,6 +517,25 @@ impl UserVerb for Verb {
)),
)
.await?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just rented a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
Ok(())
}

View File

@ -1,10 +1,15 @@
use super::{
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
corp::check_corp_perm, get_player_item_or_fail, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, ItemSpecialData},
static_content::room::{room_map_by_code, Direction},
models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
},
static_content::room::{room_map_by_code, Direction, RentSuiteType},
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools;
@ -18,7 +23,11 @@ impl UserVerb for Verb {
_verb: &str,
remaining: &str,
) -> UResult<()> {
let item_name = remaining.trim();
let remaining = remaining.trim();
let (item_name, corp_name) = match remaining.split_once(" for ") {
None => (remaining, None),
Some((i, c)) => (i.trim(), Some(c.trim())),
};
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
@ -35,7 +44,7 @@ impl UserVerb for Verb {
if room.rentable_dynzone.is_empty() {
user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?;
}
match room
let rentinfo = match room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
@ -47,17 +56,34 @@ impl UserVerb for Verb {
.map(|ri| ri.rent_what)
.join(", ")
))?,
Some(_) => (),
Some(v) => v,
};
let corp = match (&rentinfo.suite_type, corp_name) {
(RentSuiteType::Commercial, None) =>
user_error(format!(
ansi!("This is a commercial suite, you need to vacate it using the name of the corp. Try <bold>vacate {} for corpname<reset>"),
item_name
))?,
(RentSuiteType::Residential, Some(_)) =>
user_error("This is a residential suite, you can't vacate it for a corp. Try <bold>vacate {}<reset>".to_owned())?,
(RentSuiteType::Residential, None) => None,
(RentSuiteType::Commercial, Some(n)) => match ctx.trans.match_user_corp_by_name(&n, &player_item.item_code).await? {
None => user_error("I can't find that corp in your list of corps!".to_owned())?,
Some((_, _, mem)) if !check_corp_perm(&CorpPermission::Holder, &mem) => user_error("You don't have holder permissions in that corp.".to_owned())?,
Some((corp_id, corp, _)) => Some((corp_id, corp))
},
};
let exit = Direction::IN {
item: corp
.as_ref()
.map(|c| c.1.name.clone())
.unwrap_or_else(|| player_item.display.clone()),
};
match ctx
.trans
.find_exact_dyn_exit(
&player_item.location,
&Direction::IN {
item: player_item.display.clone(),
},
)
.find_exact_dyn_exit(&player_item.location, &exit)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
@ -87,7 +113,22 @@ impl UserVerb for Verb {
..(*ex_zone).clone()
})
.await?;
ctx.trans.queue_for_session(ctx.session, Some("The robot files away your notice of intention to vacate. \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"\n")).await?
ctx.trans.queue_for_session(ctx.session, Some("The robot files away your notice of intention to vacate. \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"\n")).await?;
match corp {
None => {},
Some(corptup) =>
ctx.trans.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
Some(&player_item.item_code),
&format!(
ansi!("<cyan>[{}] {} just gave notice to vacate {}! The landlord replied with: \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"<reset>\n"),
&corptup.1.name,
&player_item.display_for_sentence(true, 1, false),
&ex_zone.display
)
).await?
}
}
_ => user_error("The premises seem to be broken anyway".to_owned())?,
}

View File

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone, Debug)]
pub enum CorpPermission {
Holder, // Implies all permissions.
Hire,
@ -75,9 +75,10 @@ impl CorpCommType {
}
}
#[derive(Eq, Clone, PartialEq, Debug)]
pub struct CorpId(pub i64);
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)]
#[serde(default)]
pub struct Corp {
pub name: String,
@ -87,6 +88,8 @@ pub struct Corp {
// consent to combat with other corps, and have it apply to members.
pub allow_combat_required: bool,
pub member_permissions: Vec<CorpPermission>,
pub credits: u64,
pub tax: u16, // 0-10000, each representing 0.01%
}
impl Default for Corp {
@ -96,6 +99,8 @@ impl Default for Corp {
allow_combat_required: false,
member_permissions: vec![],
founded: Utc::now(),
credits: 0,
tax: 1000,
}
}
}

View File

@ -15,7 +15,7 @@ pub enum JournalType {
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub enum JournalInProgress {}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(default)]
pub struct JournalState {
pub completed_journals: BTreeSet<JournalType>,

View File

@ -11,14 +11,14 @@ use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct UserTermData {
pub accepted_terms: BTreeMap<String, DateTime<Utc>>,
pub terms_complete: bool, // Recalculated on accept and login.
pub last_presented_term: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(default)]
pub struct UserExperienceData {
pub spent_xp: u64, // Since last chargen complete.
@ -67,7 +67,7 @@ pub fn wristpad_hack_data() -> &'static BTreeMap<WristpadHack, WristpadHackData>
})
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct User {
pub username: String,

View File

@ -6,6 +6,7 @@ use crate::{
UResult,
},
models::{
corp::CorpCommType,
item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack},
journal::JournalType,
task::{Task, TaskDetails, TaskMeta},
@ -426,17 +427,48 @@ pub async fn consider_reward_for(
xp_gain
};
// Not intended to be saved, just a convenience to describe it before it was dead.
let mut for_item_sim_notdead = for_item.clone();
for_item_sim_notdead.death_data = None;
// Now consider kill bonuses...
if for_item.item_type == "npc" {
if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) {
if let Some(bonus) = &npc.kill_bonus {
user.credits += bonus.payment;
let mut user_payment = bonus.payment;
for (corpid, mut corp) in trans.get_corps_for_user(&by_item.item_code).await? {
let actual_tax = (corp.tax as f64 * 0.0001 * (bonus.payment as f64))
.min(user_payment as f64) as u64;
if actual_tax > 0 {
user_payment -= actual_tax;
corp.credits += actual_tax;
trans.update_corp_details(&corpid, &corp).await?;
trans
.broadcast_to_corp(
&corpid,
&CorpCommType::Reward,
None,
&format!(
"{} earned ${} (of which ${} went to {}) for killing {}.\n",
by_item.display,
bonus.payment,
actual_tax,
corp.name,
&for_item_sim_notdead.display_for_sentence(true, 1, false)
),
)
.await?;
}
// Consider a notice to corp that missed out on tax due to other corps,
// so they can consider a chat with the member about the situation?
}
user.credits += user_payment;
trans
.queue_for_session(
&session,
Some(&format!(
"{}\nYour wristpad beeps for a credit of {} for that.\n",
bonus.msg, bonus.payment
"{}\nYour wristpad beeps for a credit of ${}.\n",
bonus.msg, user_payment
)),
)
.await?;
@ -447,10 +479,7 @@ pub async fn consider_reward_for(
trans.save_user_model(&user).await?;
if xp_gain == 0 {
trans
.queue_for_session(
&session,
Some("[You didn't gain any experience for that]\n"),
)
.queue_for_session(&session, Some("[You didn't gain any experience points]\n"))
.await?;
} else {
trans

View File

@ -14,22 +14,26 @@ use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
mod cokmurl_apartment;
mod murl_deluxe_corporate;
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
pub enum DynzoneType {
CokMurlApartment,
MurlDeluxeCorporate,
}
impl DynzoneType {
pub fn from_str(i: &str) -> Option<Self> {
match i {
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
"MurlDeluxeCorporate" => Some(DynzoneType::MurlDeluxeCorporate),
_ => None,
}
}
pub fn to_str(&self) -> &'static str {
match self {
DynzoneType::CokMurlApartment => "CokMurlApartment",
DynzoneType::MurlDeluxeCorporate => "MurlDeluxeCorporate",
}
}
}
@ -49,7 +53,7 @@ impl Dynzone {
trans: &DBTrans,
connect_where: &str,
dup_message: &str,
new_owner: &Item,
owner: &str,
new_exit_direction: &Direction,
) -> UResult<String> {
// Check exit not taken...
@ -60,7 +64,6 @@ impl Dynzone {
{
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
@ -72,7 +75,7 @@ impl Dynzone {
zone_exit: Some(connect_where.to_owned()),
vacate_after: None,
}),
owner: Some(owner.clone()),
owner: Some(owner.to_owned()),
location: format!("dynzone/{}", &code),
..Default::default()
})
@ -108,7 +111,7 @@ impl Dynzone {
None
},
flags: room.item_flags.clone(),
owner: Some(owner.clone()),
owner: Some(owner.to_owned()),
door_states: Some(
room.exits
.iter()
@ -202,7 +205,7 @@ impl Default for Dynroom {
pub fn dynzone_list() -> &'static Vec<Dynzone> {
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
CELL.get_or_init(|| vec![cokmurl_apartment::zone()])
CELL.get_or_init(|| vec![cokmurl_apartment::zone(), murl_deluxe_corporate::zone()])
}
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {

View File

@ -0,0 +1,210 @@
use super::{super::room::GridCoords, Dynroom, Dynzone, DynzoneType, Exit, ExitTarget, ExitType};
use crate::models::item::ItemFlag;
use crate::static_content::room::Direction;
pub fn zone() -> Dynzone {
Dynzone {
zonetype: DynzoneType::MurlDeluxeCorporate,
zonename: "Murlison Suites",
entrypoint_subcode: "reception",
dyn_rooms: vec!(
("reception", Dynroom {
subcode: "reception",
name: "Reception",
short: "DS",
description: "A narrow public area of the corporate suite, with views \
of the bleak wasteland to the north. The walls are painted white, and a long reception \
desk, enamelled to look like dark wood, is built into the room. It is well lit \
with cool white LED lights recessed into the ceiling above",
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: "commons" },
exit_type: ExitType::Doorless
}
),
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
should_caption: true,
item_flags: vec!(),
..Default::default()
}),
("commons", Dynroom {
subcode: "commons",
name: "Common Room",
short: "CO",
description: "A large central room, carpeted with floor tiles alternating between light and dark grey in a checkerboard pattern. The walls are painted white, and the room is dimly lit from above with warm white LED bulbs",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::WEST,
target: ExitTarget::Intrazone { subcode: "reception" },
exit_type: ExitType::Doored { description: "a very sturdy looking fire-rated solid core navy blue painted door" }
},
Exit {
direction: Direction::NORTH,
target: ExitTarget::Intrazone { subcode: "northwing" },
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::EAST,
target: ExitTarget::Intrazone { subcode: "eastwing" },
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::SOUTHWEST,
target: ExitTarget::Intrazone { subcode: "healthcentre" },
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::SOUTH,
target: ExitTarget::Intrazone { subcode: "kitchen" },
exit_type: ExitType::Doorless
},
),
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("northwing", Dynroom {
subcode: "northwing",
name: "North Wing",
short: "NW",
description: "A spacious office, carpeted with floor tiles alternating between light and dark grey in a checkerboard pattern. The walls are painted white, and the room is dimly lit from above with warm white LED bulbs",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::NORTH,
target: ExitTarget::Intrazone { subcode: "northbalcony" },
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::SOUTH,
target: ExitTarget::Intrazone { subcode: "commons" },
exit_type: ExitType::Doorless
},
),
grid_coords: GridCoords { x: 1, y: -1, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("northbalcony", Dynroom {
subcode: "northbalcony",
name: "North Balcony",
short: "NB",
description: "A timber balcony cantilevered on to the north side of the building, complete with an auto-irrigated garden, and glass front panels exposing a breath-taking view of the desolate lands to the north",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::SOUTH,
target: ExitTarget::Intrazone { subcode: "commons" },
exit_type: ExitType::Doored { description: "a pair of sliding glass doors" }
},
),
grid_coords: GridCoords { x: 1, y: -2, z: 0 },
should_caption: true,
has_power: false,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("eastwing", Dynroom {
subcode: "eastwing",
name: "East Wing",
short: "EW",
description: "A spacious office, carpeted with floor tiles alternating between light and dark grey in a checkerboard pattern. The walls are painted white, and the room is dimly lit from above with warm white LED bulbs",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::EAST,
target: ExitTarget::Intrazone { subcode: "storeroom" },
exit_type: ExitType::Doorless
},
Exit {
direction: Direction::WEST,
target: ExitTarget::Intrazone { subcode: "commons" },
exit_type: ExitType::Doorless
},
),
grid_coords: GridCoords { x: 2, y: 0, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("storeroom", Dynroom {
subcode: "storeroom",
name: "Store room",
short: "SR",
description: "A strong-room apparently designed to preserve people or things in any emergency. The walls, floor, and ceiling all appear to be made of thick steel",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::WEST,
target: ExitTarget::Intrazone { subcode: "eastwing" },
exit_type: ExitType::Doored { description: "a heavy thick-plated steel door" }
},
),
grid_coords: GridCoords { x: 3, y: 0, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("healthcentre", Dynroom {
subcode: "healthcentre",
name: "Health centre",
short: "HC",
description: "A room apparently designed as a kind of corporate sick-bay. The floor is tiled, and the walls are painted with white glossy easy-wipe paint",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::NORTHEAST,
target: ExitTarget::Intrazone { subcode: "commons" },
exit_type: ExitType::Doorless
},
),
grid_coords: GridCoords { x: 0, y: 1, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
("kitchen", Dynroom {
subcode: "kitchen",
name: "Kitchen",
short: "KT",
description: "A room apparently designed for food preparation. It is tiled with small light green tiles that cover the walls and floor, and has an abundance of water and drain fittings",
description_less_explicit: None,
exits: vec!(
Exit {
direction: Direction::NORTH,
target: ExitTarget::Intrazone { subcode: "commons" },
exit_type: ExitType::Doorless
},
),
grid_coords: GridCoords { x: 1, y: 1, z: 0 },
should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
}),
).into_iter().collect(),
..Default::default()
}
}

View File

@ -309,8 +309,14 @@ impl Default for RoomStock {
}
}
pub enum RentSuiteType {
Residential,
Commercial,
}
pub struct RentInfo {
pub rent_what: &'static str,
pub suite_type: RentSuiteType,
pub dynzone: super::dynzone::DynzoneType,
pub daily_price: u64,
pub setup_fee: u64,

View File

@ -1,4 +1,6 @@
use super::{Direction, Exit, ExitTarget, GridCoords, RentInfo, Room, SecondaryZoneRecord};
use super::{
Direction, Exit, ExitTarget, GridCoords, RentInfo, RentSuiteType, Room, SecondaryZoneRecord,
};
use crate::static_content::dynzone::DynzoneType;
use ansi::ansi;
pub fn room_list() -> Vec<Room> {
@ -34,6 +36,7 @@ pub fn room_list() -> Vec<Room> {
should_caption: true,
rentable_dynzone: vec!(RentInfo {
rent_what: "studio",
suite_type: RentSuiteType::Residential,
dynzone: DynzoneType::CokMurlApartment,
daily_price: 20,
setup_fee: 40,
@ -47,9 +50,21 @@ pub fn room_list() -> Vec<Room> {
code: "murl_lobby",
name: "Murlison Suites Commercial Lobby",
short: ansi!("<bgyellow><black>ML<reset>"),
description: "A sleek reception that could have been the bridge of a 2000s era sci-fi spaceship. Linished metal plates are lit up by ambient blue LEDs, while stone tiles cover the floor",
description: ansi!(
"A sleek reception that could have been the bridge of a 2000s era sci-fi spaceship. Linished metal plates are lit up by ambient blue LEDs, while stone tiles cover the floor. You see a white android, complete with elegant rounded corners and glowing blue eyes.\n\n\
\"Welcome to Murlison Suites - the best a business can rent\", purs the bot pleasantly. \"Just say <bold>in<reset> corpname\" and I'll guide you to the suite for that corp before you \
can blink! Or if you hold a corp and would like to rent the best suite money can \
buy for it, just say <bold>rent deluxe for<reset> corpname, and I'll set you up\""
),
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
rentable_dynzone: vec!(RentInfo {
rent_what: "deluxe",
suite_type: RentSuiteType::Commercial,
dynzone: DynzoneType::MurlDeluxeCorporate,
daily_price: 200,
setup_fee: 400,
}),
exits: vec!(
Exit {
direction: Direction::WEST,