#[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> { 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> { 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(()) }