Implement computer museum puzzle.

This commit is contained in:
Condorra 2023-09-20 23:56:28 +10:00
parent 4467707d4a
commit 6dc4a870fc
15 changed files with 559 additions and 38 deletions

View File

@ -21,6 +21,7 @@ use std::collections::BTreeSet;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
#[cfg(not(test))]
use tokio_postgres::error::{DbError, SqlState}; use tokio_postgres::error::{DbError, SqlState};
use tokio_postgres::types::ToSql; use tokio_postgres::types::ToSql;
use tokio_postgres::NoTls; 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 { pub fn is_concurrency_error(e: &(dyn Error + 'static)) -> bool {
match e.source() { match e.source() {
None => {} None => {}

View File

@ -1,3 +1,4 @@
#![cfg_attr(test, allow(unused))]
use db::DBPool; use db::DBPool;
use log::{error, info, LevelFilter}; use log::{error, info, LevelFilter};
use serde::Deserialize; use serde::Deserialize;
@ -32,6 +33,7 @@ fn read_latest_config() -> DResult<Config> {
} }
#[tokio::main(worker_threads = 2)] #[tokio::main(worker_threads = 2)]
#[cfg(not(test))]
async fn main() -> DResult<()> { async fn main() -> DResult<()> {
SimpleLogger::new() SimpleLogger::new()
.with_level(LevelFilter::Info) .with_level(LevelFilter::Info)

View File

@ -1,7 +1,9 @@
use super::ListenerSession; use super::ListenerSession;
#[cfg(not(test))]
use crate::db::is_concurrency_error;
#[double] #[double]
use crate::db::DBTrans; 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::user::UserFlag;
use crate::models::{item::Item, session::Session, user::User}; use crate::models::{item::Item, session::Session, user::User};
use crate::DResult; use crate::DResult;

View File

@ -419,10 +419,14 @@ pub async fn describe_room(
ansi!("<reset> "), ansi!("<reset> "),
&word_wrap( &word_wrap(
&format!( &format!(
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"), ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat), item.display_for_session(&ctx.session_dat),
zone, zone,
item.details_for_session(&ctx.session_dat).unwrap_or(""), item.details_for_session(&ctx.session_dat).unwrap_or(""),
item.details_dyn_suffix
.as_ref()
.map(|d| d.as_str())
.unwrap_or(""),
contents, contents,
exits_for(room) exits_for(room)
), ),
@ -504,7 +508,9 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
let all_groups: Vec<Vec<&Arc<Item>>> = items let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter() .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)) .group_by(|i| i.display_for_sentence(true, 1, false))
.into_iter() .into_iter()
.map(|(_, g)| g.collect::<Vec<&Arc<Item>>>()) .map(|(_, g)| g.collect::<Vec<&Arc<Item>>>())

View File

@ -16,7 +16,7 @@ use crate::{
TaskHandler, TaskRunContext, TaskHandler, TaskRunContext,
}, },
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
static_content::{possession_type::possession_data, room::Direction}, static_content::room::Direction,
DResult, DResult,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@ -129,11 +129,8 @@ pub async fn attempt_open_immediate(
.await? .await?
.first() .first()
{ {
if let Some(lockcheck) = lock if let Some(lockcheck) =
.possession_type lock.static_data().and_then(|pd| pd.lockcheck_handler)
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler)
{ {
lockcheck.cmd(ctx, &lock).await? lockcheck.cmd(ctx, &lock).await?
} }
@ -249,11 +246,8 @@ impl QueueCommandHandler for QueueHandler {
.await? .await?
.first() .first()
{ {
if let Some(lockcheck) = lock if let Some(lockcheck) =
.possession_type lock.static_data().and_then(|pd| pd.lockcheck_handler)
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler)
{ {
lockcheck.cmd(ctx, &lock).await? lockcheck.cmd(ctx, &lock).await?
} }

View File

@ -309,6 +309,7 @@ pub enum ItemFlag {
Instructions, Instructions,
HasUrges, HasUrges,
NoUrgesHere, NoUrgesHere,
DontListInLook,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -391,6 +392,9 @@ pub enum ItemSpecialData {
HireData { HireData {
hired_by: Option<String>, hired_by: Option<String>,
}, },
HanoiPuzzle {
towers: [Vec<u8>; 3],
},
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
@ -455,6 +459,7 @@ pub struct Item {
pub display_less_explicit: Option<String>, pub display_less_explicit: Option<String>,
pub details: Option<String>, pub details: Option<String>,
pub details_less_explicit: Option<String>, pub details_less_explicit: Option<String>,
pub details_dyn_suffix: Option<String>,
pub aliases: Vec<String>, pub aliases: Vec<String>,
pub location: String, // Item reference as item_type/item_code. pub location: String, // Item reference as item_type/item_code.
pub action_type: LocationActionType, pub action_type: LocationActionType,
@ -577,6 +582,7 @@ impl Default for Item {
display_less_explicit: None, display_less_explicit: None,
details: None, details: None,
details_less_explicit: None, details_less_explicit: None,
details_dyn_suffix: None,
aliases: vec![], aliases: vec![],
location: "room/storage".to_owned(), location: "room/storage".to_owned(),
action_type: LocationActionType::Normal, action_type: LocationActionType::Normal,

View File

@ -54,6 +54,7 @@ pub enum TaskDetails {
}, },
TickUrges, TickUrges,
ResetSpawns, ResetSpawns,
ResetHanoi,
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -74,6 +75,7 @@ impl TaskDetails {
ChargeWages { .. } => "ChargeWages", ChargeWages { .. } => "ChargeWages",
TickUrges => "TickUrges", TickUrges => "TickUrges",
ResetSpawns => "ResetSpawns", ResetSpawns => "ResetSpawns",
ResetHanoi => "ResetHanoi",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
} }
} }

View File

@ -1,14 +1,16 @@
#[cfg(not(test))]
use crate::db::is_concurrency_error;
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
#[cfg(not(test))] #[cfg(not(test))]
use crate::models::task::{TaskParse, TaskRecurrence}; use crate::models::task::{TaskParse, TaskRecurrence};
use crate::{ use crate::{
db::{self, is_concurrency_error}, db,
listener::{ListenerMap, ListenerSend}, listener::{ListenerMap, ListenerSend},
message_handler::user_commands::{delete, drop, hire, open, rent}, message_handler::user_commands::{delete, drop, hire, open, rent},
models::task::Task, models::task::Task,
services::{combat, effect, spawn, urges}, services::{combat, effect, spawn, urges},
static_content::npc, static_content::npc::{self, computer_museum_npcs},
DResult, DResult,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@ -57,6 +59,10 @@ fn task_handler_registry(
("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()), ("ChargeWages", hire::CHARGE_WAGES_HANDLER.clone()),
("TickUrges", urges::TICK_URGES_HANDLER.clone()), ("TickUrges", urges::TICK_URGES_HANDLER.clone()),
("ResetSpawns", spawn::RESET_SPAWNS_HANDLER.clone()), ("ResetSpawns", spawn::RESET_SPAWNS_HANDLER.clone()),
(
"ResetHanoi",
computer_museum_npcs::RESET_GAME_HANDLER.clone(),
),
] ]
.into_iter() .into_iter()
.collect() .collect()

View File

@ -5,6 +5,7 @@ use crate::{
DResult, DResult,
}; };
use log::info; use log::info;
use room::refresh_room_exits;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
pub mod dynzone; pub mod dynzone;
@ -75,6 +76,7 @@ fn static_task_registry() -> Vec<StaticThingTypeGroup<StaticTask>> {
] ]
} }
#[cfg(not(test))]
async fn refresh_static_items(pool: &DBPool) -> DResult<()> { async fn refresh_static_items(pool: &DBPool) -> DResult<()> {
let registry = static_item_registry(); 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) { for existing_item_code in expected_set.intersection(&existing_items) {
tx.limited_update_static_item(&(expected_items let initial = &(expected_items.get(existing_item_code).unwrap().initial_item)();
.get(existing_item_code) tx.limited_update_static_item(&initial).await?;
.unwrap() if initial.item_type == "room" && initial.door_states.is_some() {
.initial_item)()) refresh_room_exits(&tx, &initial).await?;
.await?; }
} }
tx.commit().await?; tx.commit().await?;
info!( info!(
@ -127,6 +129,7 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> {
Ok(()) Ok(())
} }
#[cfg(not(test))]
async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> { async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> {
let registry = static_task_registry(); let registry = static_task_registry();
@ -174,6 +177,7 @@ async fn refresh_static_tasks(pool: &DBPool) -> DResult<()> {
Ok(()) Ok(())
} }
#[cfg(not(test))]
pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> { pub async fn refresh_static_content(pool: &DBPool) -> DResult<()> {
refresh_static_items(pool).await?; refresh_static_items(pool).await?;
refresh_static_tasks(pool).await?; refresh_static_tasks(pool).await?;

View File

@ -1,8 +1,8 @@
// For things like signs that don't do much except stay where they are and carry a description. // For things like signs that don't do much except stay where they are and carry a description.
use super::{possession_type::PossessionData, StaticItem}; use super::{possession_type::PossessionData, StaticItem};
use crate::{ use crate::{
models::item::{Item, LiquidType, Pronouns}, models::item::{Item, LiquidType, LocationActionType, Pronouns},
static_content::possession_type::LiquidContainerData, static_content::{possession_type::LiquidContainerData, room::computer_museum},
}; };
use ansi::ansi; use ansi::ansi;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -16,6 +16,22 @@ pub struct FixedItem {
pub location: &'static str, pub location: &'static str,
pub proper_noun: bool, pub proper_noun: bool,
pub aliases: Vec<&'static str>, pub aliases: Vec<&'static str>,
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<FixedItem> { fn fixed_item_list() -> &'static Vec<FixedItem> {
@ -41,6 +57,7 @@ fn fixed_item_list() -> &'static Vec<FixedItem> {
location: "room/repro_xv_updates", location: "room/repro_xv_updates",
proper_noun: false, proper_noun: false,
aliases: vec!["poster"], aliases: vec!["poster"],
..Default::default()
}, },
FixedItem { FixedItem {
code: "melbs_king_st_spring_fed_fountain", code: "melbs_king_st_spring_fed_fountain",
@ -56,8 +73,9 @@ fn fixed_item_list() -> &'static Vec<FixedItem> {
location: "room/melbs_kingst_40", location: "room/melbs_kingst_40",
proper_noun: false, proper_noun: false,
aliases: vec!["fountain"], 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() .into_iter()
.chain(computer_museum::fixed_item_properties().into_iter())
.collect() .collect()
}) })
} }
@ -96,6 +115,7 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
is_proper: r.proper_noun, is_proper: r.proper_noun,
..Pronouns::default_inanimate() ..Pronouns::default_inanimate()
}, },
action_type: r.action_type.clone(),
..Item::default() ..Item::default()
}), }),
extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())), extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())),

View File

@ -1,11 +1,342 @@
use super::{NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC}; use super::{NPCMessageHandler, NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{UResult, VerbContext},
models::{ models::{
consent::ConsentType, 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: <yellow>{}<reset>\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<u8>; 3];
fn move_between_towers(from: u8, to: u8, initial: &TowerData) -> Result<TowerData, &'static str> {
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: <blue>\"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.\"<reset>. 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<Option<time::Duration>> {
reset_puzzle(&ctx.trans, false).await?;
Ok(None)
}
}
pub static RESET_GAME_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ResetGameTaskHandler;
pub fn npc_list() -> Vec<NPC> { pub fn npc_list() -> Vec<NPC> {
use NPCSayType::FromFixedList; use NPCSayType::FromFixedList;
@ -115,5 +446,21 @@ pub fn npc_list() -> Vec<NPC> {
killbot!("4", "berserk", "hw_2"), killbot!("4", "berserk", "hw_2"),
killbot!("5", "vicious", "hw_3"), killbot!("5", "vicious", "hw_3"),
killbot!("6", "murderous", "club_door"), 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 <bold>-doorbot move from 1 to 2<reset> 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()
},
] ]
} }

View File

@ -24,7 +24,7 @@ mod corp_licence;
mod fangs; mod fangs;
mod food; mod food;
pub mod head_armour; pub mod head_armour;
mod lock; pub mod lock;
pub mod lower_armour; pub mod lower_armour;
mod meat; mod meat;
pub mod torso_armour; pub mod torso_armour;
@ -36,6 +36,7 @@ pub type AttackMessageChoice =
pub type AttackMessageChoicePart = pub type AttackMessageChoicePart =
Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>; Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
#[derive(Clone)]
pub struct SkillScaling { pub struct SkillScaling {
pub skill: SkillType, pub skill: SkillType,
pub min_skill: f64, pub min_skill: f64,
@ -141,6 +142,7 @@ impl Default for WeaponData {
} }
} }
#[derive(Clone)]
pub struct ChargeData { pub struct ChargeData {
pub max_charges: u8, pub max_charges: u8,
pub charge_name_prefix: &'static str, 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<()>; async fn check_make(&self, trans: &DBTrans, bench: &Item, recipe: &Item) -> UResult<()>;
} }
#[derive(Clone)]
pub struct ContainerData { pub struct ContainerData {
pub max_weight: u64, pub max_weight: u64,
pub base_weight: u64, pub base_weight: u64,
@ -289,6 +292,7 @@ impl Default for ContainerData {
} }
} }
#[derive(Clone)]
pub struct LiquidContainerData { pub struct LiquidContainerData {
pub capacity: u64, // in mL pub capacity: u64, // in mL
pub allowed_contents: Option<Vec<LiquidType>>, // None means anything. pub allowed_contents: Option<Vec<LiquidType>>, // None means anything.
@ -302,11 +306,13 @@ impl Default for LiquidContainerData {
} }
} }
#[derive(Clone)]
pub struct EatData { pub struct EatData {
pub hunger_impact: i16, pub hunger_impact: i16,
pub thirst_impact: i16, pub thirst_impact: i16,
} }
#[derive(Clone)]
pub struct SitData { pub struct SitData {
pub stress_impact: i16, pub stress_impact: i16,
} }
@ -427,6 +433,7 @@ pub enum PossessionType {
// Fluid containers // Fluid containers
DrinkBottle, DrinkBottle,
// Security // Security
Basiclock,
Scanlock, Scanlock,
// Food // Food
GrilledSteak, GrilledSteak,

View File

@ -12,6 +12,17 @@ use crate::{
use async_trait::async_trait; use async_trait::async_trait;
use once_cell::sync::OnceCell; 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; struct ScanLockLockcheck;
#[async_trait] #[async_trait]
impl LockcheckHandler for ScanLockLockcheck { impl LockcheckHandler for ScanLockLockcheck {
@ -145,6 +156,17 @@ impl InstallHandler for ScanLockInstall {
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new(); static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!( &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, (PossessionType::Scanlock,
PossessionData { PossessionData {
display: "scanlock", display: "scanlock",

View File

@ -1,17 +1,21 @@
use super::{possession_type::PossessionType, StaticItem}; use super::{possession_type::PossessionType, StaticItem};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::UResult, message_handler::user_commands::UResult,
models::item::{Item, ItemFlag}, models::item::{DoorState, Item, ItemFlag},
regular_tasks::queued_command::QueuedCommandContext, regular_tasks::queued_command::QueuedCommandContext,
DResult,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use mockall_double::double;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
mod chonkers; mod chonkers;
mod cok_murl; mod cok_murl;
mod computer_museum; pub mod computer_museum;
mod melbs; mod melbs;
mod repro_xv; mod repro_xv;
mod special; mod special;
@ -330,6 +334,7 @@ pub struct Room {
pub rentable_dynzone: Vec<RentInfo>, pub rentable_dynzone: Vec<RentInfo>,
pub material_type: MaterialType, pub material_type: MaterialType,
pub has_power: bool, pub has_power: bool,
pub door_states: Option<BTreeMap<Direction, DoorState>>,
} }
impl Default for Room { impl Default for Room {
@ -351,6 +356,7 @@ impl Default for Room {
rentable_dynzone: vec![], rentable_dynzone: vec![],
material_type: MaterialType::Normal, material_type: MaterialType::Normal,
has_power: false, has_power: false,
door_states: None,
} }
} }
} }
@ -403,6 +409,7 @@ pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
location: format!("zone/{}", r.zone), location: format!("zone/{}", r.zone),
is_static: true, is_static: true,
flags: r.item_flags.clone(), flags: r.item_flags.clone(),
door_states: r.door_states.clone(),
..Item::default() ..Item::default()
}), }),
extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())), 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::<BTreeSet<Direction>>();
let template_keys = template_states
.keys()
.map(|v| v.clone())
.collect::<BTreeSet<Direction>>();
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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -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 super::{Direction, Exit, ExitTarget, GridCoords, Room, SecondaryZoneRecord};
use ansi::ansi; use ansi::ansi;
pub fn room_list() -> Vec<Room> { pub fn room_list() -> Vec<Room> {
@ -128,14 +136,13 @@ pub fn room_list() -> Vec<Room> {
name: "Doorwell", name: "Doorwell",
short: ansi!("<bgblack><yellow>/\\<reset>"), short: ansi!("<bgblack><yellow>/\\<reset>"),
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\ 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 \ with with some kind of electronic screen on it, showing some kind of towers with \
coloured disks stacked on them.\n\ coloured discs stacked on them.\n\
The disks are arranged as follows:\n\ [Hint: try <bold>-doorbot move from 1 to 2<reset> to ask Doorbot \
Tower 1: A small <green>green disk<reset>, atop a medium sized <yellow>yellow disk<reset>, \ to move the top disc from tower 1 to tower 2]\n\
atop a large <bgblack><white>white disk<reset>, atop an extra large <red>red disk<reset>.\n\ The discs are arranged as follows:\n"),
Tower 2: Empty\n\
Tower 3: Empty"),
description_less_explicit: None, description_less_explicit: None,
grid_coords: GridCoords { x: 4, y: 0, z: -1 }, grid_coords: GridCoords { x: 4, y: 0, z: -1 },
exits: vec!( exits: vec!(
@ -166,8 +173,42 @@ pub fn room_list() -> Vec<Room> {
..Default::default() ..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, should_caption: true,
..Default::default() ..Default::default()
}, },
) )
} }
pub fn fixed_items() -> Vec<FixedItem> {
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()
},
)]
}