blastmud/blastmud_game/src/static_content/dynzone.rs

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());
}
}
}
}