Add a bookshop, and add getting things from containers

The book doesn't yet contain recipes, but when it does, we will be
able to get things out of it to use in the non-improv crafting system
(to be implemented).
This commit is contained in:
Condorra 2023-07-14 23:03:06 +10:00
parent ea45530a39
commit b3cbc9f544
6 changed files with 323 additions and 83 deletions

View File

@ -1,6 +1,6 @@
use super::{ use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error, get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext, user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
}; };
use crate::{ use crate::{
models::item::LocationActionType, models::item::LocationActionType,
@ -25,42 +25,94 @@ impl QueueCommandHandler for QueueHandler {
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(), "You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?; )?;
} }
let item_id = match ctx.command { match ctx.command {
QueueCommand::Get { possession_id } => possession_id, QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let msg_exp = format!(
"{} fumbles around trying to pick up {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around trying to pick up {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
}
QueueCommand::GetFromContainer {
from_possession_id,
get_possession_id,
} => {
let container = ctx
.trans
.find_item_by_type_code("possession", &from_possession_id)
.await?
.ok_or_else(|| UserError("Item to get from not found".to_owned()))?;
if container.location != ctx.item.location
&& container.location != ctx.item.refstr()
{
user_error(format!(
"You try to get something from {} but realise {} is no longer there",
container.display_for_sentence(ctx.explicit().await?, 1, false),
container.display_for_sentence(ctx.explicit().await?, 1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &get_possession_id)
.await?
.ok_or_else(|| UserError("Item to get not found".to_owned()))?;
if item.location != container.refstr() {
user_error(format!(
"You try to get {} but realise it is no longer in {}",
item.display_for_sentence(ctx.explicit().await?, 1, false),
container.display_for_sentence(ctx.explicit().await?, 1, false),
))?
}
let msg_exp = format!(
"{} fumbles around trying to get {} from {}.\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false),
&container.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around trying to get {} from {}.\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false),
&container.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
}
_ => user_error("Unexpected command".to_owned())?, _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let msg_exp = format!(
"{} fumbles around trying to pick up {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around trying to pick up {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
@ -71,24 +123,97 @@ impl QueueCommandHandler for QueueHandler {
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(), "You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?; )?;
} }
let item_id = match ctx.command { let item = match ctx.command {
QueueCommand::Get { possession_id } => possession_id, QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
&item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let msg_exp = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
item
}
QueueCommand::GetFromContainer {
from_possession_id,
get_possession_id,
} => {
let container = ctx
.trans
.find_item_by_type_code("possession", &from_possession_id)
.await?
.ok_or_else(|| UserError("Item to get from not found".to_owned()))?;
if container.location != ctx.item.location
&& container.location != ctx.item.refstr()
{
user_error(format!(
"You try to get something from {} but realise {} is no longer there",
container.display_for_sentence(ctx.explicit().await?, 1, false),
container.display_for_sentence(ctx.explicit().await?, 1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &get_possession_id)
.await?
.ok_or_else(|| UserError("Item to get not found".to_owned()))?;
if item.location != container.refstr() {
user_error(format!(
"You try to get {} but realise it is no longer in {}",
item.display_for_sentence(ctx.explicit().await?, 1, false),
container.display_for_sentence(ctx.explicit().await?, 1, false),
))?
}
let msg_exp = format!(
"{} gets {} from {}.\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false),
&container.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} gets {} from {}.\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false),
&container.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
item
}
_ => user_error("Unexpected command".to_owned())?, _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
&item.display_for_sentence(ctx.explicit().await?, 1, false)
))?
}
let possession_data = match item let possession_data = match item
.possession_type .possession_type
@ -108,7 +233,7 @@ impl QueueCommandHandler for QueueHandler {
CapacityLevel::OverBurdened => { CapacityLevel::OverBurdened => {
let explicit = ctx.explicit().await?; let explicit = ctx.explicit().await?;
user_error(format!( user_error(format!(
"{} You drop {} because it is too heavy!", "{} You can't get {} because it is too heavy!",
if explicit { "Fuck!" } else { "Rats!" }, if explicit { "Fuck!" } else { "Rats!" },
&ctx.item.display_for_sentence(explicit, 1, false) &ctx.item.display_for_sentence(explicit, 1, false)
))? ))?
@ -116,24 +241,6 @@ impl QueueCommandHandler for QueueHandler {
_ => (), _ => (),
} }
let msg_exp = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
item_mut.location = ctx.item.refstr(); item_mut.location = ctx.item.refstr();
item_mut.action_type = LocationActionType::Normal; item_mut.action_type = LocationActionType::Normal;
@ -152,7 +259,7 @@ impl UserVerb for Verb {
mut remaining: &str, mut remaining: &str,
) -> UResult<()> { ) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
// TODO: Parse "get target from container" variant
let mut get_limit = Some(1); let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") { if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim(); remaining = remaining[3..].trim();
@ -161,13 +268,33 @@ impl UserVerb for Verb {
get_limit = Some(n); get_limit = Some(n);
remaining = remaining2; remaining = remaining2;
} }
let (search_what, for_what, include_contents, include_loc_contents) =
match remaining.split_once(" from ") {
None => (player_item.clone(), remaining, false, true),
Some((item_str_raw, container_str_raw)) => {
let container = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
include_contents: true,
item_type_only: Some("possession"),
..ItemSearchParams::base(&player_item, container_str_raw.trim())
},
)
.await?;
(container, item_str_raw.trim(), true, false)
}
};
let targets = search_items_for_user( let targets = search_items_for_user(
ctx, ctx,
&ItemSearchParams { &ItemSearchParams {
include_loc_contents: true, include_loc_contents,
include_contents,
item_type_only: Some("possession"), item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100), limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&search_what, for_what)
}, },
) )
.await?; .await?;
@ -187,14 +314,26 @@ impl UserVerb for Verb {
user_error("You can't get that!".to_owned())?; user_error("You can't get that!".to_owned())?;
} }
did_anything = true; did_anything = true;
queue_command( if include_loc_contents {
ctx, queue_command(
&mut player_item_mut, ctx,
&QueueCommand::Get { &mut player_item_mut,
possession_id: target.item_code.clone(), &QueueCommand::Get {
}, possession_id: target.item_code.clone(),
) },
.await?; )
.await?;
} else {
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::GetFromContainer {
from_possession_id: search_what.item_code.clone(),
get_possession_id: target.item_code.clone(),
},
)
.await?;
}
} }
if !did_anything { if !did_anything {
user_error("I didn't find anything matching.".to_owned())?; user_error("I didn't find anything matching.".to_owned())?;

View File

@ -63,6 +63,10 @@ pub enum QueueCommand {
Get { Get {
possession_id: String, possession_id: String,
}, },
GetFromContainer {
from_possession_id: String,
get_possession_id: String,
},
Movement { Movement {
direction: Direction, direction: Direction,
source: MovementSource, source: MovementSource,
@ -100,6 +104,7 @@ impl QueueCommand {
Cut { .. } => "Cut", Cut { .. } => "Cut",
Drop { .. } => "Drop", Drop { .. } => "Drop",
Get { .. } => "Get", Get { .. } => "Get",
GetFromContainer { .. } => "GetFromContainer",
Movement { .. } => "Movement", Movement { .. } => "Movement",
OpenDoor { .. } => "OpenDoor", OpenDoor { .. } => "OpenDoor",
Remove { .. } => "Remove", Remove { .. } => "Remove",
@ -167,6 +172,10 @@ fn queue_command_registry(
"Get", "Get",
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
), ),
(
"GetFromContainer",
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
( (
"Movement", "Movement",
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),

View File

@ -64,11 +64,27 @@ pub async fn check_item_capacity(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::db::{LocationStats, MockDBTrans}; use crate::{
db::{LocationStats, MockDBTrans},
models::item::StatType,
};
#[tokio::test] #[tokio::test]
async fn check_item_capacity_should_say_above_item_limit_if_over() { async fn check_item_capacity_should_say_above_item_limit_if_over() {
let mut mock_db = MockDBTrans::new(); let mut mock_db = MockDBTrans::new();
mock_db
.expect_find_item_by_type_code()
.withf(|t, c| t == "player" && c == "foo")
.returning(|_, _| {
Ok(Some(
Item {
item_type: "player".to_owned(),
item_code: "foo".to_owned(),
..Default::default()
}
.into(),
))
});
mock_db mock_db
.expect_get_location_stats() .expect_get_location_stats()
.withf(|s| s == "player/foo") .withf(|s| s == "player/foo")
@ -89,6 +105,19 @@ mod test {
#[tokio::test] #[tokio::test]
async fn check_item_capacity_should_say_overburdened_if_over() { async fn check_item_capacity_should_say_overburdened_if_over() {
let mut mock_db = MockDBTrans::new(); let mut mock_db = MockDBTrans::new();
mock_db
.expect_find_item_by_type_code()
.withf(|t, c| t == "player" && c == "foo")
.returning(|_, _| {
Ok(Some(
Item {
item_type: "player".to_owned(),
item_code: "foo".to_owned(),
..Default::default()
}
.into(),
))
});
mock_db mock_db
.expect_get_location_stats() .expect_get_location_stats()
.withf(|s| s == "player/foo") .withf(|s| s == "player/foo")
@ -109,6 +138,20 @@ mod test {
#[tokio::test] #[tokio::test]
async fn check_item_capacity_should_say_unburdened_when_low_weight() { async fn check_item_capacity_should_say_unburdened_when_low_weight() {
let mut mock_db = MockDBTrans::new(); let mut mock_db = MockDBTrans::new();
mock_db
.expect_find_item_by_type_code()
.withf(|t, c| t == "player" && c == "foo")
.returning(|_, _| {
Ok(Some(
Item {
item_type: "player".to_owned(),
item_code: "foo".to_owned(),
total_stats: vec![(StatType::Brawn, 8.0)].into_iter().collect(),
..Default::default()
}
.into(),
))
});
mock_db mock_db
.expect_get_location_stats() .expect_get_location_stats()
.withf(|s| s == "player/foo") .withf(|s| s == "player/foo")

View File

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
mod blade; mod blade;
mod books;
mod corp_licence; mod corp_licence;
mod fangs; mod fangs;
pub mod head_armour; pub mod head_armour;
@ -338,6 +339,8 @@ pub enum PossessionType {
Steak, Steak,
AnimalSkin, AnimalSkin,
SeveredHead, SeveredHead,
// Recipes
CulinaryEssentials,
} }
impl Into<Item> for PossessionType { impl Into<Item> for PossessionType {
@ -432,6 +435,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
.iter() .iter()
.map(|v| ((*v).0.clone(), &(*v).1)), .map(|v| ((*v).0.clone(), &(*v).1)),
) )
.chain(books::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.collect() .collect()
}) })
} }

View File

@ -0,0 +1,16 @@
use super::{PossessionData, PossessionType};
use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::CulinaryEssentials,
PossessionData {
display: "Culinary Essentials cookbook",
aliases: vec!["book", "cookbook"],
details: "A weathered cookbook filled with essential recipes for survival in the post-apocalyptic world. Its pages are yellowed and marked with stains, a testament to its extensive use by those seeking sustenance and comfort amidst scarcity.",
weight: 300,
..Default::default()
}),
))
}

View File

@ -2155,10 +2155,39 @@ pub fn room_list() -> Vec<Room> {
direction: Direction::SOUTH, direction: Direction::SOUTH,
..Default::default() ..Default::default()
}, },
Exit {
direction: Direction::EAST,
..Default::default()
},
), ),
should_caption: false, should_caption: false,
..Default::default() ..Default::default()
}, },
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_dustypages",
name: "The Dusty Pages",
short: ansi!("<bgblue><yellow>DP<reset>"),
description: "Beneath a large hand-carved wooden sign reading \"The Dusty Pages\" lies a small oasis of knowledge. The room is dimly lit, with flickering candles and shafts of sunlight piercing through cracked windows. The air is heavy with the scent of decaying books and the lingering memories of a bygone era.\n\nShelves made of salvaged wood stand defiantly against the crumbling walls, bearing the weight of books that have miraculously survived the ravages of time and nuclear fallout. The covers are worn and the pages yellowed, but the knowledge contained within remains invaluable.\n\nThe inhabitants of this forsaken land gather here, seeking solace and hope within the forgotten stories and practical guides that line the shelves.\n\nThe Dusty Pages stands as a beacon of intellectual survival, a sanctuary where survivors can momentarily escape the harsh realities of their existence.",
description_less_explicit: None,
grid_coords: GridCoords { x: 6, y: 4, z: 0 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
),
should_caption: true,
stock_list: vec!(
RoomStock {
possession_type: PossessionType::CulinaryEssentials,
list_price: 200,
..Default::default()
}
),
..Default::default()
},
Room { Room {
zone: "melbs", zone: "melbs",
secondary_zones: vec!(), secondary_zones: vec!(),