diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 13d204e..a5927ed 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -21,6 +21,7 @@ use std::collections::BTreeSet; use std::error::Error; use std::str::FromStr; use std::sync::Arc; +#[cfg(not(test))] use tokio_postgres::error::{DbError, SqlState}; use tokio_postgres::types::ToSql; use tokio_postgres::NoTls; @@ -1812,6 +1813,8 @@ impl DBTrans { } } +// This seems hard to test since most of DbError is private. +#[cfg(not(test))] pub fn is_concurrency_error(e: &(dyn Error + 'static)) -> bool { match e.source() { None => {} diff --git a/blastmud_game/src/main.rs b/blastmud_game/src/main.rs index 34389bf..b047481 100644 --- a/blastmud_game/src/main.rs +++ b/blastmud_game/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(test, allow(unused))] use db::DBPool; use log::{error, info, LevelFilter}; use serde::Deserialize; @@ -32,6 +33,7 @@ fn read_latest_config() -> DResult { } #[tokio::main(worker_threads = 2)] +#[cfg(not(test))] async fn main() -> DResult<()> { SimpleLogger::new() .with_level(LevelFilter::Info) diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 756dcbd..4827aca 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -1,7 +1,9 @@ use super::ListenerSession; +#[cfg(not(test))] +use crate::db::is_concurrency_error; #[double] use crate::db::DBTrans; -use crate::db::{is_concurrency_error, DBPool, ItemSearchParams}; +use crate::db::{DBPool, ItemSearchParams}; use crate::models::user::UserFlag; use crate::models::{item::Item, session::Session, user::User}; use crate::DResult; diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index c3ce11c..5b2ce78 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -419,10 +419,14 @@ pub async fn describe_room( ansi!(" "), &word_wrap( &format!( - ansi!("{} ({})\n{}.{}\n{}\n"), + ansi!("{} ({})\n{}{}.{}\n{}\n"), item.display_for_session(&ctx.session_dat), zone, item.details_for_session(&ctx.session_dat).unwrap_or(""), + item.details_dyn_suffix + .as_ref() + .map(|d| d.as_str()) + .unwrap_or(""), contents, exits_for(room) ), @@ -504,7 +508,9 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe let all_groups: Vec>> = items .iter() - .filter(|i| i.action_type.is_visible_in_look()) + .filter(|i| { + i.action_type.is_visible_in_look() && !i.flags.contains(&ItemFlag::DontListInLook) + }) .group_by(|i| i.display_for_sentence(true, 1, false)) .into_iter() .map(|(_, g)| g.collect::>>()) diff --git a/blastmud_game/src/message_handler/user_commands/open.rs b/blastmud_game/src/message_handler/user_commands/open.rs index b07ce1c..e8aad69 100644 --- a/blastmud_game/src/message_handler/user_commands/open.rs +++ b/blastmud_game/src/message_handler/user_commands/open.rs @@ -16,7 +16,7 @@ use crate::{ TaskHandler, TaskRunContext, }, services::comms::broadcast_to_room, - static_content::{possession_type::possession_data, room::Direction}, + static_content::room::Direction, DResult, }; use async_trait::async_trait; @@ -129,11 +129,8 @@ pub async fn attempt_open_immediate( .await? .first() { - if let Some(lockcheck) = lock - .possession_type - .as_ref() - .and_then(|pt| possession_data().get(pt)) - .and_then(|pd| pd.lockcheck_handler) + if let Some(lockcheck) = + lock.static_data().and_then(|pd| pd.lockcheck_handler) { lockcheck.cmd(ctx, &lock).await? } @@ -249,11 +246,8 @@ impl QueueCommandHandler for QueueHandler { .await? .first() { - if let Some(lockcheck) = lock - .possession_type - .as_ref() - .and_then(|pt| possession_data().get(pt)) - .and_then(|pd| pd.lockcheck_handler) + if let Some(lockcheck) = + lock.static_data().and_then(|pd| pd.lockcheck_handler) { lockcheck.cmd(ctx, &lock).await? } diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 7959503..568c5b8 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -309,6 +309,7 @@ pub enum ItemFlag { Instructions, HasUrges, NoUrgesHere, + DontListInLook, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] @@ -391,6 +392,9 @@ pub enum ItemSpecialData { HireData { hired_by: Option, }, + HanoiPuzzle { + towers: [Vec; 3], + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] @@ -455,6 +459,7 @@ pub struct Item { pub display_less_explicit: Option, pub details: Option, pub details_less_explicit: Option, + pub details_dyn_suffix: Option, pub aliases: Vec, pub location: String, // Item reference as item_type/item_code. pub action_type: LocationActionType, @@ -577,6 +582,7 @@ impl Default for Item { display_less_explicit: None, details: None, details_less_explicit: None, + details_dyn_suffix: None, aliases: vec![], location: "room/storage".to_owned(), action_type: LocationActionType::Normal, diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index d69463b..9585d0a 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -54,6 +54,7 @@ pub enum TaskDetails { }, TickUrges, ResetSpawns, + ResetHanoi, } impl TaskDetails { pub fn name(self: &Self) -> &'static str { @@ -74,6 +75,7 @@ impl TaskDetails { ChargeWages { .. } => "ChargeWages", TickUrges => "TickUrges", ResetSpawns => "ResetSpawns", + ResetHanoi => "ResetHanoi", // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index bae3033..8826cf9 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -1,14 +1,16 @@ +#[cfg(not(test))] +use crate::db::is_concurrency_error; #[double] use crate::db::DBTrans; #[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence}; use crate::{ - db::{self, is_concurrency_error}, + db, listener::{ListenerMap, ListenerSend}, message_handler::user_commands::{delete, drop, hire, open, rent}, models::task::Task, services::{combat, effect, spawn, urges}, - static_content::npc, + static_content::npc::{self, computer_museum_npcs}, DResult, }; use async_trait::async_trait; @@ -57,6 +59,10 @@ fn task_handler_registry( ("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()), ("TickUrges", urges::TICK_URGES_HANDLER.clone()), ("ResetSpawns", spawn::RESET_SPAWNS_HANDLER.clone()), + ( + "ResetHanoi", + computer_museum_npcs::RESET_GAME_HANDLER.clone(), + ), ] .into_iter() .collect() diff --git a/blastmud_game/src/static_content.rs b/blastmud_game/src/static_content.rs index 7ed52e8..8f1281d 100644 --- a/blastmud_game/src/static_content.rs +++ b/blastmud_game/src/static_content.rs @@ -5,6 +5,7 @@ use crate::{ DResult, }; use log::info; +use room::refresh_room_exits; use std::collections::{BTreeMap, BTreeSet}; pub mod dynzone; @@ -75,6 +76,7 @@ fn static_task_registry() -> Vec> { ] } +#[cfg(not(test))] async fn refresh_static_items(pool: &DBPool) -> DResult<()> { let registry = static_item_registry(); @@ -112,11 +114,11 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> { } } 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?; + let initial = &(expected_items.get(existing_item_code).unwrap().initial_item)(); + tx.limited_update_static_item(&initial).await?; + if initial.item_type == "room" && initial.door_states.is_some() { + refresh_room_exits(&tx, &initial).await?; + } } tx.commit().await?; info!( @@ -127,6 +129,7 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> { Ok(()) } +#[cfg(not(test))] async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> { let registry = static_task_registry(); @@ -174,6 +177,7 @@ async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> { Ok(()) } +#[cfg(not(test))] pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> { refresh_static_items(pool).await?; refresh_static_tasks(pool).await?; diff --git a/blastmud_game/src/static_content/fixed_item.rs b/blastmud_game/src/static_content/fixed_item.rs index 2487f1c..48af7c2 100644 --- a/blastmud_game/src/static_content/fixed_item.rs +++ b/blastmud_game/src/static_content/fixed_item.rs @@ -1,8 +1,8 @@ // For things like signs that don't do much except stay where they are and carry a description. use super::{possession_type::PossessionData, StaticItem}; use crate::{ - models::item::{Item, LiquidType, Pronouns}, - static_content::possession_type::LiquidContainerData, + models::item::{Item, LiquidType, LocationActionType, Pronouns}, + static_content::{possession_type::LiquidContainerData, room::computer_museum}, }; use ansi::ansi; use once_cell::sync::OnceCell; @@ -16,6 +16,22 @@ pub struct FixedItem { pub location: &'static str, pub proper_noun: bool, pub aliases: Vec<&'static str>, + pub action_type: LocationActionType, +} + +impl Default for FixedItem { + fn default() -> Self { + Self { + code: "default", + name: "default", + description: "A thingy", + description_less_explicit: None, + location: "unset", + proper_noun: true, + aliases: vec![], + action_type: LocationActionType::Normal, + } + } } fn fixed_item_list() -> &'static Vec { @@ -41,6 +57,7 @@ fn fixed_item_list() -> &'static Vec { location: "room/repro_xv_updates", proper_noun: false, aliases: vec!["poster"], + ..Default::default() }, FixedItem { code: "melbs_king_st_spring_fed_fountain", @@ -56,8 +73,9 @@ fn fixed_item_list() -> &'static Vec { location: "room/melbs_kingst_40", proper_noun: false, aliases: vec!["fountain"], + ..Default::default() }, - ] + ].into_iter().chain(computer_museum::fixed_items().into_iter()).collect() }) } @@ -76,6 +94,7 @@ pub fn fixed_item_properties() -> &'static BTreeMap<&'static str, PossessionData }, )] .into_iter() + .chain(computer_museum::fixed_item_properties().into_iter()) .collect() }) } @@ -96,6 +115,7 @@ pub fn static_items() -> Box> { is_proper: r.proper_noun, ..Pronouns::default_inanimate() }, + action_type: r.action_type.clone(), ..Item::default() }), extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())), diff --git a/blastmud_game/src/static_content/npc/computer_museum_npcs.rs b/blastmud_game/src/static_content/npc/computer_museum_npcs.rs index 4fcdd11..a27086d 100644 --- a/blastmud_game/src/static_content/npc/computer_museum_npcs.rs +++ b/blastmud_game/src/static_content/npc/computer_museum_npcs.rs @@ -1,11 +1,342 @@ -use super::{NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC}; +use super::{NPCMessageHandler, NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC}; +#[double] +use crate::db::DBTrans; use crate::{ + message_handler::user_commands::{UResult, VerbContext}, models::{ consent::ConsentType, - item::{LocationActionType, Pronouns, SkillType}, + item::{Item, ItemFlag, ItemSpecialData, LocationActionType, Pronouns, SkillType}, + task::{Task, TaskDetails, TaskMeta}, }, - static_content::{npc::KillBonus, possession_type::PossessionType, species::SpeciesType}, + regular_tasks::{TaskHandler, TaskRunContext}, + services::comms::broadcast_to_room, + static_content::{ + npc::KillBonus, possession_type::PossessionType, room::Direction, species::SpeciesType, + }, + DResult, }; +use ansi::ansi; +use async_trait::async_trait; +use chrono::Utc; +use itertools::Itertools; +use mockall_double::double; +use nom::{ + bytes::complete::tag, + character::complete::{multispace1, u8}, + combinator::eof, + sequence::{delimited, pair, preceded, terminated}, +}; +use std::time; + +async fn reply(ctx: &VerbContext<'_>, msg: &str) -> DResult<()> { + ctx.trans + .queue_for_session( + &ctx.session, + Some(&format!( + ansi!("Doorbot beeps, then replies in low-fidelity audio: {}\n"), + msg + )), + ) + .await +} + +fn parse_move_message(mut input: &str) -> Result<(u8, u8), &str> { + input = input.trim(); + let (from, to) = match terminated( + preceded( + preceded(tag("move"), multispace1::<&str, ()>), + pair( + u8, + preceded(delimited(multispace1, tag("to"), multispace1), u8), + ), + ), + eof, + )(input) + { + Ok((_, v)) => Ok(v), + Err(_) => Err( + "Invalid command, feeble human. I only understand commands like: -doorbot move 1 to 2", + ), + }?; + if from == 0 || to == 0 { + return Err("I like starting numbers at 0 too, but, alas, my programmer was a feeble human and numbered the first tower 1!"); + } + if from > 3 || to > 3 { + return Err( + "Feeble human, this will make it hard for your simple brain, but I have but 3 towers!", + ); + } + if from == to { + return Err( + "Only a feeble human would try to move a disc from a tower... to the same tower!", + ); + } + Ok((from, to)) +} + +type TowerData = [Vec; 3]; +fn move_between_towers(from: u8, to: u8, initial: &TowerData) -> Result { + let mut new_dat = initial.clone(); + let move_disc = new_dat[(from - 1) as usize] + .pop() + .ok_or_else(|| "Feeble human, you can't move a disc from a tower with no discs.")?; + match new_dat[(to - 1) as usize].last() { + Some(onto_disc) if onto_disc > &move_disc => { + Err("Feeble human, the rules of the game say you must never place a larger disc on top of a smaller one.")?; + } + _ => {} + } + new_dat[(to - 1) as usize].push(move_disc); + return Ok(new_dat); +} + +fn describe_towers(data: &TowerData) -> String { + let mut description = String::new(); + for (discs, word) in data.iter().zip(["first", "second", "third"]) { + description.push_str(&format!("The {} tower ", word)); + if discs.len() == 0 { + description.push_str("is empty. "); + } else { + description.push_str(&format!( + "holds {}. ", + discs + .iter() + .rev() + .map(|d| match d { + 4 => "a very small red disc", + 3 => "a small orange disc", + 2 => "a medium-sized yellow disc", + _ => "a large green disc", + }) + .join(", atop ") + )); + } + } + description + "A cursor blinks as if waiting for further input" +} + +fn is_solved(data: &TowerData) -> bool { + data[2].len() == 4 +} + +fn initial_towers() -> TowerData { + [vec![1, 2, 3, 4], vec![], vec![]] +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn parse_move_message_accepts_valid() { + assert_eq!(parse_move_message(" move 1 to 2"), Ok((1, 2))); + } + + #[test] + fn parse_move_message_rejects_badstart() { + assert_eq!(parse_move_message("eat 1 to 2"), Err("Invalid command, feeble human. I only understand commands like: -doorbot move 1 to 2")); + } + + #[test] + fn parse_move_message_rejects_negative() { + assert_eq!(parse_move_message("move -1 to 2"), Err("Invalid command, feeble human. I only understand commands like: -doorbot move 1 to 2")); + } + + #[test] + fn parse_move_message_rejects_zero() { + assert_eq!(parse_move_message("move 1 to 0"), Err("I like starting numbers at 0 too, but, alas, my programmer was a feeble human and numbered the first tower 1!")); + } + + #[test] + fn parse_move_message_rejects_four() { + assert_eq!(parse_move_message("move 4 to 1"), Err("Feeble human, this will make it hard for your simple brain, but I have but 3 towers!")); + } + + #[test] + fn parse_move_message_rejects_self() { + assert_eq!( + parse_move_message("move 2 to 2"), + Err("Only a feeble human would try to move a disc from a tower... to the same tower!") + ); + } + + #[test] + fn move_between_towers_works_to_empty_tower() { + assert_eq!( + move_between_towers(1, 2, &[vec![2, 3, 4], vec![], vec![1]]), + Ok([vec![2, 3], vec![4], vec![1]]) + ) + } + + #[test] + fn move_between_towers_works_to_nonempty_tower() { + assert_eq!( + move_between_towers(1, 3, &[vec![4], vec![], vec![1, 2, 3]]), + Ok([vec![], vec![], vec![1, 2, 3, 4]]) + ) + } + + #[test] + fn move_between_towers_fails_from_empty_tower() { + assert_eq!( + move_between_towers(2, 3, &initial_towers()), + Err("Feeble human, you can't move a disc from a tower with no discs.") + ) + } + + #[test] + fn describe_towers_works() { + assert_eq!( + describe_towers(&[vec![1,2,3,4], vec![], vec![]]), + "The first tower holds a very small red disc, atop a small orange disc, atop a medium-sized yellow disc, atop a large green disc. \ + The second tower is empty. \ + The third tower is empty. A cursor blinks as if waiting for further input" + ) + } + + #[test] + fn is_solved_returns_true_when_solved() { + assert_eq!(is_solved(&[vec![], vec![], vec![1, 2, 3, 4]]), true) + } + #[test] + fn is_solved_returns_false_when_not_solved() { + assert_eq!(is_solved(&[vec![], vec![4], vec![1, 2, 3]]), false) + } +} + +struct DoorbotMsgHandler; +#[async_trait] +impl NPCMessageHandler for DoorbotMsgHandler { + async fn handle( + self: &Self, + ctx: &mut VerbContext, + _source: &Item, + target: &Item, + mut message: &str, + ) -> UResult<()> { + message = message.trim(); + let (from, to) = match parse_move_message(message) { + Err(e) => { + reply(ctx, e).await?; + return Ok(()); + } + Ok((from, to)) => (from, to), + }; + + let initial_towerdata = match target.special_data { + Some(ItemSpecialData::HanoiPuzzle { towers: ref v }) => v.clone(), + _ => initial_towers(), + }; + + let new_towerdata = match move_between_towers(from, to, &initial_towerdata) { + Err(e) => { + reply(ctx, e).await?; + return Ok(()); + } + Ok(d) => d, + }; + + if is_solved(&new_towerdata) { + reset_puzzle(&ctx.trans, true).await?; + let msg = ansi!("Doorbot moves a disc to the third tower, completing the puzzle. \ + Doorbot beeps and says: \"I underestimated you, mighty human. \ + You have proven your worth to pass through the door to the hacker's \ + club. I will open the door for you - when it closes it will be locked \ + again, ready for the next candidate for the club - so I suggest you enter \ + before it closes.\". As if by magic, the puzzle resets back to \ + its starting state, and the door swings open with a creak.\n"); + broadcast_to_room(&ctx.trans, &target.location, None, msg, Some(msg)).await?; + ctx.trans + .upsert_task(&Task { + meta: TaskMeta { + task_code: "room/computer_museum_hackers_club/south".to_owned(), + next_scheduled: Utc::now() + chrono::Duration::seconds(120), + ..Default::default() + }, + details: TaskDetails::SwingShut { + room_item: "room/computer_museum_hackers_club".to_owned(), + direction: Direction::SOUTH, + }, + }) + .await?; + } else { + let mut room_item = (*ctx + .trans + .find_item_by_type_code("room", "computer_museum_club_door") + .await? + .ok_or_else(|| "Missing room computer_musuem_club_door")?) + .clone(); + let descr = describe_towers(&new_towerdata); + let msg = format!( + "Doorbot moves a disc to tower {}. The screen now shows: {}.\n", + to, &descr + ); + broadcast_to_room(&ctx.trans, &target.location, None, &msg, Some(&msg)).await?; + room_item.details_dyn_suffix = Some(descr); + ctx.trans.save_item_model(&room_item).await?; + + ctx.trans + .upsert_task(&Task { + meta: TaskMeta { + task_code: "reset_hanoi_computer_club".to_owned(), + next_scheduled: Utc::now() + chrono::Duration::seconds(600), + ..Default::default() + }, + details: TaskDetails::ResetHanoi, + }) + .await?; + let mut target_mut = (*target).clone(); + target_mut.special_data = Some(ItemSpecialData::HanoiPuzzle { + towers: new_towerdata, + }); + ctx.trans.save_item_model(&target_mut).await?; + } + + Ok(()) + } +} + +async fn reset_puzzle(trans: &DBTrans, door_state: bool) -> DResult<()> { + let mut npc_item = (*trans + .find_item_by_type_code("npc", "computer_museum_doorbot") + .await? + .ok_or_else(|| "Missing npc computer_musuem_doorbot")?) + .clone(); + npc_item.special_data = Some(ItemSpecialData::HanoiPuzzle { + towers: initial_towers(), + }); + trans.save_item_model(&npc_item).await?; + let mut doorwell_room_item = (*trans + .find_item_by_type_code("room", "computer_museum_club_door") + .await? + .ok_or_else(|| "Missing room computer_musuem_club_door")?) + .clone(); + let mut club_room_item = (*trans + .find_item_by_type_code("room", "computer_museum_hackers_club") + .await? + .ok_or_else(|| "Missing room computer_musuem_club_door")?) + .clone(); + match club_room_item.door_states { + None => {} + Some(ref mut d) => match d.get_mut(&Direction::SOUTH) { + None => {} + Some(d) => d.open = door_state, + }, + } + doorwell_room_item.details_dyn_suffix = Some(describe_towers(&initial_towers())); + trans.save_item_model(&doorwell_room_item).await?; + trans.save_item_model(&club_room_item).await?; + Ok(()) +} + +pub struct ResetGameTaskHandler; +#[async_trait] +impl TaskHandler for ResetGameTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + reset_puzzle(&ctx.trans, false).await?; + Ok(None) + } +} +pub static RESET_GAME_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ResetGameTaskHandler; pub fn npc_list() -> Vec { use NPCSayType::FromFixedList; @@ -115,5 +446,21 @@ pub fn npc_list() -> Vec { killbot!("4", "berserk", "hw_2"), killbot!("5", "vicious", "hw_3"), killbot!("6", "murderous", "club_door"), + NPC { + code: "computer_museum_doorbot", + name: "Doorbot", + spawn_location: "room/computer_museum_club_door", + description: ansi!( + "A flat panel screen on the door, with a microphone listening for \ + commands directed at it, apparently hooked up to the door lock. \ + [Hint: try -doorbot move from 1 to 2 to ask Doorbot \ + to move the top disc from tower 1 to tower 2]" + ), + pronouns: Pronouns::default_inanimate(), + species: SpeciesType::Robot, + extra_flags: vec![ItemFlag::DontListInLook], + message_handler: Some(&DoorbotMsgHandler), + ..Default::default() + }, ] } diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index e4387c5..ba615a5 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -24,7 +24,7 @@ mod corp_licence; mod fangs; mod food; pub mod head_armour; -mod lock; +pub mod lock; pub mod lower_armour; mod meat; pub mod torso_armour; @@ -36,6 +36,7 @@ pub type AttackMessageChoice = pub type AttackMessageChoicePart = Vec String + 'static + Sync + Send>>; +#[derive(Clone)] pub struct SkillScaling { pub skill: SkillType, pub min_skill: f64, @@ -141,6 +142,7 @@ impl Default for WeaponData { } } +#[derive(Clone)] pub struct ChargeData { pub max_charges: u8, pub charge_name_prefix: &'static str, @@ -269,6 +271,7 @@ pub trait BenchData { async fn check_make(&self, trans: &DBTrans, bench: &Item, recipe: &Item) -> UResult<()>; } +#[derive(Clone)] pub struct ContainerData { pub max_weight: u64, pub base_weight: u64, @@ -289,6 +292,7 @@ impl Default for ContainerData { } } +#[derive(Clone)] pub struct LiquidContainerData { pub capacity: u64, // in mL pub allowed_contents: Option>, // None means anything. @@ -302,11 +306,13 @@ impl Default for LiquidContainerData { } } +#[derive(Clone)] pub struct EatData { pub hunger_impact: i16, pub thirst_impact: i16, } +#[derive(Clone)] pub struct SitData { pub stress_impact: i16, } @@ -427,6 +433,7 @@ pub enum PossessionType { // Fluid containers DrinkBottle, // Security + Basiclock, Scanlock, // Food GrilledSteak, diff --git a/blastmud_game/src/static_content/possession_type/lock.rs b/blastmud_game/src/static_content/possession_type/lock.rs index 8a6b0a0..807182e 100644 --- a/blastmud_game/src/static_content/possession_type/lock.rs +++ b/blastmud_game/src/static_content/possession_type/lock.rs @@ -12,6 +12,17 @@ use crate::{ use async_trait::async_trait; use once_cell::sync::OnceCell; +struct BasicLockLockcheck; +#[async_trait] +impl LockcheckHandler for BasicLockLockcheck { + async fn cmd(&self, _ctx: &mut QueuedCommandContext, _what: &Item) -> UResult<()> { + // TODO implement actual check for key once we have them. + user_error( + "You search your possessions for a key that fits the door, but end up just rattling the handle uselessly.".to_owned())?; + Ok(()) + } +} + struct ScanLockLockcheck; #[async_trait] impl LockcheckHandler for ScanLockLockcheck { @@ -145,6 +156,17 @@ impl InstallHandler for ScanLockInstall { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( + (PossessionType::Basiclock, + PossessionData { + display: "basic lock", + details: "A basic lock that looks like it needs a key to open.", + aliases: vec!("lock"), + weight: LOCK_WEIGHT, + lockcheck_handler: Some(&BasicLockLockcheck), + install_handler: None, + ..Default::default() + } + ), (PossessionType::Scanlock, PossessionData { display: "scanlock", diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index 41957a3..5037b8d 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -1,17 +1,21 @@ use super::{possession_type::PossessionType, StaticItem}; +#[double] +use crate::db::DBTrans; use crate::{ message_handler::user_commands::UResult, - models::item::{Item, ItemFlag}, + models::item::{DoorState, Item, ItemFlag}, regular_tasks::queued_command::QueuedCommandContext, + DResult, }; use async_trait::async_trait; +use mockall_double::double; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; mod chonkers; mod cok_murl; -mod computer_museum; +pub mod computer_museum; mod melbs; mod repro_xv; mod special; @@ -330,6 +334,7 @@ pub struct Room { pub rentable_dynzone: Vec, pub material_type: MaterialType, pub has_power: bool, + pub door_states: Option>, } impl Default for Room { @@ -351,6 +356,7 @@ impl Default for Room { rentable_dynzone: vec![], material_type: MaterialType::Normal, has_power: false, + door_states: None, } } } @@ -403,6 +409,7 @@ pub fn room_static_items() -> Box> { location: format!("zone/{}", r.zone), is_static: true, flags: r.item_flags.clone(), + door_states: r.door_states.clone(), ..Item::default() }), extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())), @@ -424,6 +431,58 @@ pub fn resolve_exit(room: &Room, exit: &Exit) -> Option<&'static Room> { } } +pub async fn refresh_room_exits(trans: &DBTrans, template: &Item) -> DResult<()> { + let mut target_mut = match trans + .find_item_by_type_code(&template.item_type, &template.item_code) + .await? + { + None => return Ok(()), + Some(v) => (*v).clone(), + }; + let mut need_save: bool = false; + if let None = target_mut.door_states.as_ref() { + target_mut.door_states = Some(BTreeMap::new()); + } + if let Some(target_states) = target_mut.door_states.as_mut() { + if let Some(template_states) = template.door_states.as_ref() { + let target_keys = target_states + .keys() + .map(|v| v.clone()) + .collect::>(); + let template_keys = template_states + .keys() + .map(|v| v.clone()) + .collect::>(); + for extra in &target_keys - &template_keys { + target_states.remove(&extra); + need_save = true; + } + for missing in &template_keys - &target_keys { + if let Some(template) = template_states.get(&missing) { + target_states.insert(missing, template.clone()); + need_save = true; + } + } + for same in template_keys.intersection(&target_keys) { + if let Some(target) = target_states.get_mut(&same) { + if let Some(template) = template_states.get(&same) { + if &template.description != &target.description { + // We do this whole thing so we preserve the open state, + // but change the description. + target.description = template.description.clone(); + need_save = true; + } + } + } + } + } + } + if need_save { + trans.save_item_model(&target_mut).await?; + } + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/blastmud_game/src/static_content/room/computer_museum.rs b/blastmud_game/src/static_content/room/computer_museum.rs index ce8b0ce..504dacf 100644 --- a/blastmud_game/src/static_content/room/computer_museum.rs +++ b/blastmud_game/src/static_content/room/computer_museum.rs @@ -1,3 +1,11 @@ +use crate::{ + models::item::{DoorState, LocationActionType}, + static_content::{ + fixed_item::FixedItem, + possession_type::{possession_data, PossessionData, PossessionType}, + }, +}; + use super::{Direction, Exit, ExitTarget, GridCoords, Room, SecondaryZoneRecord}; use ansi::ansi; pub fn room_list() -> Vec { @@ -128,14 +136,13 @@ pub fn room_list() -> Vec { name: "Doorwell", short: ansi!("/\\"), description: ansi!("This appears to be a security zone protecting access to a door to the north. A long corridor stretches to the west. The corridor echoes with screams of torment and the sound of metal cutting into flesh.\n\ - Mounted on the door is a large screen, showing primative ASCII art of some towers, \ + Mounted on the door is a large screen, labelled as \"Doorbot\", showing \ + primative ASCII art of some towers, \ with with some kind of electronic screen on it, showing some kind of towers with \ - coloured disks stacked on them.\n\ - The disks are arranged as follows:\n\ - Tower 1: A small green disk, atop a medium sized yellow disk, \ - atop a large white disk, atop an extra large red disk.\n\ - Tower 2: Empty\n\ - Tower 3: Empty"), + coloured discs stacked on them.\n\ + [Hint: try -doorbot move from 1 to 2 to ask Doorbot \ + to move the top disc from tower 1 to tower 2]\n\ + The discs are arranged as follows:\n"), description_less_explicit: None, grid_coords: GridCoords { x: 4, y: 0, z: -1 }, exits: vec!( @@ -166,8 +173,42 @@ pub fn room_list() -> Vec { ..Default::default() }, ), + door_states: Some(vec![ + ( + Direction::SOUTH, + DoorState { + open: false, + description: "a very heavy-duty looking solid steel door, with black and yellow warning stripes painted on the surface, and an LCD-style screen afixed to the middle a bit over a metre up".to_owned() + } + ) + ].into_iter().collect()), should_caption: true, ..Default::default() }, ) } + +pub fn fixed_items() -> Vec { + vec![FixedItem { + code: "computer_museum_club_door_lock", + name: ansi!("basic keyed lock"), + description: ansi!("A basic lock that looks like it needs a key to open"), + description_less_explicit: None, + location: "room/computer_museum_hackers_club", + proper_noun: false, + aliases: vec!["lock"], + action_type: LocationActionType::InstalledOnDoorAsLock(Direction::SOUTH), + ..Default::default() + }] +} + +pub fn fixed_item_properties() -> Vec<(&'static str, PossessionData)> { + let lock_tmpl = possession_data().get(&PossessionType::Basiclock).unwrap(); + vec![( + "computer_museum_club_door_lock", + PossessionData { + lockcheck_handler: lock_tmpl.lockcheck_handler, + ..Default::default() + }, + )] +}