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(()) 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 Ok(self
.pg_trans()? .pg_trans()?
.query( .query(
"SELECT details FROM items WHERE details->>'owner' = $1 \ "SELECT details FROM items WHERE details->>'owner' = $1 \
AND details->>'item_type' = 'dynzone'", AND details->>'item_type' = 'dynzone'",
&[&format!("player/{}", username.to_lowercase())], &[&owner],
) )
.await? .await?
.into_iter() .into_iter()
@ -1522,6 +1522,26 @@ impl DBTrans {
.collect()) .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( pub async fn get_default_corp_for_user(
&self, &self,
username: &str, username: &str,

View File

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

View File

@ -1,6 +1,7 @@
use super::{ use super::{
get_player_item_or_fail, get_user_or_fail, parsing::parse_command_name, search_item_for_user, get_player_item_or_fail, get_user_or_fail, parsing::parse_command_name,
user_error, UResult, UserVerb, UserVerbRef, VerbContext, rent::recursively_destroy_or_move_item, search_item_for_user, user_error, UResult, UserVerb,
UserVerbRef, VerbContext,
}; };
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
@ -270,6 +271,13 @@ async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
.delete_corp_membership(&corpid, &user.username) .delete_corp_membership(&corpid, &user.username)
.await?; .await?;
if delete_corp { 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?; ctx.trans.delete_corp(&corpid).await?;
} }
Ok(()) Ok(())
@ -556,8 +564,8 @@ async fn corp_info(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
(Utc::now() - corp.founded).num_seconds() as u64, (Utc::now() - corp.founded).num_seconds() as u64,
)); ));
msg.push_str(&format!( msg.push_str(&format!(
ansi!("<bold>{}'s essential information<reset>\nFounded: {} ago\n"), ansi!("<bold>{}'s essential information<reset>\nFounded: {} ago\nCredits: ${}\nTax rate: {}%\n"),
&corp.name, &founded_ago &corp.name, &founded_ago, corp.credits, corp.tax as f64 * 1E-2
)); ));
let members = ctx.trans.list_corp_members(&corp_id).await?; let members = ctx.trans.list_corp_members(&corp_id).await?;
if corp.allow_combat_required { if corp.allow_combat_required {
@ -770,8 +778,43 @@ async fn corp_config(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()>
) )
.await?; .await?;
ctx.trans.update_corp_details(&corp_id, &corp).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 {
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 { } else {
user_error("Configurations you can set:\n\tallow combat required\n\tallow combat not required\nbase permissions permission_name permission_name".to_owned())?; 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(()) Ok(())

View File

@ -128,7 +128,11 @@ impl TaskHandler for DestroyUserHandler {
}; };
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?; cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
destroy_container(&ctx.trans, &player_item).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?; recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
} }
ctx.trans.delete_user(&username).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 crate::models::consent::ConsentType;
use ansi::{ansi, strip_special_characters}; use ansi::{ansi, strip_special_characters};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::complete::{take_till, take_till1, take_while}, bytes::complete::tag,
character::complete::{alpha1, char, one_of, space0, space1, u16, u8}, bytes::complete::{take_till, take_till1, take_while, take_while1},
combinator::{eof, fail, recognize}, character::complete::{alpha1, char, one_of, space0, space1, u16, u64, u8},
combinator::{cut, eof, fail, map, opt, peek, recognize},
error::{context, VerboseError, VerboseErrorKind}, error::{context, VerboseError, VerboseErrorKind},
sequence::terminated, sequence::{pair, preceded, terminated},
IResult, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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", "on",
"privileges", "privileges",
"as", "as",
"treasury",
]) ])
}); });
if invalid_words.contains(name) { if invalid_words.contains(name) {

View File

@ -1,11 +1,12 @@
use super::{ use super::{
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserError, UserVerb, corp::check_corp_perm, get_player_item_or_fail, get_user_or_fail, user_error, UResult,
UserVerbRef, VerbContext, UserError, UserVerb, UserVerbRef, VerbContext,
}; };
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::{ use crate::{
models::{ models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData}, item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta}, task::{Task, TaskDetails, TaskMeta},
}, },
@ -13,14 +14,14 @@ use crate::{
static_content::{ static_content::{
dynzone::dynzone_by_type, dynzone::dynzone_by_type,
npc::npc_by_code, npc::npc_by_code,
room::{room_map_by_code, Direction}, room::{room_map_by_code, Direction, RentSuiteType},
}, },
DResult, DResult,
}; };
use ansi::ansi; use ansi::ansi;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use itertools::Itertools; use itertools::Itertools;
use log::info; use log::info;
use mockall_double::double; 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>"); 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; pub struct ChargeRoomTaskHandler;
#[async_trait] #[async_trait]
impl TaskHandler for ChargeRoomTaskHandler { impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { 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 { TaskDetails::ChargeRoom {
zone_item, ref zone_item,
daily_price, ref daily_price,
} => (zone_item, daily_price), } => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?, _ => 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" => { 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!( info!(
@ -126,72 +287,7 @@ impl TaskHandler for ChargeRoomTaskHandler {
); );
return Ok(None); 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; pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeRoomTaskHandler;
@ -205,7 +301,11 @@ impl UserVerb for Verb {
_verb: &str, _verb: &str,
remaining: &str, remaining: &str,
) -> UResult<()> { ) -> 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 player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item let (loc_type, loc_code) = player_item
.location .location
@ -235,22 +335,43 @@ impl UserVerb for Verb {
Some(v) => v, Some(v) => v,
}; };
let user = get_user_or_fail(ctx)?; let user = get_user_or_fail(ctx)?;
if user.credits < rentinfo.setup_fee { let mut corp = match (&rentinfo.suite_type, corp_name) {
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())? (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(|| { 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()) 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 match ctx
.trans .trans
.find_exact_dyn_exit( .find_exact_dyn_exit(&player_item.location, &exit)
&player_item.location,
&Direction::IN {
item: player_item.display.clone(),
},
)
.await? .await?
.as_ref() .as_ref()
.and_then(|it| it.location.split_once("/")) .and_then(|it| it.location.split_once("/"))
@ -273,9 +394,39 @@ impl UserVerb for Verb {
vacate_after: Some(_), vacate_after: Some(_),
zone_exit: ref ex, zone_exit: ref ex,
}) => { }) => {
let mut user_mut = user.clone(); match corp.as_mut() {
user_mut.credits -= rentinfo.setup_fee; None => {
ctx.trans.save_user_model(&user_mut).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?;
}
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 ctx.trans
.save_item_model(&Item { .save_item_model(&Item {
@ -291,7 +442,7 @@ impl UserVerb for Verb {
.trans .trans
.find_item_by_type_code( .find_item_by_type_code(
"dynroom", "dynroom",
&(ex_zone.item_code.clone() + "/doorstep"), &(ex_zone.item_code.clone() + "/" + zone.entrypoint_subcode),
) )
.await? .await?
{ {
@ -312,9 +463,8 @@ impl UserVerb for Verb {
ctx.trans.queue_for_session( ctx.trans.queue_for_session(
ctx.session, ctx.session,
Some(&format!(ansi!( 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\ "\"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?;
).await?;
return Ok(()); 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 let zonecode = zone
.create_instance( .create_instance(
ctx.trans, ctx.trans,
&player_item.location, &player_item.location,
"You can only rent one apartment here, and you already have one!", "You can only rent one apartment here, and you already have one!",
&player_item, &owner,
&Direction::IN { &exit,
item: player_item.display.clone(),
},
) )
.await?; .await?;
@ -351,18 +503,39 @@ impl UserVerb for Verb {
}) })
.await?; .await?;
let mut user_mut = user.clone(); match corp.as_mut() {
user_mut.credits -= rentinfo.setup_fee; None => {
ctx.trans.save_user_model(&user_mut).await?; let mut user_mut = user.clone();
ctx.trans user_mut.credits -= rentinfo.setup_fee;
.queue_for_session( ctx.trans.save_user_model(&user_mut).await?;
ctx.session, ctx.trans
Some(&format!( .queue_for_session(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), ctx.session,
rentinfo.setup_fee Some(&format!(
)), ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
) rentinfo.setup_fee
.await?; )),
)
.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(()) Ok(())
} }

View File

@ -1,10 +1,15 @@
use super::{ 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::{ use crate::{
models::item::{Item, ItemSpecialData}, models::{
static_content::room::{room_map_by_code, Direction}, corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
},
static_content::room::{room_map_by_code, Direction, RentSuiteType},
}; };
use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use itertools::Itertools; use itertools::Itertools;
@ -18,7 +23,11 @@ impl UserVerb for Verb {
_verb: &str, _verb: &str,
remaining: &str, remaining: &str,
) -> UResult<()> { ) -> 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 player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item let (loc_type, loc_code) = player_item
.location .location
@ -35,7 +44,7 @@ impl UserVerb for Verb {
if room.rentable_dynzone.is_empty() { if room.rentable_dynzone.is_empty() {
user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?; user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?;
} }
match room let rentinfo = match room
.rentable_dynzone .rentable_dynzone
.iter() .iter()
.find(|ri| ri.rent_what == item_name) .find(|ri| ri.rent_what == item_name)
@ -47,17 +56,34 @@ impl UserVerb for Verb {
.map(|ri| ri.rent_what) .map(|ri| ri.rent_what)
.join(", ") .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 match ctx
.trans .trans
.find_exact_dyn_exit( .find_exact_dyn_exit(&player_item.location, &exit)
&player_item.location,
&Direction::IN {
item: player_item.display.clone(),
},
)
.await? .await?
.as_ref() .as_ref()
.and_then(|it| it.location.split_once("/")) .and_then(|it| it.location.split_once("/"))
@ -87,7 +113,22 @@ impl UserVerb for Verb {
..(*ex_zone).clone() ..(*ex_zone).clone()
}) })
.await?; .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())?, _ => user_error("The premises seem to be broken anyway".to_owned())?,
} }

View File

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

View File

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

View File

@ -6,6 +6,7 @@ use crate::{
UResult, UResult,
}, },
models::{ models::{
corp::CorpCommType,
item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack}, item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack},
journal::JournalType, journal::JournalType,
task::{Task, TaskDetails, TaskMeta}, task::{Task, TaskDetails, TaskMeta},
@ -426,17 +427,48 @@ pub async fn consider_reward_for(
xp_gain 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... // Now consider kill bonuses...
if for_item.item_type == "npc" { if for_item.item_type == "npc" {
if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) { if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) {
if let Some(bonus) = &npc.kill_bonus { 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 trans
.queue_for_session( .queue_for_session(
&session, &session,
Some(&format!( Some(&format!(
"{}\nYour wristpad beeps for a credit of {} for that.\n", "{}\nYour wristpad beeps for a credit of ${}.\n",
bonus.msg, bonus.payment bonus.msg, user_payment
)), )),
) )
.await?; .await?;
@ -447,10 +479,7 @@ pub async fn consider_reward_for(
trans.save_user_model(&user).await?; trans.save_user_model(&user).await?;
if xp_gain == 0 { if xp_gain == 0 {
trans trans
.queue_for_session( .queue_for_session(&session, Some("[You didn't gain any experience points]\n"))
&session,
Some("[You didn't gain any experience for that]\n"),
)
.await?; .await?;
} else { } else {
trans trans

View File

@ -14,22 +14,26 @@ use once_cell::sync::OnceCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
mod cokmurl_apartment; mod cokmurl_apartment;
mod murl_deluxe_corporate;
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)] #[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
pub enum DynzoneType { pub enum DynzoneType {
CokMurlApartment, CokMurlApartment,
MurlDeluxeCorporate,
} }
impl DynzoneType { impl DynzoneType {
pub fn from_str(i: &str) -> Option<Self> { pub fn from_str(i: &str) -> Option<Self> {
match i { match i {
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment), "CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
"MurlDeluxeCorporate" => Some(DynzoneType::MurlDeluxeCorporate),
_ => None, _ => None,
} }
} }
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
DynzoneType::CokMurlApartment => "CokMurlApartment", DynzoneType::CokMurlApartment => "CokMurlApartment",
DynzoneType::MurlDeluxeCorporate => "MurlDeluxeCorporate",
} }
} }
} }
@ -49,7 +53,7 @@ impl Dynzone {
trans: &DBTrans, trans: &DBTrans,
connect_where: &str, connect_where: &str,
dup_message: &str, dup_message: &str,
new_owner: &Item, owner: &str,
new_exit_direction: &Direction, new_exit_direction: &Direction,
) -> UResult<String> { ) -> UResult<String> {
// Check exit not taken... // Check exit not taken...
@ -60,7 +64,6 @@ impl Dynzone {
{ {
user_error(dup_message.to_string())?; 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?); let code = format!("{}", &trans.alloc_item_code().await?);
trans trans
@ -72,7 +75,7 @@ impl Dynzone {
zone_exit: Some(connect_where.to_owned()), zone_exit: Some(connect_where.to_owned()),
vacate_after: None, vacate_after: None,
}), }),
owner: Some(owner.clone()), owner: Some(owner.to_owned()),
location: format!("dynzone/{}", &code), location: format!("dynzone/{}", &code),
..Default::default() ..Default::default()
}) })
@ -108,7 +111,7 @@ impl Dynzone {
None None
}, },
flags: room.item_flags.clone(), flags: room.item_flags.clone(),
owner: Some(owner.clone()), owner: Some(owner.to_owned()),
door_states: Some( door_states: Some(
room.exits room.exits
.iter() .iter()
@ -202,7 +205,7 @@ impl Default for Dynroom {
pub fn dynzone_list() -> &'static Vec<Dynzone> { pub fn dynzone_list() -> &'static Vec<Dynzone> {
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new(); 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> { 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 struct RentInfo {
pub rent_what: &'static str, pub rent_what: &'static str,
pub suite_type: RentSuiteType,
pub dynzone: super::dynzone::DynzoneType, pub dynzone: super::dynzone::DynzoneType,
pub daily_price: u64, pub daily_price: u64,
pub setup_fee: 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 crate::static_content::dynzone::DynzoneType;
use ansi::ansi; use ansi::ansi;
pub fn room_list() -> Vec<Room> { pub fn room_list() -> Vec<Room> {
@ -34,6 +36,7 @@ pub fn room_list() -> Vec<Room> {
should_caption: true, should_caption: true,
rentable_dynzone: vec!(RentInfo { rentable_dynzone: vec!(RentInfo {
rent_what: "studio", rent_what: "studio",
suite_type: RentSuiteType::Residential,
dynzone: DynzoneType::CokMurlApartment, dynzone: DynzoneType::CokMurlApartment,
daily_price: 20, daily_price: 20,
setup_fee: 40, setup_fee: 40,
@ -47,9 +50,21 @@ pub fn room_list() -> Vec<Room> {
code: "murl_lobby", code: "murl_lobby",
name: "Murlison Suites Commercial Lobby", name: "Murlison Suites Commercial Lobby",
short: ansi!("<bgyellow><black>ML<reset>"), 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, description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: 0 }, 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!( exits: vec!(
Exit { Exit {
direction: Direction::WEST, direction: Direction::WEST,