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::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::LocationActionType,
@ -25,13 +25,11 @@ impl QueueCommandHandler for QueueHandler {
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Get { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
match ctx.command {
QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
@ -61,6 +59,60 @@ impl QueueCommandHandler for QueueHandler {
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())?,
};
Ok(time::Duration::from_secs(1))
}
@ -71,13 +123,11 @@ impl QueueCommandHandler for QueueHandler {
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Get { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx.command {
QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
@ -90,32 +140,6 @@ impl QueueCommandHandler for QueueHandler {
))?
}
let possession_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
match check_item_capacity(ctx.trans, &ctx.item, possession_data.weight).await? {
CapacityLevel::AboveItemLimit => {
user_error("You just can't hold that many things!".to_owned())?
}
CapacityLevel::OverBurdened => {
let explicit = ctx.explicit().await?;
user_error(format!(
"{} You drop {} because it is too heavy!",
if explicit { "Fuck!" } else { "Rats!" },
&ctx.item.display_for_sentence(explicit, 1, false)
))?
}
_ => (),
}
let msg_exp = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(true, 1, true),
@ -134,6 +158,89 @@ impl QueueCommandHandler for QueueHandler {
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())?,
};
let possession_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
match check_item_capacity(ctx.trans, &ctx.item, possession_data.weight).await? {
CapacityLevel::AboveItemLimit => {
user_error("You just can't hold that many things!".to_owned())?
}
CapacityLevel::OverBurdened => {
let explicit = ctx.explicit().await?;
user_error(format!(
"{} You can't get {} because it is too heavy!",
if explicit { "Fuck!" } else { "Rats!" },
&ctx.item.display_for_sentence(explicit, 1, false)
))?
}
_ => (),
}
let mut item_mut = (*item).clone();
item_mut.location = ctx.item.refstr();
item_mut.action_type = LocationActionType::Normal;
@ -152,7 +259,7 @@ impl UserVerb for Verb {
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
// TODO: Parse "get target from container" variant
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
@ -161,13 +268,33 @@ impl UserVerb for Verb {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
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(
ctx,
&ItemSearchParams {
include_loc_contents,
include_contents,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
..ItemSearchParams::base(&search_what, for_what)
},
)
.await?;
@ -187,6 +314,7 @@ impl UserVerb for Verb {
user_error("You can't get that!".to_owned())?;
}
did_anything = true;
if include_loc_contents {
queue_command(
ctx,
&mut player_item_mut,
@ -195,6 +323,17 @@ impl UserVerb for Verb {
},
)
.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 {
user_error("I didn't find anything matching.".to_owned())?;

View File

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

View File

@ -64,11 +64,27 @@ pub async fn check_item_capacity(
#[cfg(test)]
mod test {
use super::*;
use crate::db::{LocationStats, MockDBTrans};
use crate::{
db::{LocationStats, MockDBTrans},
models::item::StatType,
};
#[tokio::test]
async fn check_item_capacity_should_say_above_item_limit_if_over() {
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
.expect_get_location_stats()
.withf(|s| s == "player/foo")
@ -89,6 +105,19 @@ mod test {
#[tokio::test]
async fn check_item_capacity_should_say_overburdened_if_over() {
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
.expect_get_location_stats()
.withf(|s| s == "player/foo")
@ -109,6 +138,20 @@ mod test {
#[tokio::test]
async fn check_item_capacity_should_say_unburdened_when_low_weight() {
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
.expect_get_location_stats()
.withf(|s| s == "player/foo")

View File

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
mod blade;
mod books;
mod corp_licence;
mod fangs;
pub mod head_armour;
@ -338,6 +339,8 @@ pub enum PossessionType {
Steak,
AnimalSkin,
SeveredHead,
// Recipes
CulinaryEssentials,
}
impl Into<Item> for PossessionType {
@ -432,6 +435,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
.iter()
.map(|v| ((*v).0.clone(), &(*v).1)),
)
.chain(books::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.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,9 +2155,38 @@ pub fn room_list() -> Vec<Room> {
direction: Direction::SOUTH,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..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 {
zone: "melbs",