Implement computer museum puzzle.
This commit is contained in:
parent
4467707d4a
commit
6dc4a870fc
@ -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 => {}
|
||||
|
@ -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<Config> {
|
||||
}
|
||||
|
||||
#[tokio::main(worker_threads = 2)]
|
||||
#[cfg(not(test))]
|
||||
async fn main() -> DResult<()> {
|
||||
SimpleLogger::new()
|
||||
.with_level(LevelFilter::Info)
|
||||
|
@ -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;
|
||||
|
@ -419,10 +419,14 @@ pub async fn describe_room(
|
||||
ansi!("<reset> "),
|
||||
&word_wrap(
|
||||
&format!(
|
||||
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
|
||||
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\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<Vec<&Arc<Item>>> = 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::<Vec<&Arc<Item>>>())
|
||||
|
@ -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?
|
||||
}
|
||||
|
@ -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<String>,
|
||||
},
|
||||
HanoiPuzzle {
|
||||
towers: [Vec<u8>; 3],
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
|
||||
@ -455,6 +459,7 @@ pub struct Item {
|
||||
pub display_less_explicit: Option<String>,
|
||||
pub details: Option<String>,
|
||||
pub details_less_explicit: Option<String>,
|
||||
pub details_dyn_suffix: Option<String>,
|
||||
pub aliases: Vec<String>,
|
||||
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,
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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<StaticThingTypeGroup<StaticTask>> {
|
||||
]
|
||||
}
|
||||
|
||||
#[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?;
|
||||
|
@ -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<FixedItem> {
|
||||
@ -41,6 +57,7 @@ fn fixed_item_list() -> &'static Vec<FixedItem> {
|
||||
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<FixedItem> {
|
||||
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<dyn Iterator<Item = StaticItem>> {
|
||||
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())),
|
||||
|
@ -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: <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> {
|
||||
use NPCSayType::FromFixedList;
|
||||
@ -115,5 +446,21 @@ pub fn npc_list() -> Vec<NPC> {
|
||||
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 <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()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -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<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> 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<Vec<LiquidType>>, // 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,
|
||||
|
@ -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<Vec<(PossessionType, PossessionData)>> = 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",
|
||||
|
@ -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<RentInfo>,
|
||||
pub material_type: MaterialType,
|
||||
pub has_power: bool,
|
||||
pub door_states: Option<BTreeMap<Direction, DoorState>>,
|
||||
}
|
||||
|
||||
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<dyn Iterator<Item = StaticItem>> {
|
||||
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::<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)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -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<Room> {
|
||||
@ -128,14 +136,13 @@ pub fn room_list() -> Vec<Room> {
|
||||
name: "Doorwell",
|
||||
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\
|
||||
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>green disk<reset>, atop a medium sized <yellow>yellow disk<reset>, \
|
||||
atop a large <bgblack><white>white disk<reset>, atop an extra large <red>red disk<reset>.\n\
|
||||
Tower 2: Empty\n\
|
||||
Tower 3: Empty"),
|
||||
coloured discs stacked on them.\n\
|
||||
[Hint: try <bold>-doorbot move from 1 to 2<reset> 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<Room> {
|
||||
..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<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()
|
||||
},
|
||||
)]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user