Implement hunger, eating, and drinking.
This commit is contained in:
parent
a1495e6731
commit
fab18d604e
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
249
blastmud_game/src/message_handler/user_commands/drink.rs
Normal file
249
blastmud_game/src/message_handler/user_commands/drink.rs
Normal 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;
|
250
blastmud_game/src/message_handler/user_commands/eat.rs
Normal file
250
blastmud_game/src/message_handler/user_commands/eat.rs
Normal 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;
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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),
|
||||||
|
@ -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("/") {
|
||||||
|
@ -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;
|
||||||
|
201
blastmud_game/src/services/spawn.rs
Normal file
201
blastmud_game/src/services/spawn.rs
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
377
blastmud_game/src/services/urges.rs
Normal file
377
blastmud_game/src/services/urges.rs
Normal 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(())
|
||||||
|
}
|
@ -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))
|
||||||
|
.collect();
|
||||||
let expected_set: BTreeSet<String> = expected_items.keys().map(|x| x.to_owned()).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))
|
||||||
|
.collect();
|
||||||
let expected_set: BTreeSet<String> = expected_tasks.keys().map(|x| x.to_owned()).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,21 +176,25 @@ 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() {
|
||||||
@ -170,9 +205,11 @@ mod test {
|
|||||||
.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,13 +219,17 @@ 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() {
|
||||||
@ -199,9 +240,11 @@ mod test {
|
|||||||
.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(", ")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>> {
|
||||||
@ -58,6 +97,6 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
|||||||
..Pronouns::default_inanimate()
|
..Pronouns::default_inanimate()
|
||||||
},
|
},
|
||||||
..Item::default()
|
..Item::default()
|
||||||
})
|
}),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -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()
|
||||||
},
|
},
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user