blastmud/blastmud_game/src/services/urges.rs

401 lines
13 KiB
Rust

#[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 change_stress_considering_cool(
trans: &DBTrans,
who: &mut Item,
max_magnitude: i64,
) -> DResult<()> {
if !who.flags.contains(&ItemFlag::HasUrges) {
return Ok(());
}
let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0);
let stress_factor = 1.0 - 1.0 / (1.0 + (-0.7 * (cool - 8.0)).exp());
match who.urges.as_mut() {
None => {}
Some(urges) => {
urges.stress.last_value = urges.stress.value;
urges.stress.value = (urges.stress.value as i64
+ (max_magnitude as f64 * stress_factor) as i64)
.max(0)
.min(10000) as u16;
}
}
stress_changed(trans, who).await
}
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(())
}