Implement hunger, eating, and drinking.

This commit is contained in:
Condorra 2023-08-05 01:49:46 +10:00
parent a1495e6731
commit fab18d604e
26 changed files with 1642 additions and 108 deletions

View File

@ -653,7 +653,7 @@ impl DBTrans {
) -> DResult<()> { ) -> DResult<()> {
self.pg_trans()? self.pg_trans()?
.query( .query(
"DELETE FROM task WHERE details->>'is_static' = 'true' AND \ "DELETE FROM tasks WHERE details->>'is_static' = 'true' AND \
details->>'task_type' = $1 AND \ details->>'task_type' = $1 AND \
details->>'task_code' = $2", details->>'task_code' = $2",
&[&task_type, &task_code], &[&task_type, &task_code],
@ -1717,6 +1717,75 @@ impl DBTrans {
.get("n")) .get("n"))
} }
pub async fn stop_urges_for_sessionless(&self) -> DResult<()> {
self.pg_trans()?
.execute(
"UPDATE items SET details = JSONB_SET(details, '{flags}', \
(SELECT COALESCE(jsonb_agg(elem), '[]') FROM jsonb_array_elements(details->'flags') elem \
WHERE elem <> '\"HasUrges\"')) \
WHERE details->'flags' @> '\"HasUrges\"' AND \
details->>'item_type' = 'player' AND NOT EXISTS \
(SELECT 1 FROM users WHERE current_session IS NOT NULL AND \
username = items.details->>'item_code')",
&[],
)
.await?;
Ok(())
}
// name is static since it should not be user generated (not escaped).
pub async fn apply_urge_tick(&self, name: &'static str) -> DResult<()> {
self.pg_trans()?
.execute(
&format!(
"UPDATE items SET details = \
JSONB_SET(\
JSONB_SET(details, '{{urges, {}, last_value}}', details->'urges'->'{}'->'value'), \
'{{urges, {}, value}}', \
TO_JSONB(GREATEST(0, LEAST(10000, (details->'urges'->'{}'->'value')::NUMERIC + (details->'urges'->'{}'->'growth')::NUMERIC))) \
) \
WHERE details->'flags' @> '\"HasUrges\"'",
name, name, name, name, name
),
&[]
)
.await?;
Ok(())
}
pub async fn get_urges_crossed_milestones(
&self,
name: &'static str,
) -> DResult<Vec<Arc<Item>>> {
Ok(self
.pg_trans()?
.query(
&format!(
"WITH details_urg AS (\
SELECT details, (details->'urges'->'{}'->'last_value')::NUMERIC AS last,\
(details->'urges'->'{}'->'value')::NUMERIC AS curr \
FROM items WHERE details->'flags' @> '\"HasUrges\"' \
) \
SELECT details FROM details_urg WHERE (\
(last < 2500 AND curr >= 2500) OR \
(last >= 2500 AND curr < 2500) OR \
(last < 5000 AND curr >= 5000) OR \
(last >= 5000 AND curr < 5000) OR \
(last < 7500 AND curr >= 7500) OR \
(last >= 7500 AND curr < 7500) OR \
(last = 10000 AND curr <> 10000) OR \
(last <> 10000 AND curr = 10000))",
name, name
),
&[],
)
.await?
.into_iter()
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
.map(Arc::new)
.collect())
}
pub async fn commit(mut self: Self) -> DResult<()> { pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
if let Some(trans) = trans_opt { if let Some(trans) = trans_opt {

View File

@ -2,6 +2,7 @@ use super::ListenerSession;
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::db::{DBPool, ItemSearchParams}; use crate::db::{DBPool, ItemSearchParams};
use crate::models::user::UserFlag;
use crate::models::{item::Item, session::Session, user::User}; use crate::models::{item::Item, session::Session, user::User};
use crate::DResult; use crate::DResult;
#[cfg(not(test))] #[cfg(not(test))]
@ -25,7 +26,9 @@ pub mod corp;
pub mod cut; pub mod cut;
pub mod delete; pub mod delete;
mod describe; mod describe;
pub mod drink;
pub mod drop; pub mod drop;
pub mod eat;
mod fire; mod fire;
pub mod follow; pub mod follow;
mod gear; mod gear;
@ -53,6 +56,7 @@ pub mod register;
pub mod remove; pub mod remove;
pub mod rent; pub mod rent;
mod report; mod report;
mod reset_spawns;
pub mod say; pub mod say;
mod score; mod score;
mod sign; mod sign;
@ -152,7 +156,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"corp" => corp::VERB, "corp" => corp::VERB,
"cut" => cut::VERB, "cut" => cut::VERB,
"delete" => delete::VERB, "delete" => delete::VERB,
"drink" => drink::VERB,
"drop" => drop::VERB, "drop" => drop::VERB,
"eat" => eat::VERB,
"fire" => fire::VERB, "fire" => fire::VERB,
@ -231,6 +237,10 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"write" => write::VERB, "write" => write::VERB,
}; };
static STAFF_COMMANDS: UserVerbRegistry = phf_map! {
"staff_reset_spawns" => reset_spawns::VERB,
};
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> { fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd); let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
@ -241,6 +251,10 @@ fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef>
Some(user_dat) => { Some(user_dat) => {
if user_dat.terms.terms_complete { if user_dat.terms.terms_complete {
result = result.or_else(|| REGISTERED_COMMANDS.get(cmd)); result = result.or_else(|| REGISTERED_COMMANDS.get(cmd));
if user_dat.user_flags.contains(&UserFlag::Staff) {
result = result.or_else(|| STAFF_COMMANDS.get(cmd));
}
} else if cmd == "agree" { } else if cmd == "agree" {
result = Some(&agree::VERB); result = Some(&agree::VERB);
} }

View File

@ -89,6 +89,7 @@ async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> {
player_item.total_xp = ((player_item.total_xp as i64) player_item.total_xp = ((player_item.total_xp as i64)
- user_dat.experience.xp_change_for_this_reroll) - user_dat.experience.xp_change_for_this_reroll)
.max(0) as u64; .max(0) as u64;
player_item.urges = Some(Default::default());
user_dat.experience.xp_change_for_this_reroll = 0; user_dat.experience.xp_change_for_this_reroll = 0;
user_dat.raw_stats = BTreeMap::new(); user_dat.raw_stats = BTreeMap::new();
user_dat.raw_skills = BTreeMap::new(); user_dat.raw_skills = BTreeMap::new();

View File

@ -0,0 +1,249 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
comms::broadcast_to_room,
urges::{hunger_changed, thirst_changed},
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::{collections::BTreeMap, time};
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item_type, item_code) = match ctx.command {
QueueCommand::Drink {
item_type,
item_code,
} => (item_type, item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location && item.location != ctx.item.refstr() {
user_error(format!(
"You try to drink {} but realise you no longer have it",
item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let msg_exp = format!(
"{} prepares to drink from {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} prepares to drink from {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item_type, item_code) = match ctx.command {
QueueCommand::Drink {
item_type,
item_code,
} => (item_type, item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location && item.location != ctx.item.refstr() {
user_error(format!(
"You try to drink {} but realise you no longer have it!",
&item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let liquid_details = item
.liquid_details
.as_ref()
.ok_or_else(|| UserError("You try to drink, but it's empty!".to_owned()))?;
let mut it = liquid_details.contents.iter();
let contents = it
.next()
.ok_or_else(|| UserError("You try to drink, but it's empty!".to_owned()))?;
let contents_2 = it.next();
if !contents_2.is_none() {
user_error("It seems to be a weird mixture of different fluids... you are not sure you should drink it!".to_owned())?;
}
let drink_data = match contents.0.drink_data() {
None => user_error(format!(
"It smells like {}... you are not sure you should drink it!",
contents.0.display()
))?,
Some(v) => v,
};
let urges = ctx
.item
.urges
.as_ref()
.ok_or_else(|| UserError("You don't seem to have the thirst.".to_owned()))?;
if (urges.thirst.value as i16) < -drink_data.thirst_impact {
user_error("You don't seem to have the thirst.".to_owned())?;
}
let how_many_left = (if contents.1 <= &1 {
1
} else {
contents.1.clone()
}) as u64;
let how_many_to_fill = if drink_data.thirst_impact >= 0 {
1
} else {
urges.thirst.value / ((-drink_data.thirst_impact) as u16)
};
let how_many_drunk = how_many_to_fill.min(how_many_left.min(10000) as u16).max(1);
let msg_exp = format!(
"{} drinks from {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} drinks from {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
if let Some(urges) = ctx.item.urges.as_mut() {
urges.hunger.last_value = urges.hunger.value;
urges.hunger.value = (urges.hunger.value as i64
+ (how_many_drunk as i64) * (drink_data.hunger_impact as i64))
.clamp(0, 10000) as u16;
urges.thirst.last_value = urges.thirst.value;
urges.thirst.value = (urges.thirst.value as i64
+ (how_many_drunk as i64) * (drink_data.thirst_impact as i64))
.clamp(0, 10000) as u16;
}
hunger_changed(&ctx.trans, &ctx.item).await?;
thirst_changed(&ctx.trans, &ctx.item).await?;
let mut item_mut = (*item).clone();
if let Some(ld) = item_mut.liquid_details.as_mut() {
if (*contents.1) <= how_many_drunk as u64 {
ld.contents = BTreeMap::new();
} else {
ld.contents
.entry(contents.0.clone())
.and_modify(|v| *v -= how_many_drunk as u64);
}
}
ctx.trans.save_item_model(&item_mut).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if !remaining.starts_with("from ") {
user_error(ansi!("Try <bold>drink from<reset> container.").to_owned())?;
}
remaining = remaining[5..].trim();
let mut drink_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
drink_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
drink_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: drink_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let mut player_item_mut = (*player_item).clone();
for target in targets {
if target.item_type != "possession" && target.item_type != "fixed_item" {
user_error("You can't drink that!".to_owned())?;
}
if target.liquid_details.is_none() {
user_error("There's nothing to drink!".to_owned())?;
}
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Drink {
item_type: target.item_type.clone(),
item_code: target.item_code.clone(),
},
)
.await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -0,0 +1,250 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::LocationActionType,
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
comms::broadcast_to_room,
urges::{hunger_changed, thirst_changed},
},
static_content::possession_type::possession_data,
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Eat { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to eat {} but realise you no longer have it",
item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let msg_exp = format!(
"{} prepares to eat {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} prepares to eat {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Eat { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to eat {} but realise you no longer have it!",
&item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
}
let possession_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
let eat_data = possession_data
.eat_data
.as_ref()
.ok_or_else(|| UserError("You can't eat that!".to_owned()))?;
let urges = ctx
.item
.urges
.as_ref()
.ok_or_else(|| UserError("You don't seem to have the appetite.".to_owned()))?;
if (urges.hunger.value as i16) < -eat_data.hunger_impact {
user_error("You don't seem to have the appetite.".to_owned())?;
}
let how_many_left = (if item.charges <= 1 { 1 } else { item.charges }) as u16;
let how_many_to_fill = if eat_data.hunger_impact >= 0 {
1
} else {
urges.hunger.value / ((-eat_data.hunger_impact) as u16)
};
let how_many_eaten = how_many_to_fill.min(how_many_left).max(1);
let msg_exp = format!(
"{} {} {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&(if how_many_eaten == how_many_left {
"polishes off".to_owned()
} else {
format!(
"eats {} bite{} from",
how_many_eaten,
if how_many_eaten == 1 { "" } else { "s" }
)
}),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} {} {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&(if how_many_eaten == how_many_left {
"polishes off".to_owned()
} else {
format!("eats {} bites from", how_many_eaten)
}),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
if let Some(urges) = ctx.item.urges.as_mut() {
urges.hunger.last_value = urges.hunger.value;
urges.hunger.value = (urges.hunger.value as i16
+ (how_many_eaten as i16) * eat_data.hunger_impact)
.clamp(0, 10000) as u16;
urges.thirst.last_value = urges.thirst.value;
urges.thirst.value = (urges.thirst.value as i16
+ (how_many_eaten as i16) * eat_data.thirst_impact)
.clamp(0, 10000) as u16;
}
hunger_changed(&ctx.trans, &ctx.item).await?;
thirst_changed(&ctx.trans, &ctx.item).await?;
if item.charges <= (how_many_eaten as u8) {
ctx.trans.delete_item("possession", &item_id).await?;
} else {
let mut item_mut = (*item).clone();
item_mut.charges -= how_many_eaten as u8;
ctx.trans.save_item_model(&item_mut).await?;
}
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut eat_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
eat_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
eat_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: eat_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let mut player_item_mut = (*player_item).clone();
for target in targets {
if target.item_type != "possession" {
user_error("You can't eat that!".to_owned())?;
}
target
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.eat_data.as_ref())
.ok_or_else(|| UserError("You can't eat that!".to_owned()))?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Eat {
possession_id: target.item_code.clone(),
},
)
.await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,5 +1,6 @@
use super::look; use super::look;
use super::{user_error, UResult, UserVerb, UserVerbRef, VerbContext}; use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::urges::set_has_urges_if_needed;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::time; use tokio::time;
@ -59,6 +60,9 @@ impl UserVerb for Verb {
ctx.trans.save_user_model(user).await?; ctx.trans.save_user_model(user).await?;
look::VERB.handle(ctx, "look", "").await?; look::VERB.handle(ctx, "look", "").await?;
} }
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
set_has_urges_if_needed(&ctx.trans, &mut player_item).await?;
ctx.trans.save_item_model(&player_item).await?;
Ok(()) Ok(())
} }

View File

@ -9,7 +9,10 @@ use crate::db::DBTrans;
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
language, language,
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack}, models::item::{
DoorState, Item, ItemFlag, ItemSpecialData, LiquidDetails, LiquidType, LocationActionType,
Subattack,
},
services::{combat::max_health, skills::calc_level_gap}, services::{combat::max_health, skills::calc_level_gap},
static_content::{ static_content::{
dynzone, dynzone,
@ -75,6 +78,44 @@ pub async fn describe_normal_item(
contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n")); contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n"));
} }
if let Some(liq_data) = item
.static_data()
.and_then(|sd| sd.liquid_container_data.as_ref())
{
match item.liquid_details.as_ref() {
Some(LiquidDetails { contents, .. }) if !contents.is_empty() => {
let total_volume: u64 = contents.iter().map(|c| c.1.clone()).sum();
let vol_frac = (total_volume as f64) / (liq_data.capacity as f64);
if vol_frac >= 0.99 {
contents_desc.push_str("It's full to the top with");
} else if vol_frac >= 0.75 {
contents_desc.push_str("It's nearly completely full of");
} else if vol_frac > 0.6 {
contents_desc.push_str("It's more than half full of");
} else if vol_frac > 0.4 {
contents_desc.push_str("It's about half full of");
} else if vol_frac > 0.29 {
contents_desc.push_str("It's about a third full of");
} else if vol_frac > 0.22 {
contents_desc.push_str("It's about a quarter full of");
} else {
contents_desc.push_str("It contains a tiny bit of");
}
contents_desc.push_str(" ");
let mut it = contents.iter();
let f1_opt = it.next();
let f2_opt = it.next();
match (f1_opt, f2_opt) {
(Some((&LiquidType::Water, _)), None) => contents_desc.push_str("water"),
_ => contents_desc.push_str("mixed fluids"),
}
contents_desc.push_str(".\n");
}
_ => contents_desc.push_str("It's completely dry.\n"),
}
}
let anything_worn = items let anything_worn = items
.iter() .iter()
.any(|it| it.action_type == LocationActionType::Worn); .any(|it| it.action_type == LocationActionType::Worn);

View File

@ -23,6 +23,7 @@ use crate::{
combat::{change_health, handle_resurrect, stop_attacking_mut}, combat::{change_health, handle_resurrect, stop_attacking_mut},
comms::broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
urges::{recalculate_urge_growth, thirst_changed},
}, },
static_content::{ static_content::{
dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget}, dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget},
@ -597,6 +598,13 @@ async fn attempt_move_immediate(
} }
} }
recalculate_urge_growth(ctx.trans, &mut ctx.item).await?;
if let Some(urges) = ctx.item.urges.as_mut() {
urges.thirst.last_value = urges.thirst.value;
urges.thirst.value = (urges.thirst.value + 10).min(10000);
thirst_changed(&ctx.trans, &ctx.item).await?;
}
ctx.item.location = new_loc.clone(); ctx.item.location = new_loc.clone();
ctx.item.action_type = LocationActionType::Normal; ctx.item.action_type = LocationActionType::Normal;
ctx.item.active_combat = None; ctx.item.active_combat = None;

View File

@ -0,0 +1,27 @@
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::spawn::refresh_all_spawn_points;
use async_trait::async_trait;
use log::info;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
info!(
"Staff-triggered spawn reset by {}",
ctx.user_dat
.as_ref()
.map(|u| u.username.clone())
.unwrap_or_else(|| "???".to_owned())
);
refresh_all_spawn_points(&ctx.trans).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,5 +1,5 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext}; use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::combat::max_health; use crate::{models::item::Urges, services::combat::max_health};
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;
@ -41,6 +41,41 @@ impl UserVerb for Verb {
player_item.health, player_item.health,
maxh maxh
)); ));
let (hunger, thirst, bladder, stress) = match player_item.urges.as_ref() {
None => (0, 0, 0, 0),
Some(Urges {
hunger,
thirst,
bladder,
stress,
}) => (hunger.value, thirst.value, bladder.value, stress.value),
};
msg.push_str(&format!(
ansi!("<bold>Hunger [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((hunger / 200) as u64, 50),
hunger / 100,
100
));
msg.push_str(&format!(
ansi!("<bold>Thirst [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((thirst / 200) as u64, 50),
thirst / 100,
100
));
msg.push_str(&format!(
ansi!("<bold>Stress [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((stress / 200) as u64, 50),
stress / 100,
100
));
if bladder >= 7500 {
msg.push_str("Your bladder is so full it hurts!\n");
} else if bladder >= 5000 {
msg.push_str("You really need the toilet!\n");
} else if bladder >= 2500 {
msg.push_str("Your bladder is slightly full.\n");
}
msg.push_str(&format!( msg.push_str(&format!(
ansi!("<bold>Credits <green>${}<reset>\n"), ansi!("<bold>Credits <green>${}<reset>\n"),
user.credits user.credits

View File

@ -2,9 +2,12 @@ use super::session::Session;
use crate::{ use crate::{
language, language,
regular_tasks::queued_command::QueueCommand, regular_tasks::queued_command::QueueCommand,
static_content::possession_type::{possession_data, PossessionType}, static_content::{
static_content::room::Direction, fixed_item::fixed_item_properties,
static_content::species::SpeciesType, possession_type::{possession_data, EatData, PossessionData, PossessionType},
room::Direction,
species::SpeciesType,
},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -150,6 +153,41 @@ impl StatType {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum LiquidType {
Water,
}
impl LiquidType {
pub fn drink_data(&self) -> Option<EatData> {
match self {
LiquidType::Water => Some(EatData {
hunger_impact: 0,
thirst_impact: -1, // 0.01% per mL
}),
}
}
pub fn display(&self) -> &str {
match self {
LiquidType::Water => "water",
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct LiquidDetails {
// In mLs...
pub contents: BTreeMap<LiquidType, u64>,
}
impl Default for LiquidDetails {
fn default() -> Self {
Self {
contents: BTreeMap::<LiquidType, u64>::new(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Pronouns { pub struct Pronouns {
pub subject: String, pub subject: String,
@ -264,6 +302,8 @@ pub enum ItemFlag {
Bench, Bench,
Book, Book,
Instructions, Instructions,
HasUrges,
NoUrgesHere,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -294,6 +334,44 @@ impl Default for ActiveClimb {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[serde(default)]
pub struct Urge {
pub last_value: u16,
pub value: u16, // In hundreths of a percent (0-10000).
pub growth: i16,
}
impl Default for Urge {
fn default() -> Self {
Self {
last_value: 0,
value: 0,
growth: 42, // About 4 hours of once a minute ticks to hit 10k.
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[serde(default)]
pub struct Urges {
pub hunger: Urge,
pub bladder: Urge,
pub thirst: Urge,
pub stress: Urge,
}
impl Default for Urges {
fn default() -> Self {
Self {
hunger: Default::default(),
bladder: Default::default(),
thirst: Default::default(),
stress: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
pub enum ItemSpecialData { pub enum ItemSpecialData {
ItemWriting { ItemWriting {
@ -400,6 +478,8 @@ pub struct Item {
pub door_states: Option<BTreeMap<Direction, DoorState>>, pub door_states: Option<BTreeMap<Direction, DoorState>>,
pub following: Option<FollowData>, pub following: Option<FollowData>,
pub queue: VecDeque<QueueCommand>, pub queue: VecDeque<QueueCommand>,
pub urges: Option<Urges>,
pub liquid_details: Option<LiquidDetails>,
} }
impl Item { impl Item {
@ -469,6 +549,19 @@ impl Item {
as u64 as u64
} }
} }
pub fn static_data(&self) -> Option<&'static PossessionData> {
if self.item_type == "possession" {
self.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.copied()
} else if self.item_type == "fixed_item" {
fixed_item_properties().get(self.item_code.as_str())
} else {
None
}
}
} }
impl Default for Item { impl Default for Item {
@ -507,6 +600,8 @@ impl Default for Item {
door_states: None, door_states: None,
following: None, following: None,
queue: VecDeque::new(), queue: VecDeque::new(),
urges: None,
liquid_details: None,
} }
} }
} }

View File

@ -52,6 +52,8 @@ pub enum TaskDetails {
ChargeWages { ChargeWages {
npc: String, npc: String,
}, },
TickUrges,
ResetSpawns,
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -70,6 +72,8 @@ impl TaskDetails {
SwingShut { .. } => "SwingShut", SwingShut { .. } => "SwingShut",
DestroyUser { .. } => "DestroyUser", DestroyUser { .. } => "DestroyUser",
ChargeWages { .. } => "ChargeWages", ChargeWages { .. } => "ChargeWages",
TickUrges => "TickUrges",
ResetSpawns => "ResetSpawns",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
} }
} }

View File

@ -22,6 +22,11 @@ pub struct UserExperienceData {
pub crafted_items: BTreeMap<String, u64>, pub crafted_items: BTreeMap<String, u64>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub enum UserFlag {
Staff,
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(default)] #[serde(default)]
pub struct User { pub struct User {
@ -42,6 +47,7 @@ pub struct User {
pub last_page_from: Option<String>, pub last_page_from: Option<String>,
pub credits: u64, pub credits: u64,
pub danger_code: Option<String>, pub danger_code: Option<String>,
pub user_flags: Vec<UserFlag>,
// Reminder: Consider backwards compatibility when updating this. // Reminder: Consider backwards compatibility when updating this.
} }
@ -86,6 +92,7 @@ impl Default for User {
last_page_from: None, last_page_from: None,
credits: 500, credits: 500,
danger_code: None, danger_code: None,
user_flags: vec![],
} }
} }
} }

View File

@ -7,7 +7,7 @@ use crate::{
listener::{ListenerMap, ListenerSend}, listener::{ListenerMap, ListenerSend},
message_handler::user_commands::{delete, drop, hire, open, rent}, message_handler::user_commands::{delete, drop, hire, open, rent},
models::task::Task, models::task::Task,
services::{combat, effect}, services::{combat, effect, spawn, urges},
static_content::npc, static_content::npc,
DResult, DResult,
}; };
@ -55,6 +55,8 @@ fn task_handler_registry(
("SwingShut", open::SWING_SHUT_HANDLER.clone()), ("SwingShut", open::SWING_SHUT_HANDLER.clone()),
("DestroyUser", delete::DESTROY_USER_HANDLER.clone()), ("DestroyUser", delete::DESTROY_USER_HANDLER.clone()),
("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()), ("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()),
("TickUrges", urges::TICK_URGES_HANDLER.clone()),
("ResetSpawns", spawn::RESET_SPAWNS_HANDLER.clone()),
] ]
.into_iter() .into_iter()
.collect() .collect()

View File

@ -2,8 +2,8 @@ use super::{TaskHandler, TaskRunContext};
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::message_handler::user_commands::{ use crate::message_handler::user_commands::{
close, cut, drop, get, improvise, make, movement, open, put, remove, use_cmd, user_error, wear, close, cut, drink, drop, eat, get, improvise, make, movement, open, put, remove, use_cmd,
wield, CommandHandlingError, UResult, VerbContext, user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
}; };
use crate::message_handler::ListenerSession; use crate::message_handler::ListenerSession;
use crate::models::session::Session; use crate::models::session::Session;
@ -57,9 +57,16 @@ pub enum QueueCommand {
from_corpse: String, from_corpse: String,
what_part: String, what_part: String,
}, },
Drink {
item_type: String,
item_code: String,
},
Drop { Drop {
possession_id: String, possession_id: String,
}, },
Eat {
possession_id: String,
},
Get { Get {
possession_id: String, possession_id: String,
}, },
@ -111,7 +118,9 @@ impl QueueCommand {
match self { match self {
CloseDoor { .. } => "CloseDoor", CloseDoor { .. } => "CloseDoor",
Cut { .. } => "Cut", Cut { .. } => "Cut",
Drink { .. } => "Drink",
Drop { .. } => "Drop", Drop { .. } => "Drop",
Eat { .. } => "Eat",
Get { .. } => "Get", Get { .. } => "Get",
GetFromContainer { .. } => "GetFromContainer", GetFromContainer { .. } => "GetFromContainer",
Make { .. } => "Make", Make { .. } => "Make",
@ -175,6 +184,10 @@ fn queue_command_registry(
"CloseDoor", "CloseDoor",
&close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
), ),
(
"Drink",
&drink::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
( (
"Drop", "Drop",
&drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
@ -187,6 +200,10 @@ fn queue_command_registry(
"GetFromContainer", "GetFromContainer",
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
), ),
(
"Eat",
&eat::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
( (
"Make", "Make",
&make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),

View File

@ -21,6 +21,8 @@ pub mod combat;
pub mod comms; pub mod comms;
pub mod effect; pub mod effect;
pub mod skills; pub mod skills;
pub mod spawn;
pub mod urges;
fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool { fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
if let Some((loctype, loccode)) = target.location.split_once("/") { if let Some((loctype, loccode)) = target.location.split_once("/") {

View File

@ -5,7 +5,7 @@ use crate::{
follow::cancel_follow_by_leader, user_error, CommandHandlingError, UResult, follow::cancel_follow_by_leader, user_error, CommandHandlingError, UResult,
}, },
models::{ models::{
item::{DeathData, Item, LocationActionType, SkillType, Subattack}, item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack},
journal::JournalType, journal::JournalType,
task::{Task, TaskDetails, TaskMeta}, task::{Task, TaskDetails, TaskMeta},
}, },
@ -14,6 +14,7 @@ use crate::{
comms::broadcast_to_room, comms::broadcast_to_room,
destroy_container, destroy_container,
skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only}, skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only},
urges::recalculate_urge_growth,
}, },
static_content::{ static_content::{
journals::{award_journal_if_needed, check_journal_for_kill}, journals::{award_journal_if_needed, check_journal_for_kill},
@ -524,7 +525,9 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult<boo
player.total_xp -= lost_xp; player.total_xp -= lost_xp;
user.experience.xp_change_for_this_reroll -= lost_xp as i64; user.experience.xp_change_for_this_reroll -= lost_xp as i64;
player.temporary_buffs = vec![]; player.temporary_buffs = vec![];
player.flags.push(ItemFlag::HasUrges);
calculate_total_stats_skills_for_user(player, &user); calculate_total_stats_skills_for_user(player, &user);
recalculate_urge_growth(trans, player).await?;
player.health = max_health(&player); player.health = max_health(&player);
player.active_climb = None; player.active_climb = None;

View File

@ -0,0 +1,201 @@
#[double]
use crate::db::DBTrans;
use crate::{
message_handler::user_commands::rent::recursively_destroy_or_move_item,
models::{
item::{LiquidDetails, LiquidType},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
},
regular_tasks::{TaskHandler, TaskRunContext},
services::Item,
static_content::{possession_type::PossessionType, StaticTask},
DResult,
};
use async_recursion::async_recursion;
use async_trait::async_trait;
use log::{info, warn};
use mockall_double::double;
use once_cell::sync::OnceCell;
use rand::{thread_rng, Rng};
use std::collections::BTreeMap;
use std::time;
#[allow(dead_code)]
pub enum SpawnDistribution {
DoNothing,
SpawnLiquid {
what: LiquidType,
how_much: u64,
},
SpawnPossession {
what: PossessionType,
},
SpawnWithProb {
pspawn: f64,
subspawn: Box<SpawnDistribution>,
},
SpawnOne {
pvec: Vec<(f64, Box<SpawnDistribution>)>,
},
SpawnAll {
subspawns: Vec<Box<SpawnDistribution>>,
},
}
#[async_recursion]
async fn add_per_spawn(
trans: &DBTrans,
distribution: &SpawnDistribution,
location: &mut Item,
) -> DResult<bool> {
match distribution {
SpawnDistribution::DoNothing => Ok(false),
SpawnDistribution::SpawnLiquid { what, how_much } => {
match location.liquid_details.as_mut() {
None => {
location.liquid_details = Some(LiquidDetails {
contents: vec![(what.clone(), how_much.clone())].into_iter().collect(),
..Default::default()
});
Ok(true)
}
Some(liq_dets) => {
liq_dets
.contents
.entry(what.clone())
.and_modify(|d| *d += how_much)
.or_insert(how_much.clone());
Ok(true)
}
}
}
SpawnDistribution::SpawnPossession { what } => {
let mut item: Item = what.clone().into();
item.location = location.refstr();
trans.create_item(&item).await?;
Ok(false)
}
SpawnDistribution::SpawnWithProb { pspawn, subspawn } => {
let samp: f64 = thread_rng().gen();
if samp < *pspawn {
add_per_spawn(trans, &subspawn, location).await
} else {
Ok(false)
}
}
SpawnDistribution::SpawnOne { pvec } => {
let samp: f64 = thread_rng().gen();
let mut cumsum = 0.0_f64;
for opt in pvec {
cumsum += opt.0;
if samp < cumsum {
return add_per_spawn(trans, &opt.1, location).await;
}
}
Ok(false)
}
SpawnDistribution::SpawnAll { subspawns } => {
let mut did_mut_location = false;
for spawn in subspawns {
did_mut_location |= add_per_spawn(trans, &spawn, location).await?
}
Ok(did_mut_location)
}
}
}
fn spawn_list() -> &'static BTreeMap<&'static str, SpawnDistribution> {
static SPAWN_LIST: OnceCell<BTreeMap<&'static str, SpawnDistribution>> = OnceCell::new();
SPAWN_LIST.get_or_init(|| {
vec![(
"fixed_item/melbs_king_st_spring_fed_fountain",
SpawnDistribution::SpawnOne {
pvec: vec![(
1.0,
Box::new(SpawnDistribution::SpawnLiquid {
what: LiquidType::Water,
how_much: 1000000,
}),
)],
},
)]
.into_iter()
.collect()
})
}
pub async fn refresh_all_spawn_points(trans: &DBTrans) -> DResult<()> {
for (location_ref, distro) in spawn_list().iter() {
let (loc_type, loc_code) = match location_ref.split_once("/") {
None => {
warn!("Invalid spawn point location {}", location_ref);
continue;
}
Some(v) => v,
};
let location = match trans.find_item_by_type_code(loc_type, loc_code).await? {
None => {
warn!("Spawn point location {} not found", location_ref);
continue;
}
Some(v) => v,
};
// Step 1: Clear spawned site. It's paginated so we loop to get everything...
loop {
let result = trans.find_items_by_location(location_ref).await?;
if result.is_empty() {
break;
}
for sub_item in result {
recursively_destroy_or_move_item(trans, &sub_item).await?;
}
}
let mut location_mut = (*location).clone();
let mut need_save = false;
match location_mut.liquid_details.as_mut() {
None => {}
Some(liq_dets) => {
if !liq_dets.contents.is_empty() {
need_save = true;
liq_dets.contents = BTreeMap::new();
}
}
}
// Step 2: Add any spawns that come up
need_save |= add_per_spawn(trans, distro, &mut location_mut).await?;
// Step 3: Save the location if needed (for liquids)
if need_save {
trans.save_item_model(&location_mut).await?;
}
}
Ok(())
}
pub struct ResetSpawnsTaskHandler;
#[async_trait]
impl TaskHandler for ResetSpawnsTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
info!("Running spawn point refresh (timed)");
refresh_all_spawn_points(ctx.trans).await?;
Ok(Some(time::Duration::from_secs(3600)))
}
}
pub static RESET_SPAWNS_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ResetSpawnsTaskHandler;
pub fn reset_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
Box::new(
vec![StaticTask {
task_code: "ResetSpawns".to_owned(),
initial_task: Box::new(|| Task {
meta: TaskMeta {
task_code: "ResetSpawns".to_owned(),
is_static: true,
recurrence: Some(TaskRecurrence::FixedDuration { seconds: 3600 }),
..Default::default()
},
details: TaskDetails::ResetSpawns,
}),
}]
.into_iter(),
)
}

View File

@ -0,0 +1,377 @@
#[double]
use crate::db::DBTrans;
use crate::{
message_handler::user_commands::{
say::say_to_room,
CommandHandlingError::{SystemError, UserError},
},
models::{
item::{Item, ItemFlag, LocationActionType, StatType, Urge, Urges},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::{species::SpeciesType, StaticTask},
DResult,
};
use async_trait::async_trait;
use chrono::{self, Utc};
use mockall_double::double;
use std::time;
fn urge_threshold_check(urge: &Urge) -> bool {
(urge.last_value < 2500 && urge.value >= 2500)
|| (urge.last_value >= 2500 && urge.value < 2500)
|| (urge.last_value < 5000 && urge.value >= 5000)
|| (urge.last_value >= 5000 && urge.value < 5000)
|| (urge.last_value < 7500 && urge.value >= 7500)
|| (urge.last_value >= 7500 && urge.value < 7500)
|| (urge.last_value == 10000 && urge.value != 10000)
|| (urge.last_value != 10000 && urge.value == 10000)
}
pub async fn hunger_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.hunger) {
None => return Ok(()),
Some(u) => u,
};
if !urge_threshold_check(&urge) {
return Ok(());
}
let is_player = who.item_type == "player";
let msg = if urge.last_value < urge.value {
// Rising
if urge.value < 5000 {
if is_player {
"You're starting to feel a bit peckish."
} else {
"I'm getting a bit hungry, you know."
}
} else if urge.value < 7500 {
if is_player {
"You feel sharp pangs of hunger."
} else {
"I really wish I had something to eat."
}
} else if urge.value < 10000 {
if is_player {
"You are absolutely starving!"
} else {
"I'm SO hungry!"
}
} else {
if is_player {
"You're literally famished and can barely move."
} else {
"I'm literally starving."
}
}
} else {
if urge.value < 2500 {
if is_player {
"You're not that hungry now."
} else {
"I don't feel so hungry anymore!"
}
} else if urge.value < 5000 {
if is_player {
"You're only a bit hungry now."
} else {
"I'm still a bit hungry, you know."
}
} else if urge.value < 7500 {
if is_player {
"You're still pretty hungry."
} else {
"I still feel pretty hungry."
}
} else {
if is_player {
"You're still really hungry."
} else {
"I'm still really hungry!"
}
}
};
if is_player {
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
trans
.queue_for_session(&sess, Some(&format!("{}\n", msg)))
.await?;
}
} else {
if who.species == SpeciesType::Human {
match say_to_room(&trans, who, &who.location, msg, false).await {
Ok(_) => Ok(()),
Err(SystemError(e)) => Err(e),
Err(UserError(_)) => Ok(()),
}?;
}
}
Ok(())
}
pub async fn bladder_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.bladder) {
None => return Ok(()),
Some(u) => u,
};
if !urge_threshold_check(&urge) {
return Ok(());
}
if who.item_type == "player" && urge.value > urge.last_value {
let msg = if urge.value < 5000 {
"You feel a slight pressure building in your bladder."
} else if urge.value < 7500 {
"You've really got to find a toilet soon."
} else if urge.value < 10000 {
"You're absolutely busting!"
} else {
"You can't hold your bladder any longer."
};
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
trans
.queue_for_session(&sess, Some(&format!("{}\n", msg)))
.await?;
}
}
// TODO soil the room
Ok(())
}
pub async fn thirst_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.thirst) {
None => return Ok(()),
Some(u) => u,
};
if !urge_threshold_check(&urge) {
return Ok(());
}
let is_player = who.item_type == "player";
let msg = if urge.last_value < urge.value {
// Rising
if urge.value < 5000 {
if is_player {
"You're starting to feel a bit thirsty."
} else {
"I'm getting a bit thirsty, you know."
}
} else if urge.value < 7500 {
if is_player {
"Your through feels really dry."
} else {
"I really wish I had something to drink."
}
} else if urge.value < 10000 {
if is_player {
"You're throat is absolutely dry with thirst!"
} else {
"I'm SO thirsty!"
}
} else {
if is_player {
"You're literally dehydrated and can barely move."
} else {
"I'm literally dehydrated."
}
}
} else {
if urge.value < 2500 {
if is_player {
"You're not that thirsty now."
} else {
"I don't feel so thirsty anymore!"
}
} else if urge.value < 5000 {
if is_player {
"You're only a bit thirsty now."
} else {
"I'm still a bit thirsty, you know."
}
} else if urge.value < 7500 {
if is_player {
"You're still pretty thirsty."
} else {
"I still feel pretty thirsty."
}
} else {
if is_player {
"You're still really thirsty."
} else {
"I'm still really thirsty!"
}
}
};
if is_player {
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
trans
.queue_for_session(&sess, Some(&format!("{}\n", msg)))
.await?;
}
} else {
match say_to_room(&trans, who, &who.location, msg, false).await {
Ok(_) => Ok(()),
Err(SystemError(e)) => Err(e),
Err(UserError(_)) => Ok(()),
}?;
}
Ok(())
}
pub async fn stress_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.stress) {
None => return Ok(()),
Some(u) => u,
};
if !urge_threshold_check(&urge) {
return Ok(());
}
if who.item_type == "player" {
let msg = if urge.value > urge.last_value {
if urge.value < 5000 {
"You're getting a bit stressed."
} else if urge.value < 7500 {
"You're pretty strssed out."
} else if urge.value < 10000 {
"You're so stressed you'd really better rest!"
} else {
"You're so stressed you can't move, and need to sleep now."
}
} else {
if urge.value < 2500 {
"You're no longer stressed."
} else if urge.value < 5000 {
"You're only a bit stressed now."
} else if urge.value < 7500 {
"You're still pretty stressed."
} else {
"You're still really stressed."
}
};
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
trans
.queue_for_session(&sess, Some(&format!("{}\n", msg)))
.await?;
}
}
Ok(())
}
pub struct TickUrgesTaskHandler;
#[async_trait]
impl TaskHandler for TickUrgesTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
ctx.trans.stop_urges_for_sessionless().await?;
ctx.trans.apply_urge_tick("hunger").await?;
for item in ctx.trans.get_urges_crossed_milestones("hunger").await? {
hunger_changed(&ctx.trans, &item).await?;
}
ctx.trans.apply_urge_tick("bladder").await?;
for item in ctx.trans.get_urges_crossed_milestones("bladder").await? {
bladder_changed(&ctx.trans, &item).await?;
}
ctx.trans.apply_urge_tick("thirst").await?;
for item in ctx.trans.get_urges_crossed_milestones("thirst").await? {
thirst_changed(&ctx.trans, &item).await?;
}
ctx.trans.apply_urge_tick("stress").await?;
for item in ctx.trans.get_urges_crossed_milestones("stress").await? {
stress_changed(&ctx.trans, &item).await?;
}
Ok(Some(time::Duration::from_secs(60)))
}
}
pub static TICK_URGES_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &TickUrgesTaskHandler;
pub fn urge_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
Box::new(
vec![StaticTask {
task_code: "tick_urges".to_owned(),
initial_task: Box::new(|| Task {
meta: TaskMeta {
task_code: "tick_urges".to_owned(),
is_static: true,
recurrence: Some(TaskRecurrence::FixedDuration { seconds: 60 }),
next_scheduled: Utc::now() + chrono::Duration::seconds(60),
..TaskMeta::default()
},
details: TaskDetails::TickUrges,
}),
}]
.into_iter(),
)
}
pub async fn set_has_urges_if_needed(trans: &DBTrans, player_item: &mut Item) -> DResult<()> {
let mut has_urges = player_item.death_data.is_none()
&& match player_item.urges {
None => false,
Some(Urges {
hunger: Urge { growth: hunger, .. },
stress: Urge { growth: stress, .. },
thirst: Urge { growth: thirst, .. },
..
}) => hunger != 0 || stress != 0 || thirst != 0,
};
if has_urges {
match player_item.location.split_once("/") {
None => {}
Some((loc_type, loc_code)) => {
has_urges |= trans
.find_item_by_type_code(loc_type, loc_code)
.await?
.map(|it| !it.flags.contains(&ItemFlag::NoUrgesHere))
.unwrap_or(true);
}
}
}
if has_urges {
player_item.flags.push(ItemFlag::HasUrges);
} else {
player_item.flags = player_item
.flags
.clone()
.into_iter()
.filter(|f| f != &ItemFlag::HasUrges)
.collect();
}
Ok(())
}
pub async fn recalculate_urge_growth(_trans: &DBTrans, item: &mut Item) -> DResult<()> {
let cool = item.total_stats.get(&StatType::Cool).unwrap_or(&8.0);
let relax_action_factor = match item.action_type {
LocationActionType::Sitting => 100.0,
LocationActionType::Reclining => 150.0,
LocationActionType::Attacking(_) => 0.5,
_ => 1.0,
};
let old_urges = item.urges.clone().unwrap_or_else(|| Default::default());
item.urges = Some(Urges {
hunger: Urge {
growth: 42,
..old_urges.hunger
},
thirst: Urge {
growth: 0,
..old_urges.thirst
}, // To do: climate based?
bladder: Urge {
growth: 42,
..old_urges.bladder
},
stress: Urge {
growth: (-(cool.max(7.0) - 7.0) * 4.0 * relax_action_factor) as i16,
..old_urges.stress
},
});
Ok(())
}

View File

@ -1,66 +1,77 @@
use crate::DResult; use crate::{
use crate::db::DBPool; db::DBPool,
use crate::models::{item::Item, task::Task}; models::{item::Item, task::Task},
use std::collections::{BTreeSet, BTreeMap}; services::{spawn, urges},
DResult,
};
use log::info; use log::info;
use std::collections::{BTreeMap, BTreeSet};
pub mod room;
pub mod dynzone; pub mod dynzone;
pub mod fixed_item;
pub mod journals;
pub mod npc; pub mod npc;
pub mod possession_type; pub mod possession_type;
pub mod room;
pub mod species; pub mod species;
pub mod journals;
mod fixed_item;
pub struct StaticItem { pub struct StaticItem {
pub item_code: &'static str, pub item_code: &'static str,
pub initial_item: Box<dyn Fn() -> Item> pub initial_item: Box<dyn Fn() -> Item>,
} }
pub struct StaticTask { pub struct StaticTask {
pub task_code: String, pub task_code: String,
pub initial_task: Box<dyn Fn() -> Task> pub initial_task: Box<dyn Fn() -> Task>,
} }
struct StaticThingTypeGroup<Thing> { struct StaticThingTypeGroup<Thing> {
thing_type: &'static str, thing_type: &'static str,
things: fn () -> Box<dyn Iterator<Item = Thing>> things: fn() -> Box<dyn Iterator<Item = Thing>>,
} }
fn static_item_registry() -> Vec<StaticThingTypeGroup<StaticItem>> { fn static_item_registry() -> Vec<StaticThingTypeGroup<StaticItem>> {
vec!( vec![
// Must have no duplicates. // Must have no duplicates.
StaticThingTypeGroup::<StaticItem> { StaticThingTypeGroup::<StaticItem> {
thing_type: "npc", thing_type: "npc",
things: || npc::npc_static_items() things: || npc::npc_static_items(),
}, },
StaticThingTypeGroup::<StaticItem> { StaticThingTypeGroup::<StaticItem> {
thing_type: "room", thing_type: "room",
things: || room::room_static_items() things: || room::room_static_items(),
}, },
StaticThingTypeGroup::<StaticItem> { StaticThingTypeGroup::<StaticItem> {
thing_type: "fixed_item", thing_type: "fixed_item",
things: || fixed_item::static_items() things: || fixed_item::static_items(),
}, },
) ]
} }
fn static_task_registry() -> Vec<StaticThingTypeGroup<StaticTask>> { fn static_task_registry() -> Vec<StaticThingTypeGroup<StaticTask>> {
vec!( vec![
// Must have no duplicates. // Must have no duplicates.
StaticThingTypeGroup::<StaticTask> { StaticThingTypeGroup::<StaticTask> {
thing_type: "NPCSay", thing_type: "NPCSay",
things: || npc::npc_say_tasks() things: || npc::npc_say_tasks(),
}, },
StaticThingTypeGroup::<StaticTask> { StaticThingTypeGroup::<StaticTask> {
thing_type: "NPCWander", thing_type: "NPCWander",
things: || npc::npc_wander_tasks() things: || npc::npc_wander_tasks(),
}, },
StaticThingTypeGroup::<StaticTask> { StaticThingTypeGroup::<StaticTask> {
thing_type: "NPCAggro", thing_type: "NPCAggro",
things: || npc::npc_aggro_tasks() things: || npc::npc_aggro_tasks(),
}, },
) StaticThingTypeGroup::<StaticTask> {
thing_type: "TickUrges",
things: || urges::urge_tasks(),
},
StaticThingTypeGroup::<StaticTask> {
thing_type: "ResetSpawns",
things: || spawn::reset_tasks(),
},
]
} }
async fn refresh_static_items(pool: &DBPool) -> DResult<()> { async fn refresh_static_items(pool: &DBPool) -> DResult<()> {
@ -74,28 +85,38 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> {
} }
for type_group in registry.iter() { for type_group in registry.iter() {
info!("Checking static_content of item_type {}", type_group.thing_type); info!(
"Checking static_content of item_type {}",
type_group.thing_type
);
let tx = pool.start_transaction().await?; let tx = pool.start_transaction().await?;
let existing_items = tx.find_static_items_by_type(type_group.thing_type).await?; let existing_items = tx.find_static_items_by_type(type_group.thing_type).await?;
let expected_items: BTreeMap<String, StaticItem> = let expected_items: BTreeMap<String, StaticItem> = (type_group.things)()
(type_group.things)().map(|x| (x.item_code.to_owned(), x)).collect(); .map(|x| (x.item_code.to_owned(), x))
let expected_set: BTreeSet<String> = expected_items.keys().map(|x|x.to_owned()).collect(); .collect();
let expected_set: BTreeSet<String> = expected_items.keys().map(|x| x.to_owned()).collect();
for unwanted_item in existing_items.difference(&expected_set) { for unwanted_item in existing_items.difference(&expected_set) {
info!("Deleting item {:?}", unwanted_item); info!("Deleting item {:?}", unwanted_item);
tx.delete_static_items_by_code(type_group.thing_type, unwanted_item).await?; tx.delete_static_items_by_code(type_group.thing_type, unwanted_item)
.await?;
} }
for new_item_code in expected_set.difference(&existing_items) { for new_item_code in expected_set.difference(&existing_items) {
info!("Creating item {:?}", new_item_code); info!("Creating item {:?}", new_item_code);
tx.create_item(&(expected_items.get(new_item_code) tx.create_item(&(expected_items.get(new_item_code).unwrap().initial_item)())
.unwrap().initial_item)()).await?; .await?;
} }
for existing_item_code in expected_set.intersection(&existing_items) { for existing_item_code in expected_set.intersection(&existing_items) {
tx.limited_update_static_item( tx.limited_update_static_item(&(expected_items
&(expected_items.get(existing_item_code) .get(existing_item_code)
.unwrap().initial_item)()).await?; .unwrap()
.initial_item)())
.await?;
} }
tx.commit().await?; tx.commit().await?;
info!("Committed any changes for static_content of item_type {}", type_group.thing_type); info!(
"Committed any changes for static_content of item_type {}",
type_group.thing_type
);
} }
Ok(()) Ok(())
} }
@ -111,28 +132,38 @@ async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> {
} }
for type_group in registry.iter() { for type_group in registry.iter() {
info!("Checking static_content of task_type {}", type_group.thing_type); info!(
"Checking static_content of task_type {}",
type_group.thing_type
);
let tx = pool.start_transaction().await?; let tx = pool.start_transaction().await?;
let existing_tasks = tx.find_static_tasks_by_type(type_group.thing_type).await?; let existing_tasks = tx.find_static_tasks_by_type(type_group.thing_type).await?;
let expected_tasks: BTreeMap<String, StaticTask> = let expected_tasks: BTreeMap<String, StaticTask> = (type_group.things)()
(type_group.things)().map(|x| (x.task_code.to_owned(), x)).collect(); .map(|x| (x.task_code.to_owned(), x))
let expected_set: BTreeSet<String> = expected_tasks.keys().map(|x|x.to_owned()).collect(); .collect();
let expected_set: BTreeSet<String> = expected_tasks.keys().map(|x| x.to_owned()).collect();
for unwanted_task in existing_tasks.difference(&expected_set) { for unwanted_task in existing_tasks.difference(&expected_set) {
info!("Deleting task {:?}", unwanted_task); info!("Deleting task {:?}", unwanted_task);
tx.delete_static_tasks_by_code(type_group.thing_type, unwanted_task).await?; tx.delete_static_tasks_by_code(type_group.thing_type, unwanted_task)
.await?;
} }
for new_task_code in expected_set.difference(&existing_tasks) { for new_task_code in expected_set.difference(&existing_tasks) {
info!("Creating task {:?}", new_task_code); info!("Creating task {:?}", new_task_code);
tx.upsert_task(&(expected_tasks.get(new_task_code) tx.upsert_task(&(expected_tasks.get(new_task_code).unwrap().initial_task)())
.unwrap().initial_task)()).await?; .await?;
} }
for existing_task_code in expected_set.intersection(&existing_tasks) { for existing_task_code in expected_set.intersection(&existing_tasks) {
tx.limited_update_static_task( tx.limited_update_static_task(&(expected_tasks
&(expected_tasks.get(existing_task_code) .get(existing_task_code)
.unwrap().initial_task)()).await?; .unwrap()
.initial_task)())
.await?;
} }
tx.commit().await?; tx.commit().await?;
info!("Committed any changes for static_content of task_type {}", type_group.thing_type); info!(
"Committed any changes for static_content of task_type {}",
type_group.thing_type
);
} }
Ok(()) Ok(())
} }
@ -145,34 +176,40 @@ pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use itertools::Itertools;
use super::*; use super::*;
use itertools::Itertools;
#[test] #[test]
fn no_duplicate_static_items() { fn no_duplicate_static_items() {
let mut registry = static_item_registry(); let mut registry = static_item_registry();
registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type)); registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type));
let duplicates: Vec<&'static str> = let duplicates: Vec<&'static str> = registry
registry.iter() .iter()
.group_by(|x| x.thing_type).into_iter() .group_by(|x| x.thing_type)
.into_iter()
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
.collect(); .collect();
if duplicates.len() > 0 { if duplicates.len() > 0 {
panic!("static_item_registry has duplicate item_types: {:}", duplicates.join(", ")); panic!(
"static_item_registry has duplicate item_types: {:}",
duplicates.join(", ")
);
} }
for type_group in registry.iter() { for type_group in registry.iter() {
let iterator : Box<dyn Iterator<Item = StaticItem>> = (type_group.things)(); let iterator: Box<dyn Iterator<Item = StaticItem>> = (type_group.things)();
let duplicates: Vec<&'static str> = iterator let duplicates: Vec<&'static str> = iterator
.group_by(|x| x.item_code) .group_by(|x| x.item_code)
.into_iter() .into_iter()
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
.collect(); .collect();
if duplicates.len() > 0 { if duplicates.len() > 0 {
panic!("static_item_registry has duplicate item_codes for {}: {:}", panic!(
"static_item_registry has duplicate item_codes for {}: {:}",
type_group.thing_type, type_group.thing_type,
duplicates.join(", ")); duplicates.join(", ")
);
} }
} }
} }
@ -182,26 +219,32 @@ mod test {
let mut registry = static_task_registry(); let mut registry = static_task_registry();
registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type)); registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type));
let duplicates: Vec<&'static str> = let duplicates: Vec<&'static str> = registry
registry.iter() .iter()
.group_by(|x| x.thing_type).into_iter() .group_by(|x| x.thing_type)
.into_iter()
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
.collect(); .collect();
if duplicates.len() > 0 { if duplicates.len() > 0 {
panic!("static_task_registry has duplicate task_types: {:}", duplicates.join(", ")); panic!(
"static_task_registry has duplicate task_types: {:}",
duplicates.join(", ")
);
} }
for type_group in registry.iter() { for type_group in registry.iter() {
let iterator : Box<dyn Iterator<Item = StaticTask>> = (type_group.things)(); let iterator: Box<dyn Iterator<Item = StaticTask>> = (type_group.things)();
let duplicates: Vec<String> = iterator let duplicates: Vec<String> = iterator
.group_by(|x| x.task_code.clone()) .group_by(|x| x.task_code.clone())
.into_iter() .into_iter()
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) }) .filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
.collect(); .collect();
if duplicates.len() > 0 { if duplicates.len() > 0 {
panic!("static_task_registry has duplicate task_codes for {}: {:}", panic!(
"static_task_registry has duplicate task_codes for {}: {:}",
type_group.thing_type, type_group.thing_type,
duplicates.join(", ")); duplicates.join(", ")
);
} }
} }
} }

View File

@ -1,8 +1,12 @@
// For things like signs that don't do much except stay where they are and carry a description. // For things like signs that don't do much except stay where they are and carry a description.
use super::StaticItem; use super::{possession_type::PossessionData, StaticItem};
use once_cell::sync::OnceCell; use crate::{
use crate::models::item::{Item, Pronouns}; models::item::{Item, LiquidType, Pronouns},
static_content::possession_type::LiquidContainerData,
};
use ansi::ansi; use ansi::ansi;
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
pub struct FixedItem { pub struct FixedItem {
pub code: &'static str, pub code: &'static str,
@ -11,17 +15,17 @@ pub struct FixedItem {
pub description_less_explicit: Option<&'static str>, pub description_less_explicit: Option<&'static str>,
pub location: &'static str, pub location: &'static str,
pub proper_noun: bool, pub proper_noun: bool,
pub aliases: Vec<&'static str> pub aliases: Vec<&'static str>,
} }
fn fixed_item_list() -> &'static Vec<FixedItem> { fn fixed_item_list() -> &'static Vec<FixedItem> {
static FIXED_ITEM_LIST: OnceCell<Vec<FixedItem>> = OnceCell::new(); static FIXED_ITEM_LIST: OnceCell<Vec<FixedItem>> = OnceCell::new();
FIXED_ITEM_LIST.get_or_init(|| vec!( FIXED_ITEM_LIST.get_or_init(|| {
vec![
FixedItem { FixedItem {
code: "repro_xv_updates_red_poster", code: "repro_xv_updates_red_poster",
name: ansi!("red poster"), name: ansi!("red poster"),
description: description: "A larger faded poster with a thick red border. It says:\n\
"A larger faded poster with a thick red border. It says:\n\
\t\"Dear newly memory wiped citizen! Welcome to Melbs! A lot \ \t\"Dear newly memory wiped citizen! Welcome to Melbs! A lot \
has changed since the memories your implant is based on were \ has changed since the memories your implant is based on were \
created. The global Gazos-Murlison Co empire fell in a nuclear \ created. The global Gazos-Murlison Co empire fell in a nuclear \
@ -36,9 +40,44 @@ fn fixed_item_list() -> &'static Vec<FixedItem> {
description_less_explicit: None, description_less_explicit: None,
location: "room/repro_xv_updates", location: "room/repro_xv_updates",
proper_noun: false, proper_noun: false,
aliases: vec!("poster") aliases: vec!["poster"],
} },
)) FixedItem {
code: "melbs_king_st_spring_fed_fountain",
name: "spring fed fountain",
description: ansi!("A stainless steel fountain, clearly old, but in surprisingly good \
condition. A discoloured bronze plaque attached to it proudly declares \
that it is fed by a natural spring underneath it. It was designed so that \
unused water runs off it into a dog bowl - presumably in a time long past when \
dogs were friendly companions and not the menace they are today. It smells \
faintly of iron. [Try <bold>drink from fountain<reset> or, if you have a suitable \
container, <bold>fill<reset> container <bold>from fountain<reset>]."),
description_less_explicit: None,
location: "room/melbs_kingst_40",
proper_noun: false,
aliases: vec!["fountain"],
},
]
})
}
pub fn fixed_item_properties() -> &'static BTreeMap<&'static str, PossessionData> {
static PROPS: OnceCell<BTreeMap<&'static str, PossessionData>> = OnceCell::new();
PROPS.get_or_init(|| {
vec![(
"melbs_king_st_spring_fed_fountain",
PossessionData {
liquid_container_data: Some(LiquidContainerData {
capacity: 5000000, // mL
allowed_contents: Some(vec![LiquidType::Water]),
..Default::default()
}),
..Default::default()
},
)]
.into_iter()
.collect()
})
} }
pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> { pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
@ -49,7 +88,7 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
item_type: "fixed_item".to_owned(), item_type: "fixed_item".to_owned(),
display: r.name.to_owned(), display: r.name.to_owned(),
details: Some(r.description.to_owned()), details: Some(r.description.to_owned()),
details_less_explicit: r.description_less_explicit.map(|d|d.to_owned()), details_less_explicit: r.description_less_explicit.map(|d| d.to_owned()),
location: r.location.to_owned(), location: r.location.to_owned(),
is_static: true, is_static: true,
aliases: r.aliases.iter().map(|s| (*s).to_owned()).collect(), aliases: r.aliases.iter().map(|s| (*s).to_owned()).collect(),
@ -58,6 +97,6 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
..Pronouns::default_inanimate() ..Pronouns::default_inanimate()
}, },
..Item::default() ..Item::default()
}) }),
})) }))
} }

View File

@ -12,6 +12,7 @@ use crate::{
user::User, user::User,
}, },
regular_tasks::queued_command::QueuedCommandContext, regular_tasks::queued_command::QueuedCommandContext,
services::urges::recalculate_urge_growth,
}; };
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;
@ -278,6 +279,7 @@ async fn stat_command(
ctx.trans.save_user_model(user).await?; ctx.trans.save_user_model(user).await?;
let mut item_updated = item.clone(); let mut item_updated = item.clone();
item_updated.total_stats = user.raw_stats.clone(); item_updated.total_stats = user.raw_stats.clone();
recalculate_urge_growth(&ctx.trans, &mut item_updated).await?;
ctx.trans.save_item_model(&item_updated).await?; ctx.trans.save_item_model(&item_updated).await?;
reply( reply(
ctx, ctx,

View File

@ -3,7 +3,7 @@ use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{UResult, VerbContext}, message_handler::user_commands::{UResult, VerbContext},
models::consent::ConsentType, models::consent::ConsentType,
models::item::{Item, ItemFlag, Pronouns, SkillType}, models::item::{Item, ItemFlag, LiquidType, Pronouns, SkillType},
regular_tasks::queued_command::QueuedCommandContext, regular_tasks::queued_command::QueuedCommandContext,
static_content::{room::Direction, species::BodyPart}, static_content::{room::Direction, species::BodyPart},
}; };
@ -286,6 +286,24 @@ impl Default for ContainerData {
} }
} }
pub struct LiquidContainerData {
pub capacity: u64, // in mL
pub allowed_contents: Option<Vec<LiquidType>>, // None means anything.
}
impl Default for LiquidContainerData {
fn default() -> Self {
Self {
capacity: 1000,
allowed_contents: None,
}
}
}
pub struct EatData {
pub hunger_impact: i16,
pub thirst_impact: i16,
}
pub struct PossessionData { pub struct PossessionData {
pub weapon_data: Option<WeaponData>, pub weapon_data: Option<WeaponData>,
pub display: &'static str, pub display: &'static str,
@ -306,7 +324,9 @@ pub struct PossessionData {
pub bench_data: Option<&'static (dyn BenchData + Sync + Send)>, pub bench_data: Option<&'static (dyn BenchData + Sync + Send)>,
pub wear_data: Option<WearData>, pub wear_data: Option<WearData>,
pub container_data: Option<ContainerData>, pub container_data: Option<ContainerData>,
pub liquid_container_data: Option<LiquidContainerData>,
pub default_flags: Vec<ItemFlag>, pub default_flags: Vec<ItemFlag>,
pub eat_data: Option<EatData>,
} }
impl Default for PossessionData { impl Default for PossessionData {
@ -331,7 +351,9 @@ impl Default for PossessionData {
bench_data: None, bench_data: None,
wear_data: None, wear_data: None,
container_data: None, container_data: None,
liquid_container_data: None,
default_flags: vec![], default_flags: vec![],
eat_data: None,
} }
} }
} }

View File

@ -1,4 +1,6 @@
use super::{PossessionData, PossessionType}; use crate::static_content::possession_type::EatData;
use super::{ChargeData, PossessionData, PossessionType};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
@ -19,6 +21,15 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "steak", display: "steak",
details: "A hunk of raw red meat, dripping with blood", details: "A hunk of raw red meat, dripping with blood",
weight: 250, weight: 250,
eat_data: Some(EatData {
hunger_impact: -500,
thirst_impact: 0,
}),
charge_data: Some(ChargeData {
max_charges: 20,
charge_name_prefix: "bite",
charge_name_suffix: "of food",
}),
..Default::default() ..Default::default()
} }
), ),
@ -38,6 +49,15 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "grilled steak", display: "grilled steak",
details: "A mouth-wateringly grilled steak, its outer brown surface a perfect demonstration of the Maillard reaction, with a thin bit of fat adjoining the lean protein", details: "A mouth-wateringly grilled steak, its outer brown surface a perfect demonstration of the Maillard reaction, with a thin bit of fat adjoining the lean protein",
weight: 250, weight: 250,
eat_data: Some(EatData {
hunger_impact: -600,
thirst_impact: 0,
}),
charge_data: Some(ChargeData {
max_charges: 20,
charge_name_prefix: "bite",
charge_name_suffix: "of food",
}),
..Default::default() ..Default::default()
} }
), ),

View File

@ -1,5 +1,5 @@
use super::{Direction, Exit, ExitTarget, ExitType, GridCoords, Room, SecondaryZoneRecord}; use super::{Direction, Exit, ExitTarget, ExitType, GridCoords, Room, SecondaryZoneRecord};
use crate::static_content::npc; use crate::{models::item::ItemFlag, static_content::npc};
use ansi::ansi; use ansi::ansi;
pub fn room_list() -> Vec<Room> { pub fn room_list() -> Vec<Room> {
@ -28,6 +28,7 @@ pub fn room_list() -> Vec<Room> {
exit_type: ExitType::Blocked(&npc::statbot::ChoiceRoomBlocker), exit_type: ExitType::Blocked(&npc::statbot::ChoiceRoomBlocker),
..Default::default() ..Default::default()
}), }),
item_flags: vec![ItemFlag::NoUrgesHere],
should_caption: true, should_caption: true,
..Default::default() ..Default::default()
}, },

View File

@ -33,6 +33,7 @@ CREATE INDEX item_by_following ON items ((details->'following'->>'follow_whom'))
CREATE UNIQUE INDEX item_dynamic_entrance ON items ( CREATE UNIQUE INDEX item_dynamic_entrance ON items (
(details->'dynamic_entrance'->>'source_item'), (details->'dynamic_entrance'->>'source_item'),
(LOWER(details->'dynamic_entrance'->>'direction'))); (LOWER(details->'dynamic_entrance'->>'direction')));
CREATE INDEX item_id_with_urges ON items (item_id) WHERE details->'flags' @> '"HasUrges"';
CREATE TABLE users ( CREATE TABLE users (
-- Username here is all lower case, but details has correct case version. -- Username here is all lower case, but details has correct case version.