use super::StaticItem; use once_cell::sync::OnceCell; use std::collections::BTreeMap; use ansi::ansi; use async_trait::async_trait; use serde::{Serialize, Deserialize}; use crate::message_handler::user_commands::{ UResult, VerbContext }; use crate::models::item::Item; pub struct Zone { pub code: &'static str, pub display: &'static str, pub outdoors: bool, } static STATIC_ZONE_DETAILS: OnceCell> = OnceCell::new(); pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> { STATIC_ZONE_DETAILS.get_or_init( || vec!( Zone { code: "melbs", display: "Melbs", outdoors: true }, Zone { code: "repro_xv", display: "Reprolabs XV", outdoors: true }, ).into_iter().map(|x|(x.code, x)).collect()) } #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)] pub struct GridCoords { pub x: i64, pub y: i64, pub z: i64 } impl GridCoords { pub fn apply(self: &GridCoords, dir: &Direction) -> GridCoords { match dir { Direction::NORTH => GridCoords {y: self.y - 1, ..*self}, Direction::SOUTH => GridCoords {y: self.y + 1, ..*self}, Direction::EAST => GridCoords {x: self.x + 1, ..*self}, Direction::WEST => GridCoords {x: self.x - 1, ..*self}, Direction::NORTHEAST => GridCoords {x: self.x + 1, y: self.y - 1, ..*self}, Direction::SOUTHEAST => GridCoords {x: self.x + 1, y: self.y + 1, ..*self}, Direction::NORTHWEST => GridCoords {x: self.x - 1, y: self.y - 1, ..*self}, Direction::SOUTHWEST => GridCoords {x: self.x - 1, y: self.y + 1, ..*self}, Direction::UP => GridCoords {z: self.z + 1, ..*self}, Direction::DOWN => GridCoords {z: self.z - 1, ..*self}, Direction::IN { .. } => self.clone() } } } #[async_trait] pub trait ExitBlocker { // True if they will be allowed to pass the exit, false otherwise. async fn attempt_exit( self: &Self, ctx: &mut VerbContext, player: &Item, exit: &Exit ) -> UResult; } pub enum ExitType { Free, // Anyone can just walk it. Blocked(&'static (dyn ExitBlocker + Sync + Send)), // Custom code about who can pass. // Future ideas: Doors with locks, etc... } #[allow(dead_code)] #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug, Serialize, Deserialize)] pub enum Direction { NORTH, SOUTH, EAST, WEST, NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST, UP, DOWN, IN { item: String } } impl Direction { pub fn describe(self: &Self) -> String { match self { Direction::NORTH => "north".to_owned(), Direction::SOUTH => "south".to_owned(), Direction::EAST => "east".to_owned(), Direction::WEST => "west".to_owned(), Direction::NORTHEAST => "northeast".to_owned(), Direction::SOUTHEAST => "southeast".to_owned(), Direction::NORTHWEST => "northwest".to_owned(), Direction::SOUTHWEST => "southwest".to_owned(), Direction::UP => "up".to_owned(), Direction::DOWN => "down".to_owned(), Direction::IN { item } => item.to_owned() } } pub fn parse(input: &str) -> Option<&'static Direction> { match input { "north" | "n" => Some(&Direction::NORTH), "south" | "s" => Some(&Direction::SOUTH), "east" | "e" => Some(&Direction::EAST), "west" | "w" => Some(&Direction::WEST), "northeast" | "ne" => Some(&Direction::NORTHEAST), "southeast" | "se" => Some(&Direction::SOUTHEAST), "northwest" | "nw" => Some(&Direction::NORTHEAST), "southwest" | "sw" => Some(&Direction::SOUTHWEST), "up" => Some(&Direction::UP), "down" => Some(&Direction::DOWN), _ => None } } } pub enum ExitTarget { UseGPS, Custom(&'static str) } pub struct Exit { pub direction: Direction, pub target: ExitTarget, pub exit_type: ExitType, } pub struct SecondaryZoneRecord { pub zone: &'static str, pub short: &'static str, pub grid_coords: GridCoords } pub struct Room { pub zone: &'static str, // Other zones where it can be seen on the map and accessed. pub secondary_zones: Vec, pub code: &'static str, pub name: &'static str, pub short: &'static str, pub grid_coords: GridCoords, pub description: &'static str, pub description_less_explicit: Option<&'static str>, pub exits: Vec } static STATIC_ROOM_LIST: OnceCell> = OnceCell::new(); pub fn room_list() -> &'static Vec { STATIC_ROOM_LIST.get_or_init( || vec!( Room { zone: "repro_xv", secondary_zones: vec!(), code: "repro_xv_chargen", name: ansi!("Choice Room"), short: ansi!("CR"), description: ansi!( "A room brightly lit in unnaturally white light, covered in sparkling \ white tiles from floor to ceiling. A loudspeaker plays a message on \ loop:\n\ \t\"Citizen, you are here because your memory has been wiped and \ you are ready to start a fresh life. As a being enhanced by \ Gazos-Murlison Co technology, the emperor has granted you the power \ to choose 14 points of upgrades to yourself. Choose wisely, as it \ will impact who you end up being, and you would need to completely \ wipe your brain again to change them. Talk to Statbot to spend your \ 14 points and create your body.\"\n\ [Try -statbot hi, to send hi to statbot - the - means \ to whisper to a particular person in the room]"), description_less_explicit: None, grid_coords: GridCoords { x: 0, y: 0, z: -1 }, exits: vec!(Exit { direction: Direction::EAST, target: ExitTarget::UseGPS, exit_type: ExitType::Blocked(&super::npc::statbot::ChoiceRoomBlocker), }) }, Room { zone: "repro_xv", secondary_zones: vec!(), code: "repro_xv_updates", name: ansi!("Update Centre"), short: ansi!("UC"), description: ansi!( "A room covered in posters, evidently meant to help newly wiped individuals \ get up to speed on what has happened in the world since their memory implant was \ created. A one-way opens to the east - you have a feeling that once you go through, \ there will be no coming back in here. [Hint: Try reading the posters here.]"), description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 0, z: -1 }, exits: vec!(Exit { direction: Direction::EAST, target: ExitTarget::UseGPS, exit_type: ExitType::Free, }) }, Room { zone: "repro_xv", secondary_zones: vec!(), code: "repro_xv_respawn", name: ansi!("Body Factory"), short: ansi!("BF"), description: ansi!( "A room filled with glass vats full of clear fluids, with bodies of \ various stages of development floating in them. It smells like bleach. \ Being here makes you realise you aren't exactly alive right now... you \ have no body. But you sense you could go up and attach \ your memories to a body matching your current stats"), description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 0, z: -1 }, exits: vec!(Exit { direction: Direction::UP, target: ExitTarget::UseGPS, exit_type: ExitType::Free }) }, Room { zone: "repro_xv", secondary_zones: vec!(SecondaryZoneRecord { zone: "melbs", short: ansi!("RL"), grid_coords: GridCoords { x: 2, y: 0, z: 0 }, }), code: "repro_xv_lobby", name: "Lobby", short: "<=", description: ansi!( "An entrance for an establishment called ReproLabs XV. \ It looks like they make bodies and attach peoples memories to \ them, and allow people to reclone when they die. It has an \ unattended reception desk, with chrome-plated letters reading \ ReproLabs XV stuck to the wall behind it. A pipe down to into the ground \ opens up here, but the airflow is so strong, it looks like it is out \ only - it seems to be how newly re-cloned bodies get back into the world"), description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 0, z: 0 }, exits: vec!( Exit { direction: Direction::WEST, target: ExitTarget::Custom("room/melbs_kingst_50"), exit_type: ExitType::Free }) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_latrobest", name: "King Street & Latrobe St", short: ansi!("##"), description: "A wide road (5 lanes each way) intersects a narrower 3 lane road. Both have been rather poorly maintained. Potholes dot the ashphalt road.", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: -5, z: 0 }, exits: vec!( Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_10", name: "King Street - 10 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: -4, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_20", name: "King Street - 20 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: -3, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_30", name: "King Street - 30 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: -2, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_lonsdalest", name: "King Street & Lonsdale St", short: ansi!("##"), description: "A wide road (5 lanes each way) intersects a narrower 2 lane each way road. Both have been rather poorly maintained. Potholes dot the ashphalt road", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: -1, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_40", name: ansi!("King Street - 40 block"), short: ansi!("||"), description: ansi!( "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side"), description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 0, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!( SecondaryZoneRecord { zone: "repro_xv", short: ansi!("EX"), grid_coords: GridCoords { x: 1, y: 0, z: 0 } } ), code: "melbs_kingst_50", name: ansi!("King Street - 50 block"), short: ansi!("||"), description: ansi!( "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side"), description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 1, z: 0 }, exits: vec!( Exit { direction: Direction::EAST, target: ExitTarget::Custom("room/repro_xv_lobby"), exit_type: ExitType::Free }, Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_60", name: "King Street - 60 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 2, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_bourkest", name: "King Street & Bourke St", short: ansi!("##"), description: "A wide road (5 lanes each way) intersects a slightly narrower 4-lane road with wide but heavily cracked concrete footpaths. Potholes dot the ashphalt road.", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 3, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_70", name: "King Street - 70 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 4, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_80", name: "King Street - 80 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 5, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_90", name: "King Street - 90 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 6, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_collinsst", name: "King Street & Collins St", short: ansi!("##"), description: "A wide road (5 lanes each way) intersects another wide 4-lane road. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 7, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_100", name: "King Street - 100 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 8, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_110", name: "King Street - 110 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 9, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_120", name: "King Street - 120 block", short: ansi!("||"), description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 10, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, Exit { direction: Direction::SOUTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, Room { zone: "melbs", secondary_zones: vec!(), code: "melbs_kingst_flinderst", name: "King Street & Flinders St", short: ansi!("##"), description: "A wide road (5 lanes each way) intersects a wide road with rusted tram tracks in the middle. Potholes dot the ashphalt road.", description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 11, z: 0 }, exits: vec!( Exit { direction: Direction::NORTH, target: ExitTarget::UseGPS, exit_type: ExitType::Free }, ) }, ).into_iter().collect()) } static STATIC_ROOM_MAP_BY_CODE: OnceCell> = OnceCell::new(); pub fn room_map_by_code() -> &'static BTreeMap<&'static str, &'static Room> { STATIC_ROOM_MAP_BY_CODE.get_or_init( || room_list().iter().map(|r| (r.code, r)).collect()) } static STATIC_ROOM_MAP_BY_ZLOC: OnceCell> = OnceCell::new(); pub fn room_map_by_zloc() -> &'static BTreeMap<(&'static str, &'static GridCoords), &'static Room> { STATIC_ROOM_MAP_BY_ZLOC.get_or_init( || room_list().iter() .map(|r| ((r.zone, &r.grid_coords), r)) .chain(room_list().iter() .flat_map(|r| r.secondary_zones.iter() .map(|sz| ((sz.zone, &sz.grid_coords), r)) .collect::>())) .collect()) } pub fn room_static_items() -> Box> { Box::new(room_list().iter().map(|r| StaticItem { item_code: r.code, initial_item: Box::new(|| Item { item_code: r.code.to_owned(), item_type: "room".to_owned(), display: r.name.to_owned(), details: Some(r.description.to_owned()), details_less_explicit: r.description_less_explicit.map(|d|d.to_owned()), location: format!("zone/{}", r.zone), is_static: true, ..Item::default() }) })) } pub fn resolve_exit(room: &Room, exit: &Exit) -> Option<&'static Room> { match exit.target { ExitTarget::Custom(t) => t.split_once("/").and_then( |(t,c)| if t != "room" { None } else { room_map_by_code().get(c).map(|r|*r) }), ExitTarget::UseGPS => room_map_by_zloc().get(&(room.zone, &room.grid_coords.apply(&exit.direction))).map(|r|*r) } } #[cfg(test)] mod test { use super::*; #[test] fn room_zones_should_exist() { for room in room_list() { zone_details().get(room.zone).expect( &format!("zone {} for room {} should exist", room.zone, room.code)); } } #[test] fn room_map_by_code_should_have_repro_xv_chargen() { assert_eq!(room_map_by_code().get("repro_xv_chargen").expect("repro_xv_chargen to exist").code, "repro_xv_chargen"); } }