forked from blasthavers/blastmud
254 lines
8.8 KiB
Rust
254 lines
8.8 KiB
Rust
// A dynzone is a template for a set of rooms arranged in a fixed pattern.
|
|
// While the layout is static, an arbitrary number of instances can be created
|
|
// dynamically. They can dynamically connect to the grid.
|
|
// Apartments, planes, and boats are all expected to be specific instances of dynzones.
|
|
use super::room::{Direction, GridCoords};
|
|
use crate::{
|
|
message_handler::user_commands::{user_error, UResult},
|
|
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance, DoorState}
|
|
};
|
|
use once_cell::sync::OnceCell;
|
|
use std::collections::BTreeMap;
|
|
use mockall_double::double;
|
|
#[double] use crate::db::DBTrans;
|
|
|
|
mod cokmurl_apartment;
|
|
|
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
|
|
pub enum DynzoneType {
|
|
CokMurlApartment
|
|
}
|
|
|
|
impl DynzoneType {
|
|
pub fn from_str(i: &str) -> Option<Self> {
|
|
match i {
|
|
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
|
|
_ => None
|
|
}
|
|
}
|
|
pub fn to_str(&self) -> &'static str {
|
|
match self {
|
|
DynzoneType::CokMurlApartment => "CokMurlApartment"
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
|
|
pub struct Dynzone {
|
|
pub zonetype: DynzoneType,
|
|
pub zonename: &'static str,
|
|
pub entrypoint_subcode: &'static str,
|
|
pub dyn_rooms: BTreeMap<&'static str, Dynroom>,
|
|
}
|
|
|
|
impl Dynzone {
|
|
// Returns None if there is already an instance in the same exit direction.
|
|
pub async fn create_instance(&self, trans: &DBTrans, connect_where: &str, dup_message: &str,
|
|
new_owner: &Item, new_exit_direction: &Direction) -> UResult<String> {
|
|
// Check exit not taken...
|
|
if trans.find_exact_dyn_exit(connect_where, new_exit_direction).await?.is_some() {
|
|
user_error(dup_message.to_string())?;
|
|
}
|
|
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
|
|
|
|
let code = format!("{}", &trans.alloc_item_code().await?);
|
|
trans.create_item(
|
|
&Item {
|
|
item_type: "dynzone".to_owned(),
|
|
item_code: code.clone(),
|
|
display: self.zonename.to_owned(),
|
|
special_data: Some(
|
|
ItemSpecialData::DynzoneData {
|
|
zone_exit: Some(connect_where.to_owned()),
|
|
vacate_after: None
|
|
}
|
|
),
|
|
owner: Some(owner.clone()),
|
|
location: format!("dynzone/{}", &code),
|
|
..Default::default()
|
|
}
|
|
).await?;
|
|
|
|
let mut should_connect = true;
|
|
for (_, room) in &self.dyn_rooms {
|
|
let roomcode = format!("{}/{}", &code, room.subcode);
|
|
let will_connect = should_connect &&
|
|
room.exits.iter().any(|r| match r.target {
|
|
ExitTarget::ExitZone => true,
|
|
_ => false
|
|
});
|
|
should_connect &= !will_connect;
|
|
trans.create_item(
|
|
&Item {
|
|
item_type: "dynroom".to_owned(),
|
|
item_code: roomcode,
|
|
display: room.name.to_owned(),
|
|
details: Some(room.description.to_owned()),
|
|
details_less_explicit: room.description_less_explicit.map(|s| s.to_owned()),
|
|
location: format!("dynzone/{}", &code),
|
|
special_data: Some(ItemSpecialData::DynroomData {
|
|
dynzone_code: self.zonetype.to_str().to_owned(),
|
|
dynroom_code: room.subcode.to_owned()
|
|
}),
|
|
dynamic_entrance: if will_connect {
|
|
Some(DynamicEntrance {
|
|
direction: new_exit_direction.clone(),
|
|
source_item: connect_where.to_owned()
|
|
})
|
|
} else { None },
|
|
flags: room.item_flags.clone(),
|
|
owner: Some(owner.clone()),
|
|
door_states: Some(room.exits.iter()
|
|
.filter_map(|ex|
|
|
if let ExitType::Doored { description } = ex.exit_type {
|
|
Some((ex.direction.clone(), DoorState {
|
|
open: false,
|
|
description: description.to_owned()
|
|
}))
|
|
} else {
|
|
None
|
|
}).collect()),
|
|
..Default::default()
|
|
}
|
|
).await?;
|
|
}
|
|
|
|
Ok(format!("dynzone/{}", &code))
|
|
}
|
|
}
|
|
|
|
impl Default for Dynzone {
|
|
fn default() -> Self {
|
|
Self {
|
|
zonename: "undefined",
|
|
zonetype: DynzoneType::CokMurlApartment,
|
|
entrypoint_subcode: "entry",
|
|
dyn_rooms: BTreeMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note that either the room being entered or the room being left should be
|
|
// doored, not both. And doors should be on the inner-most room (furthest from
|
|
// public) - locks only protect entry into the room, not exit from it.
|
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
|
|
pub enum ExitType {
|
|
Doorless,
|
|
Doored { description: &'static str },
|
|
}
|
|
|
|
#[derive(Eq, Ord, Debug, PartialEq, PartialOrd, Clone)]
|
|
pub enum ExitTarget {
|
|
ExitZone,
|
|
Intrazone { subcode: &'static str },
|
|
}
|
|
|
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
|
|
pub struct Exit {
|
|
pub direction: Direction,
|
|
pub target: ExitTarget,
|
|
pub exit_type: ExitType,
|
|
}
|
|
|
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
|
|
pub struct Dynroom {
|
|
pub subcode: &'static str,
|
|
pub name: &'static str,
|
|
pub short: &'static str,
|
|
pub description: &'static str,
|
|
pub description_less_explicit: Option<&'static str>,
|
|
pub exits: Vec<Exit>,
|
|
pub should_caption: bool,
|
|
pub item_flags: Vec<ItemFlag>,
|
|
pub grid_coords: GridCoords,
|
|
}
|
|
|
|
impl Default for Dynroom {
|
|
fn default() -> Self {
|
|
Self {
|
|
subcode: "undefined",
|
|
name: "Undefined",
|
|
short: "XX",
|
|
description: "A generic room",
|
|
description_less_explicit: None,
|
|
exits: vec!(),
|
|
should_caption: false,
|
|
item_flags: vec!(),
|
|
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn dynzone_list() -> &'static Vec<Dynzone> {
|
|
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
|
|
CELL.get_or_init(
|
|
|| vec!(
|
|
cokmurl_apartment::zone()
|
|
)
|
|
)
|
|
}
|
|
|
|
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
|
|
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
|
|
CELL.get_or_init(
|
|
|| dynzone_list().iter().map(|z| (&z.zonetype, (*z).clone())).collect()
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use itertools::Itertools;
|
|
use super::super::room::{Direction};
|
|
use super::{dynzone_list, ExitTarget, DynzoneType};
|
|
#[test]
|
|
fn dynzone_types_unique() {
|
|
let mut sorted_list = dynzone_list().clone();
|
|
sorted_list.sort();
|
|
assert_eq!(Vec::<(&DynzoneType, usize)>::new(),
|
|
sorted_list.iter()
|
|
.group_by(|v| &v.zonetype)
|
|
.into_iter()
|
|
.map(|v| (v.0, v.1.count()))
|
|
.filter(|v| v.1 > 1)
|
|
.collect::<Vec<(&DynzoneType, usize)>>()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dynroom_codes_match_struct() {
|
|
for dynzone in dynzone_list() {
|
|
assert_eq!(
|
|
dynzone.dyn_rooms.iter().filter(|v| *v.0 != v.1.subcode)
|
|
.map(|v| *v.0).collect::<Vec<&str>>(),
|
|
Vec::<&str>::new()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dynzone_has_dynroom() {
|
|
for dynzone in dynzone_list() {
|
|
assert_ne!(0, dynzone.dyn_rooms.len(), "# rooms in zone {}",
|
|
dynzone.zonetype.to_str())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dynroom_exits_subcodes_exists() {
|
|
for dynzone in dynzone_list() {
|
|
for dynroom in dynzone.dyn_rooms.iter() {
|
|
let exits = dynroom.1.exits.iter().filter(
|
|
|ex|
|
|
if let ExitTarget::Intrazone { subcode } = ex.target {
|
|
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
|
|
} else {
|
|
false
|
|
}).map(|ex| &ex.direction).collect::<Vec<&Direction>>();
|
|
assert_eq!(Vec::<&Direction>::new(), exits,
|
|
"exits to invalid subcode in room {} in zone {}", dynroom.0,
|
|
dynzone.zonetype.to_str());
|
|
}
|
|
}
|
|
}
|
|
}
|