Implement hunger, eating, and drinking.
This commit is contained in:
parent
a1495e6731
commit
fab18d604e
@ -653,7 +653,7 @@ impl DBTrans {
|
||||
) -> DResult<()> {
|
||||
self.pg_trans()?
|
||||
.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_code' = $2",
|
||||
&[&task_type, &task_code],
|
||||
@ -1717,6 +1717,75 @@ impl DBTrans {
|
||||
.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<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
if let Some(trans) = trans_opt {
|
||||
|
@ -2,6 +2,7 @@ use super::ListenerSession;
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::db::{DBPool, ItemSearchParams};
|
||||
use crate::models::user::UserFlag;
|
||||
use crate::models::{item::Item, session::Session, user::User};
|
||||
use crate::DResult;
|
||||
#[cfg(not(test))]
|
||||
@ -25,7 +26,9 @@ pub mod corp;
|
||||
pub mod cut;
|
||||
pub mod delete;
|
||||
mod describe;
|
||||
pub mod drink;
|
||||
pub mod drop;
|
||||
pub mod eat;
|
||||
mod fire;
|
||||
pub mod follow;
|
||||
mod gear;
|
||||
@ -53,6 +56,7 @@ pub mod register;
|
||||
pub mod remove;
|
||||
pub mod rent;
|
||||
mod report;
|
||||
mod reset_spawns;
|
||||
pub mod say;
|
||||
mod score;
|
||||
mod sign;
|
||||
@ -152,7 +156,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"corp" => corp::VERB,
|
||||
"cut" => cut::VERB,
|
||||
"delete" => delete::VERB,
|
||||
"drink" => drink::VERB,
|
||||
"drop" => drop::VERB,
|
||||
"eat" => eat::VERB,
|
||||
|
||||
"fire" => fire::VERB,
|
||||
|
||||
@ -231,6 +237,10 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"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> {
|
||||
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) => {
|
||||
if user_dat.terms.terms_complete {
|
||||
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" {
|
||||
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)
|
||||
- user_dat.experience.xp_change_for_this_reroll)
|
||||
.max(0) as u64;
|
||||
player_item.urges = Some(Default::default());
|
||||
user_dat.experience.xp_change_for_this_reroll = 0;
|
||||
user_dat.raw_stats = 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::{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 tokio::time;
|
||||
|
||||
@ -59,6 +60,9 @@ impl UserVerb for Verb {
|
||||
ctx.trans.save_user_model(user).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(())
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ use crate::db::DBTrans;
|
||||
use crate::{
|
||||
db::ItemSearchParams,
|
||||
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},
|
||||
static_content::{
|
||||
dynzone,
|
||||
@ -75,6 +78,44 @@ pub async fn describe_normal_item(
|
||||
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
|
||||
.iter()
|
||||
.any(|it| it.action_type == LocationActionType::Worn);
|
||||
|
@ -23,6 +23,7 @@ use crate::{
|
||||
combat::{change_health, handle_resurrect, stop_attacking_mut},
|
||||
comms::broadcast_to_room,
|
||||
skills::skill_check_and_grind,
|
||||
urges::{recalculate_urge_growth, thirst_changed},
|
||||
},
|
||||
static_content::{
|
||||
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.action_type = LocationActionType::Normal;
|
||||
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 crate::services::combat::max_health;
|
||||
use crate::{models::item::Urges, services::combat::max_health};
|
||||
use ansi::ansi;
|
||||
use async_trait::async_trait;
|
||||
|
||||
@ -41,6 +41,41 @@ impl UserVerb for Verb {
|
||||
player_item.health,
|
||||
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!(
|
||||
ansi!("<bold>Credits <green>${}<reset>\n"),
|
||||
user.credits
|
||||
|
@ -2,9 +2,12 @@ use super::session::Session;
|
||||
use crate::{
|
||||
language,
|
||||
regular_tasks::queued_command::QueueCommand,
|
||||
static_content::possession_type::{possession_data, PossessionType},
|
||||
static_content::room::Direction,
|
||||
static_content::species::SpeciesType,
|
||||
static_content::{
|
||||
fixed_item::fixed_item_properties,
|
||||
possession_type::{possession_data, EatData, PossessionData, PossessionType},
|
||||
room::Direction,
|
||||
species::SpeciesType,
|
||||
},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
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)]
|
||||
pub struct Pronouns {
|
||||
pub subject: String,
|
||||
@ -264,6 +302,8 @@ pub enum ItemFlag {
|
||||
Bench,
|
||||
Book,
|
||||
Instructions,
|
||||
HasUrges,
|
||||
NoUrgesHere,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub enum ItemSpecialData {
|
||||
ItemWriting {
|
||||
@ -400,6 +478,8 @@ pub struct Item {
|
||||
pub door_states: Option<BTreeMap<Direction, DoorState>>,
|
||||
pub following: Option<FollowData>,
|
||||
pub queue: VecDeque<QueueCommand>,
|
||||
pub urges: Option<Urges>,
|
||||
pub liquid_details: Option<LiquidDetails>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
@ -469,6 +549,19 @@ impl Item {
|
||||
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 {
|
||||
@ -507,6 +600,8 @@ impl Default for Item {
|
||||
door_states: None,
|
||||
following: None,
|
||||
queue: VecDeque::new(),
|
||||
urges: None,
|
||||
liquid_details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,8 @@ pub enum TaskDetails {
|
||||
ChargeWages {
|
||||
npc: String,
|
||||
},
|
||||
TickUrges,
|
||||
ResetSpawns,
|
||||
}
|
||||
impl TaskDetails {
|
||||
pub fn name(self: &Self) -> &'static str {
|
||||
@ -70,6 +72,8 @@ impl TaskDetails {
|
||||
SwingShut { .. } => "SwingShut",
|
||||
DestroyUser { .. } => "DestroyUser",
|
||||
ChargeWages { .. } => "ChargeWages",
|
||||
TickUrges => "TickUrges",
|
||||
ResetSpawns => "ResetSpawns",
|
||||
// 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>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum UserFlag {
|
||||
Staff,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct User {
|
||||
@ -42,6 +47,7 @@ pub struct User {
|
||||
pub last_page_from: Option<String>,
|
||||
pub credits: u64,
|
||||
pub danger_code: Option<String>,
|
||||
pub user_flags: Vec<UserFlag>,
|
||||
// Reminder: Consider backwards compatibility when updating this.
|
||||
}
|
||||
|
||||
@ -86,6 +92,7 @@ impl Default for User {
|
||||
last_page_from: None,
|
||||
credits: 500,
|
||||
danger_code: None,
|
||||
user_flags: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use crate::{
|
||||
listener::{ListenerMap, ListenerSend},
|
||||
message_handler::user_commands::{delete, drop, hire, open, rent},
|
||||
models::task::Task,
|
||||
services::{combat, effect},
|
||||
services::{combat, effect, spawn, urges},
|
||||
static_content::npc,
|
||||
DResult,
|
||||
};
|
||||
@ -55,6 +55,8 @@ fn task_handler_registry(
|
||||
("SwingShut", open::SWING_SHUT_HANDLER.clone()),
|
||||
("DestroyUser", delete::DESTROY_USER_HANDLER.clone()),
|
||||
("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()),
|
||||
("TickUrges", urges::TICK_URGES_HANDLER.clone()),
|
||||
("ResetSpawns", spawn::RESET_SPAWNS_HANDLER.clone()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
|
@ -2,8 +2,8 @@ use super::{TaskHandler, TaskRunContext};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::message_handler::user_commands::{
|
||||
close, cut, drop, get, improvise, make, movement, open, put, remove, use_cmd, user_error, wear,
|
||||
wield, CommandHandlingError, UResult, VerbContext,
|
||||
close, cut, drink, drop, eat, get, improvise, make, movement, open, put, remove, use_cmd,
|
||||
user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
|
||||
};
|
||||
use crate::message_handler::ListenerSession;
|
||||
use crate::models::session::Session;
|
||||
@ -57,9 +57,16 @@ pub enum QueueCommand {
|
||||
from_corpse: String,
|
||||
what_part: String,
|
||||
},
|
||||
Drink {
|
||||
item_type: String,
|
||||
item_code: String,
|
||||
},
|
||||
Drop {
|
||||
possession_id: String,
|
||||
},
|
||||
Eat {
|
||||
possession_id: String,
|
||||
},
|
||||
Get {
|
||||
possession_id: String,
|
||||
},
|
||||
@ -111,7 +118,9 @@ impl QueueCommand {
|
||||
match self {
|
||||
CloseDoor { .. } => "CloseDoor",
|
||||
Cut { .. } => "Cut",
|
||||
Drink { .. } => "Drink",
|
||||
Drop { .. } => "Drop",
|
||||
Eat { .. } => "Eat",
|
||||
Get { .. } => "Get",
|
||||
GetFromContainer { .. } => "GetFromContainer",
|
||||
Make { .. } => "Make",
|
||||
@ -175,6 +184,10 @@ fn queue_command_registry(
|
||||
"CloseDoor",
|
||||
&close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Drink",
|
||||
&drink::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Drop",
|
||||
&drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
@ -187,6 +200,10 @@ fn queue_command_registry(
|
||||
"GetFromContainer",
|
||||
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Eat",
|
||||
&eat::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Make",
|
||||
&make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
|
@ -21,6 +21,8 @@ pub mod combat;
|
||||
pub mod comms;
|
||||
pub mod effect;
|
||||
pub mod skills;
|
||||
pub mod spawn;
|
||||
pub mod urges;
|
||||
|
||||
fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
|
||||
if let Some((loctype, loccode)) = target.location.split_once("/") {
|
||||
|
@ -5,7 +5,7 @@ use crate::{
|
||||
follow::cancel_follow_by_leader, user_error, CommandHandlingError, UResult,
|
||||
},
|
||||
models::{
|
||||
item::{DeathData, Item, LocationActionType, SkillType, Subattack},
|
||||
item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack},
|
||||
journal::JournalType,
|
||||
task::{Task, TaskDetails, TaskMeta},
|
||||
},
|
||||
@ -14,6 +14,7 @@ use crate::{
|
||||
comms::broadcast_to_room,
|
||||
destroy_container,
|
||||
skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only},
|
||||
urges::recalculate_urge_growth,
|
||||
},
|
||||
static_content::{
|
||||
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;
|
||||
user.experience.xp_change_for_this_reroll -= lost_xp as i64;
|
||||
player.temporary_buffs = vec![];
|
||||
player.flags.push(ItemFlag::HasUrges);
|
||||
calculate_total_stats_skills_for_user(player, &user);
|
||||
recalculate_urge_growth(trans, player).await?;
|
||||
|
||||
player.health = max_health(&player);
|
||||
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,138 +1,169 @@
|
||||
use crate::DResult;
|
||||
use crate::db::DBPool;
|
||||
use crate::models::{item::Item, task::Task};
|
||||
use std::collections::{BTreeSet, BTreeMap};
|
||||
use crate::{
|
||||
db::DBPool,
|
||||
models::{item::Item, task::Task},
|
||||
services::{spawn, urges},
|
||||
DResult,
|
||||
};
|
||||
use log::info;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub mod room;
|
||||
pub mod dynzone;
|
||||
pub mod fixed_item;
|
||||
pub mod journals;
|
||||
pub mod npc;
|
||||
pub mod possession_type;
|
||||
pub mod room;
|
||||
pub mod species;
|
||||
pub mod journals;
|
||||
mod fixed_item;
|
||||
|
||||
pub struct StaticItem {
|
||||
pub item_code: &'static str,
|
||||
pub initial_item: Box<dyn Fn() -> Item>
|
||||
pub initial_item: Box<dyn Fn() -> Item>,
|
||||
}
|
||||
|
||||
pub struct StaticTask {
|
||||
pub task_code: String,
|
||||
pub initial_task: Box<dyn Fn() -> Task>
|
||||
pub initial_task: Box<dyn Fn() -> Task>,
|
||||
}
|
||||
|
||||
struct StaticThingTypeGroup<Thing> {
|
||||
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>> {
|
||||
vec!(
|
||||
vec![
|
||||
// Must have no duplicates.
|
||||
StaticThingTypeGroup::<StaticItem> {
|
||||
thing_type: "npc",
|
||||
things: || npc::npc_static_items()
|
||||
things: || npc::npc_static_items(),
|
||||
},
|
||||
StaticThingTypeGroup::<StaticItem> {
|
||||
thing_type: "room",
|
||||
things: || room::room_static_items()
|
||||
things: || room::room_static_items(),
|
||||
},
|
||||
StaticThingTypeGroup::<StaticItem> {
|
||||
thing_type: "fixed_item",
|
||||
things: || fixed_item::static_items()
|
||||
things: || fixed_item::static_items(),
|
||||
},
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
fn static_task_registry() -> Vec<StaticThingTypeGroup<StaticTask>> {
|
||||
vec!(
|
||||
vec![
|
||||
// Must have no duplicates.
|
||||
StaticThingTypeGroup::<StaticTask> {
|
||||
thing_type: "NPCSay",
|
||||
things: || npc::npc_say_tasks()
|
||||
things: || npc::npc_say_tasks(),
|
||||
},
|
||||
StaticThingTypeGroup::<StaticTask> {
|
||||
thing_type: "NPCWander",
|
||||
things: || npc::npc_wander_tasks()
|
||||
things: || npc::npc_wander_tasks(),
|
||||
},
|
||||
StaticThingTypeGroup::<StaticTask> {
|
||||
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<()> {
|
||||
let registry = static_item_registry();
|
||||
|
||||
|
||||
let expected_type: BTreeSet<String> =
|
||||
registry.iter().map(|x| x.thing_type.to_owned()).collect();
|
||||
let cur_types: Box<BTreeSet<String>> = pool.find_static_item_types().await?;
|
||||
for item_type in cur_types.difference(&expected_type) {
|
||||
pool.delete_static_items_by_type(item_type).await?;
|
||||
}
|
||||
|
||||
|
||||
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 existing_items = tx.find_static_items_by_type(type_group.thing_type).await?;
|
||||
let expected_items: BTreeMap<String, StaticItem> =
|
||||
(type_group.things)().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_items: BTreeMap<String, StaticItem> = (type_group.things)()
|
||||
.map(|x| (x.item_code.to_owned(), x))
|
||||
.collect();
|
||||
let expected_set: BTreeSet<String> = expected_items.keys().map(|x| x.to_owned()).collect();
|
||||
for unwanted_item in existing_items.difference(&expected_set) {
|
||||
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) {
|
||||
info!("Creating item {:?}", new_item_code);
|
||||
tx.create_item(&(expected_items.get(new_item_code)
|
||||
.unwrap().initial_item)()).await?;
|
||||
tx.create_item(&(expected_items.get(new_item_code).unwrap().initial_item)())
|
||||
.await?;
|
||||
}
|
||||
for existing_item_code in expected_set.intersection(&existing_items) {
|
||||
tx.limited_update_static_item(
|
||||
&(expected_items.get(existing_item_code)
|
||||
.unwrap().initial_item)()).await?;
|
||||
tx.limited_update_static_item(&(expected_items
|
||||
.get(existing_item_code)
|
||||
.unwrap()
|
||||
.initial_item)())
|
||||
.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(())
|
||||
}
|
||||
|
||||
async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> {
|
||||
let registry = static_task_registry();
|
||||
|
||||
|
||||
let expected_type: BTreeSet<String> =
|
||||
registry.iter().map(|x| x.thing_type.to_owned()).collect();
|
||||
let cur_types: Box<BTreeSet<String>> = pool.find_static_task_types().await?;
|
||||
for task_type in cur_types.difference(&expected_type) {
|
||||
pool.delete_static_tasks_by_type(task_type).await?;
|
||||
}
|
||||
|
||||
|
||||
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 existing_tasks = tx.find_static_tasks_by_type(type_group.thing_type).await?;
|
||||
let expected_tasks: BTreeMap<String, StaticTask> =
|
||||
(type_group.things)().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_tasks: BTreeMap<String, StaticTask> = (type_group.things)()
|
||||
.map(|x| (x.task_code.to_owned(), x))
|
||||
.collect();
|
||||
let expected_set: BTreeSet<String> = expected_tasks.keys().map(|x| x.to_owned()).collect();
|
||||
for unwanted_task in existing_tasks.difference(&expected_set) {
|
||||
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) {
|
||||
info!("Creating task {:?}", new_task_code);
|
||||
tx.upsert_task(&(expected_tasks.get(new_task_code)
|
||||
.unwrap().initial_task)()).await?;
|
||||
tx.upsert_task(&(expected_tasks.get(new_task_code).unwrap().initial_task)())
|
||||
.await?;
|
||||
}
|
||||
for existing_task_code in expected_set.intersection(&existing_tasks) {
|
||||
tx.limited_update_static_task(
|
||||
&(expected_tasks.get(existing_task_code)
|
||||
.unwrap().initial_task)()).await?;
|
||||
tx.limited_update_static_task(&(expected_tasks
|
||||
.get(existing_task_code)
|
||||
.unwrap()
|
||||
.initial_task)())
|
||||
.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(())
|
||||
}
|
||||
@ -145,34 +176,40 @@ pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use itertools::Itertools;
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_static_items() {
|
||||
let mut registry = static_item_registry();
|
||||
registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type));
|
||||
|
||||
let duplicates: Vec<&'static str> =
|
||||
registry.iter()
|
||||
.group_by(|x| x.thing_type).into_iter()
|
||||
|
||||
let duplicates: Vec<&'static str> = registry
|
||||
.iter()
|
||||
.group_by(|x| x.thing_type)
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
|
||||
.collect();
|
||||
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() {
|
||||
let iterator : Box<dyn Iterator<Item = StaticItem>> = (type_group.things)();
|
||||
let iterator: Box<dyn Iterator<Item = StaticItem>> = (type_group.things)();
|
||||
let duplicates: Vec<&'static str> = iterator
|
||||
.group_by(|x| x.item_code)
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
|
||||
.collect();
|
||||
if duplicates.len() > 0 {
|
||||
panic!("static_item_registry has duplicate item_codes for {}: {:}",
|
||||
type_group.thing_type,
|
||||
duplicates.join(", "));
|
||||
panic!(
|
||||
"static_item_registry has duplicate item_codes for {}: {:}",
|
||||
type_group.thing_type,
|
||||
duplicates.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,27 +218,33 @@ mod test {
|
||||
fn no_duplicate_static_tasks() {
|
||||
let mut registry = static_task_registry();
|
||||
registry.sort_unstable_by(|x, y| x.thing_type.cmp(y.thing_type));
|
||||
|
||||
let duplicates: Vec<&'static str> =
|
||||
registry.iter()
|
||||
.group_by(|x| x.thing_type).into_iter()
|
||||
|
||||
let duplicates: Vec<&'static str> = registry
|
||||
.iter()
|
||||
.group_by(|x| x.thing_type)
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
|
||||
.collect();
|
||||
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() {
|
||||
let iterator : Box<dyn Iterator<Item = StaticTask>> = (type_group.things)();
|
||||
let iterator: Box<dyn Iterator<Item = StaticTask>> = (type_group.things)();
|
||||
let duplicates: Vec<String> = iterator
|
||||
.group_by(|x| x.task_code.clone())
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| if v.count() <= 1 { None } else { Some(k) })
|
||||
.collect();
|
||||
if duplicates.len() > 0 {
|
||||
panic!("static_task_registry has duplicate task_codes for {}: {:}",
|
||||
type_group.thing_type,
|
||||
duplicates.join(", "));
|
||||
panic!(
|
||||
"static_task_registry has duplicate task_codes for {}: {:}",
|
||||
type_group.thing_type,
|
||||
duplicates.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
// For things like signs that don't do much except stay where they are and carry a description.
|
||||
use super::StaticItem;
|
||||
use once_cell::sync::OnceCell;
|
||||
use crate::models::item::{Item, Pronouns};
|
||||
use super::{possession_type::PossessionData, StaticItem};
|
||||
use crate::{
|
||||
models::item::{Item, LiquidType, Pronouns},
|
||||
static_content::possession_type::LiquidContainerData,
|
||||
};
|
||||
use ansi::ansi;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct FixedItem {
|
||||
pub code: &'static str,
|
||||
@ -11,34 +15,69 @@ pub struct FixedItem {
|
||||
pub description_less_explicit: Option<&'static str>,
|
||||
pub location: &'static str,
|
||||
pub proper_noun: bool,
|
||||
pub aliases: Vec<&'static str>
|
||||
pub aliases: Vec<&'static str>,
|
||||
}
|
||||
|
||||
fn fixed_item_list() -> &'static Vec<FixedItem> {
|
||||
static FIXED_ITEM_LIST: OnceCell<Vec<FixedItem>> = OnceCell::new();
|
||||
FIXED_ITEM_LIST.get_or_init(|| vec!(
|
||||
FixedItem {
|
||||
code: "repro_xv_updates_red_poster",
|
||||
name: ansi!("red poster"),
|
||||
description:
|
||||
"A larger faded poster with a thick red border. It says:\n\
|
||||
\t\"Dear newly memory wiped citizen! Welcome to Melbs! A lot \
|
||||
has changed since the memories your implant is based on were \
|
||||
created. The global Gazos-Murlison Co empire fell in a nuclear \
|
||||
attack, and most cities of the world were destroyed. \
|
||||
A few cities around Australia, like this one, took some fallout \
|
||||
but survived. The few remaining cities are now all independently \
|
||||
run. I was a young governor under the empire, and I now rule inner \
|
||||
Melbs as the King. I have gotten all the fallout out from the inner city, \
|
||||
and I have a robot police force to keep you safe from the worst baddies, \
|
||||
but be warned - there still are some dangers near by, and the world \
|
||||
further out, outside my realm, is a dangerous and radioactive place.\"",
|
||||
description_less_explicit: None,
|
||||
location: "room/repro_xv_updates",
|
||||
proper_noun: false,
|
||||
aliases: vec!("poster")
|
||||
}
|
||||
))
|
||||
FIXED_ITEM_LIST.get_or_init(|| {
|
||||
vec![
|
||||
FixedItem {
|
||||
code: "repro_xv_updates_red_poster",
|
||||
name: ansi!("red poster"),
|
||||
description: "A larger faded poster with a thick red border. It says:\n\
|
||||
\t\"Dear newly memory wiped citizen! Welcome to Melbs! A lot \
|
||||
has changed since the memories your implant is based on were \
|
||||
created. The global Gazos-Murlison Co empire fell in a nuclear \
|
||||
attack, and most cities of the world were destroyed. \
|
||||
A few cities around Australia, like this one, took some fallout \
|
||||
but survived. The few remaining cities are now all independently \
|
||||
run. I was a young governor under the empire, and I now rule inner \
|
||||
Melbs as the King. I have gotten all the fallout out from the inner city, \
|
||||
and I have a robot police force to keep you safe from the worst baddies, \
|
||||
but be warned - there still are some dangers near by, and the world \
|
||||
further out, outside my realm, is a dangerous and radioactive place.\"",
|
||||
description_less_explicit: None,
|
||||
location: "room/repro_xv_updates",
|
||||
proper_noun: false,
|
||||
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>> {
|
||||
@ -49,7 +88,7 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||
item_type: "fixed_item".to_owned(),
|
||||
display: r.name.to_owned(),
|
||||
details: Some(r.description.to_owned()),
|
||||
details_less_explicit: r.description_less_explicit.map(|d|d.to_owned()),
|
||||
details_less_explicit: r.description_less_explicit.map(|d| d.to_owned()),
|
||||
location: r.location.to_owned(),
|
||||
is_static: true,
|
||||
aliases: r.aliases.iter().map(|s| (*s).to_owned()).collect(),
|
||||
@ -58,6 +97,6 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||
..Pronouns::default_inanimate()
|
||||
},
|
||||
..Item::default()
|
||||
})
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ use crate::{
|
||||
user::User,
|
||||
},
|
||||
regular_tasks::queued_command::QueuedCommandContext,
|
||||
services::urges::recalculate_urge_growth,
|
||||
};
|
||||
use ansi::ansi;
|
||||
use async_trait::async_trait;
|
||||
@ -278,6 +279,7 @@ async fn stat_command(
|
||||
ctx.trans.save_user_model(user).await?;
|
||||
let mut item_updated = item.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?;
|
||||
reply(
|
||||
ctx,
|
||||
|
@ -3,7 +3,7 @@ use crate::db::DBTrans;
|
||||
use crate::{
|
||||
message_handler::user_commands::{UResult, VerbContext},
|
||||
models::consent::ConsentType,
|
||||
models::item::{Item, ItemFlag, Pronouns, SkillType},
|
||||
models::item::{Item, ItemFlag, LiquidType, Pronouns, SkillType},
|
||||
regular_tasks::queued_command::QueuedCommandContext,
|
||||
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 weapon_data: Option<WeaponData>,
|
||||
pub display: &'static str,
|
||||
@ -306,7 +324,9 @@ pub struct PossessionData {
|
||||
pub bench_data: Option<&'static (dyn BenchData + Sync + Send)>,
|
||||
pub wear_data: Option<WearData>,
|
||||
pub container_data: Option<ContainerData>,
|
||||
pub liquid_container_data: Option<LiquidContainerData>,
|
||||
pub default_flags: Vec<ItemFlag>,
|
||||
pub eat_data: Option<EatData>,
|
||||
}
|
||||
|
||||
impl Default for PossessionData {
|
||||
@ -331,7 +351,9 @@ impl Default for PossessionData {
|
||||
bench_data: None,
|
||||
wear_data: None,
|
||||
container_data: None,
|
||||
liquid_container_data: None,
|
||||
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;
|
||||
|
||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
@ -19,6 +21,15 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
display: "steak",
|
||||
details: "A hunk of raw red meat, dripping with blood",
|
||||
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()
|
||||
}
|
||||
),
|
||||
@ -38,6 +49,15 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
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",
|
||||
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()
|
||||
}
|
||||
),
|
||||
|
@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
pub fn room_list() -> Vec<Room> {
|
||||
@ -28,6 +28,7 @@ pub fn room_list() -> Vec<Room> {
|
||||
exit_type: ExitType::Blocked(&npc::statbot::ChoiceRoomBlocker),
|
||||
..Default::default()
|
||||
}),
|
||||
item_flags: vec![ItemFlag::NoUrgesHere],
|
||||
should_caption: true,
|
||||
..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 (
|
||||
(details->'dynamic_entrance'->>'source_item'),
|
||||
(LOWER(details->'dynamic_entrance'->>'direction')));
|
||||
CREATE INDEX item_id_with_urges ON items (item_id) WHERE details->'flags' @> '"HasUrges"';
|
||||
|
||||
CREATE TABLE users (
|
||||
-- Username here is all lower case, but details has correct case version.
|
||||
|
Loading…
Reference in New Issue
Block a user