966 lines
30 KiB
Rust
966 lines
30 KiB
Rust
use super::{
|
|
journals::award_journal_if_needed, possession_type::PossessionType, scavtable::ScavtableType,
|
|
StaticItem,
|
|
};
|
|
#[double]
|
|
use crate::db::DBTrans;
|
|
use crate::{
|
|
message_handler::user_commands::{CommandHandlingError, UResult, VerbContext},
|
|
models::{
|
|
effect::SimpleEffect,
|
|
item::{DoorState, Item, ItemFlag},
|
|
journal::JournalType,
|
|
user::WristpadHack,
|
|
},
|
|
regular_tasks::queued_command::QueuedCommandContext,
|
|
services::room_effects::{RoomEffectEntryTrigger, RoomEffectExitTrigger},
|
|
DResult,
|
|
};
|
|
use ansi_markup::parse_ansi_markup;
|
|
use async_trait::async_trait;
|
|
use mockall_double::double;
|
|
use once_cell::sync::OnceCell;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
mod chonkers;
|
|
mod cok_murl;
|
|
pub mod computer_museum;
|
|
pub mod general_hospital;
|
|
pub mod melbs;
|
|
pub mod melbs_sewers;
|
|
pub mod northern_radfields;
|
|
mod repro_xv;
|
|
mod special;
|
|
|
|
pub struct Zone {
|
|
pub code: &'static str,
|
|
pub display: &'static str,
|
|
pub outdoors: bool,
|
|
}
|
|
|
|
static STATIC_ZONE_DETAILS: OnceCell<BTreeMap<&'static str, Zone>> = OnceCell::new();
|
|
pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> {
|
|
STATIC_ZONE_DETAILS.get_or_init(|| {
|
|
vec![
|
|
Zone {
|
|
code: "special",
|
|
display: "Outside of Time",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "melbs",
|
|
display: "Melbs",
|
|
outdoors: true,
|
|
},
|
|
Zone {
|
|
code: "repro_xv",
|
|
display: "Reprolabs XV",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "cok_murl",
|
|
display: "CoK-Murlison Complex",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "chonkers",
|
|
display: "Chonker's Gym",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "computer_museum",
|
|
display: "Computer Museum",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "general_hospital",
|
|
display: "General Hospital",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "melbs_sewers",
|
|
display: "Melbs Sewers",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "kings_office",
|
|
display: "King's Office",
|
|
outdoors: false,
|
|
},
|
|
Zone {
|
|
code: "northern_radfields",
|
|
display: "Northern Radfields",
|
|
outdoors: false,
|
|
},
|
|
]
|
|
.into_iter()
|
|
.map(|x| (x.code, x))
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug, Serialize, Deserialize)]
|
|
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 QueuedCommandContext,
|
|
exit: &Exit,
|
|
) -> UResult<bool>;
|
|
}
|
|
|
|
pub enum ExitType {
|
|
Free, // Anyone can just walk it (subject to any door logic).
|
|
Blocked(Box<dyn ExitBlocker + Sync + Send>), // Custom code about who can pass.
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
|
|
pub enum Direction {
|
|
NORTH,
|
|
SOUTH,
|
|
EAST,
|
|
WEST,
|
|
NORTHEAST,
|
|
SOUTHEAST,
|
|
NORTHWEST,
|
|
SOUTHWEST,
|
|
UP,
|
|
DOWN,
|
|
IN { item: String },
|
|
}
|
|
impl Serialize for Direction {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
Serialize::serialize(&self.describe(), serializer)
|
|
}
|
|
}
|
|
impl<'de> Deserialize<'de> for Direction {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
Deserialize::deserialize(deserializer).and_then(|s: String| {
|
|
Self::parse(s.as_str()).ok_or(serde::de::Error::custom("Invalid direction"))
|
|
})
|
|
}
|
|
}
|
|
|
|
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 } => "in ".to_owned() + item,
|
|
}
|
|
}
|
|
|
|
pub fn describe_climb(self: &Self, climb_dir: &str) -> String {
|
|
match self {
|
|
Direction::NORTH => format!("{} to the north", climb_dir),
|
|
Direction::SOUTH => format!("{} to the south", climb_dir),
|
|
Direction::EAST => format!("{} to the east", climb_dir),
|
|
Direction::WEST => format!("{} to the west", climb_dir),
|
|
Direction::NORTHEAST => format!("{} to the northeast", climb_dir),
|
|
Direction::SOUTHEAST => format!("{} to the southeast", climb_dir),
|
|
Direction::NORTHWEST => format!("{} to the northwest", climb_dir),
|
|
Direction::SOUTHWEST => format!("{} to the southwest", climb_dir),
|
|
Direction::UP => "upwards".to_owned(),
|
|
Direction::DOWN => "downwards".to_owned(),
|
|
Direction::IN { item } => format!("{} and in ", item),
|
|
}
|
|
}
|
|
|
|
pub fn parse(input: &str) -> Option<Direction> {
|
|
if input.starts_with("in ") {
|
|
return Some(Direction::IN {
|
|
item: input["in ".len()..].trim().to_owned(),
|
|
});
|
|
}
|
|
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::NORTHWEST),
|
|
"southwest" | "sw" => Some(Direction::SOUTHWEST),
|
|
"up" => Some(Direction::UP),
|
|
"down" => Some(Direction::DOWN),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn reverse(&self) -> Option<Direction> {
|
|
match self {
|
|
Direction::NORTH => Some(Direction::SOUTH),
|
|
Direction::SOUTH => Some(Direction::NORTH),
|
|
Direction::EAST => Some(Direction::WEST),
|
|
Direction::WEST => Some(Direction::EAST),
|
|
Direction::NORTHEAST => Some(Direction::SOUTHWEST),
|
|
Direction::SOUTHEAST => Some(Direction::NORTHWEST),
|
|
Direction::NORTHWEST => Some(Direction::SOUTHEAST),
|
|
Direction::SOUTHWEST => Some(Direction::NORTHEAST),
|
|
Direction::UP => Some(Direction::DOWN),
|
|
Direction::DOWN => Some(Direction::UP),
|
|
Direction::IN { .. } => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Eq, Ord, Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)]
|
|
pub enum ExitTarget {
|
|
UseGPS,
|
|
Custom(String),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct ExitClimb {
|
|
// Negative if it is down.
|
|
pub height: i64,
|
|
pub difficulty: i64,
|
|
}
|
|
|
|
pub struct Exit {
|
|
pub direction: Direction,
|
|
pub target: ExitTarget,
|
|
pub exit_type: ExitType,
|
|
pub exit_climb: Option<ExitClimb>,
|
|
}
|
|
|
|
impl Default for Exit {
|
|
fn default() -> Self {
|
|
Self {
|
|
direction: Direction::NORTH,
|
|
target: ExitTarget::UseGPS,
|
|
exit_type: ExitType::Free,
|
|
exit_climb: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(default)]
|
|
pub struct ScanBlockerInfo {
|
|
need_scancode: ScanCode,
|
|
block_message: String,
|
|
}
|
|
|
|
impl Default for ScanBlockerInfo {
|
|
fn default() -> Self {
|
|
Self {
|
|
need_scancode: ScanCode::SewerAccess,
|
|
block_message: "You aren't allowed in there".to_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ExitBlocker for ScanBlockerInfo {
|
|
async fn attempt_exit(&self, ctx: &mut QueuedCommandContext, _exit: &Exit) -> UResult<bool> {
|
|
if ctx.item.item_type != "player" {
|
|
return Ok(false);
|
|
}
|
|
let user = ctx
|
|
.trans
|
|
.find_by_username(&ctx.item.item_code)
|
|
.await?
|
|
.ok_or_else(|| CommandHandlingError::UserError("No user exists".to_owned()))?;
|
|
if !user.scan_codes.contains(&self.need_scancode) {
|
|
if let Some((sess, _sess_dat)) = ctx
|
|
.trans
|
|
.find_session_for_player(&ctx.item.item_code)
|
|
.await?
|
|
{
|
|
ctx.trans
|
|
.queue_for_session(&sess, Some(&format!("{}\n", &self.block_message)))
|
|
.await?;
|
|
}
|
|
return Ok(false);
|
|
}
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct NpcBlockerInfo {
|
|
block_message: String,
|
|
}
|
|
#[async_trait]
|
|
impl ExitBlocker for NpcBlockerInfo {
|
|
async fn attempt_exit(&self, ctx: &mut QueuedCommandContext, _exit: &Exit) -> UResult<bool> {
|
|
if ctx.item.item_type != "player" {
|
|
return Ok(false);
|
|
}
|
|
if ctx
|
|
.trans
|
|
.count_items_by_location_type(&ctx.item.location, "npc")
|
|
.await?
|
|
== 0
|
|
{
|
|
return Ok(true);
|
|
}
|
|
|
|
if let Some((sess, _sess_dat)) = ctx
|
|
.trans
|
|
.find_session_for_player(&ctx.item.item_code)
|
|
.await?
|
|
{
|
|
ctx.trans
|
|
.queue_for_session(&sess, Some(&format!("{}\n", &self.block_message)))
|
|
.await?;
|
|
}
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(default)]
|
|
pub struct SimpleExit {
|
|
pub direction: Direction,
|
|
pub target: ExitTarget,
|
|
pub exit_climb: Option<ExitClimb>,
|
|
pub needs_scan: Option<ScanBlockerInfo>,
|
|
pub needs_npc_cleared: Option<NpcBlockerInfo>,
|
|
}
|
|
|
|
impl Default for SimpleExit {
|
|
fn default() -> Self {
|
|
Self {
|
|
direction: Direction::NORTH,
|
|
target: ExitTarget::UseGPS,
|
|
exit_climb: None,
|
|
needs_scan: None,
|
|
needs_npc_cleared: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Into<Exit> for SimpleExit {
|
|
fn into(self: SimpleExit) -> Exit {
|
|
Exit {
|
|
direction: self.direction,
|
|
target: self.target,
|
|
exit_type: match self.needs_scan {
|
|
None => match self.needs_npc_cleared {
|
|
None => ExitType::Free,
|
|
Some(s) => ExitType::Blocked(Box::new(NpcBlockerInfo {
|
|
block_message: parse_ansi_markup(&s.block_message).unwrap(),
|
|
..s
|
|
})),
|
|
},
|
|
Some(s) => ExitType::Blocked(Box::new(ScanBlockerInfo {
|
|
block_message: parse_ansi_markup(&s.block_message).unwrap(),
|
|
..s
|
|
})),
|
|
},
|
|
exit_climb: self.exit_climb,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct SecondaryZoneRecord {
|
|
pub zone: String,
|
|
pub short: String,
|
|
pub grid_coords: GridCoords,
|
|
pub caption: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(default)]
|
|
pub struct RoomStock {
|
|
pub possession_type: PossessionType,
|
|
pub list_price: u64,
|
|
pub poverty_discount: bool,
|
|
pub can_buy: bool,
|
|
pub can_sell: Option<u64>, // sell price in hundredths of a percent of the buy price, e.g. 8000 = 80% of buy price back.
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum ScanCode {
|
|
SewerAccess,
|
|
RedImperialCode,
|
|
}
|
|
|
|
impl Default for RoomStock {
|
|
fn default() -> Self {
|
|
Self {
|
|
possession_type: PossessionType::AntennaWhip,
|
|
list_price: 1000000000,
|
|
poverty_discount: false,
|
|
can_buy: true,
|
|
can_sell: Some(8000),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub enum RentSuiteType {
|
|
Residential,
|
|
Commercial,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct RentInfo {
|
|
pub rent_what: String,
|
|
pub suite_type: RentSuiteType,
|
|
pub dynzone: super::dynzone::DynzoneType,
|
|
pub daily_price: u64,
|
|
pub setup_fee: u64,
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub enum MaterialType {
|
|
Normal,
|
|
WaterSurface,
|
|
Underwater,
|
|
Soft { damage_modifier: u64 },
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait RoomEnterTrigger {
|
|
async fn handle_enter(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>;
|
|
}
|
|
#[async_trait]
|
|
pub trait RoomExitTrigger {
|
|
async fn handle_exit(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>;
|
|
}
|
|
#[async_trait]
|
|
pub trait RoomSellTrigger {
|
|
async fn handle_sell(
|
|
&self,
|
|
ctx: &mut VerbContext,
|
|
room: &Room,
|
|
player_item: &Item,
|
|
sell_item: &Item,
|
|
) -> UResult<()>;
|
|
}
|
|
|
|
pub struct Room {
|
|
pub zone: String,
|
|
// Other zones where it can be seen on the map and accessed.
|
|
pub secondary_zones: Vec<SecondaryZoneRecord>,
|
|
pub code: String,
|
|
pub name: String,
|
|
pub short: String,
|
|
pub grid_coords: GridCoords,
|
|
pub description: String,
|
|
pub exits: Vec<Exit>,
|
|
pub should_caption: bool,
|
|
pub repel_npc: bool,
|
|
pub item_flags: Vec<ItemFlag>,
|
|
// Empty means not a shop.
|
|
pub stock_list: Vec<RoomStock>,
|
|
// What can be rented here...
|
|
pub rentable_dynzone: Vec<RentInfo>,
|
|
pub material_type: MaterialType,
|
|
pub has_power: bool,
|
|
pub door_states: Option<BTreeMap<Direction, DoorState>>,
|
|
pub wristpad_hack_allowed: Option<WristpadHack>,
|
|
pub scan_code: Option<ScanCode>,
|
|
pub journal: Option<JournalType>,
|
|
pub enter_trigger: Option<Box<dyn RoomEnterTrigger + Sync + Send>>,
|
|
pub exit_trigger: Option<Box<dyn RoomExitTrigger + Sync + Send>>,
|
|
pub sell_trigger: Option<Box<dyn RoomSellTrigger + Sync + Send>>,
|
|
pub scavtable: ScavtableType,
|
|
}
|
|
|
|
impl Default for Room {
|
|
fn default() -> Self {
|
|
Self {
|
|
zone: "default".to_owned(),
|
|
secondary_zones: vec![],
|
|
code: "default".to_owned(),
|
|
name: "default".to_owned(),
|
|
short: "DF".to_owned(),
|
|
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
|
description: "default".to_owned(),
|
|
exits: vec![],
|
|
should_caption: true,
|
|
repel_npc: false,
|
|
item_flags: vec![],
|
|
stock_list: vec![],
|
|
rentable_dynzone: vec![],
|
|
material_type: MaterialType::Normal,
|
|
has_power: false,
|
|
door_states: None,
|
|
wristpad_hack_allowed: None,
|
|
scan_code: None,
|
|
journal: None,
|
|
enter_trigger: None,
|
|
exit_trigger: None,
|
|
sell_trigger: None,
|
|
scavtable: ScavtableType::Nothing,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct SimpleRoom<T> {
|
|
pub zone: String,
|
|
// Other zones where it can be seen on the map and accessed.
|
|
pub secondary_zones: Vec<SecondaryZoneRecord>,
|
|
pub code: String,
|
|
pub name: String,
|
|
pub short: String,
|
|
pub grid_coords: GridCoords,
|
|
pub description: String,
|
|
pub exits: Vec<SimpleExit>,
|
|
pub should_caption: bool,
|
|
pub repel_npc: bool,
|
|
pub item_flags: Vec<ItemFlag>,
|
|
// Empty means not a shop.
|
|
pub stock_list: Vec<RoomStock>,
|
|
// What can be rented here...
|
|
pub rentable_dynzone: Vec<RentInfo>,
|
|
pub material_type: MaterialType,
|
|
pub has_power: bool,
|
|
pub door_states: Option<BTreeMap<Direction, DoorState>>,
|
|
pub wristpad_hack_allowed: Option<WristpadHack>,
|
|
pub scan_code: Option<ScanCode>,
|
|
pub journal: Option<JournalType>,
|
|
pub scavtable: ScavtableType,
|
|
pub effects: Option<Vec<SimpleEffect>>,
|
|
pub extra: T,
|
|
}
|
|
|
|
impl<T> Into<Room> for SimpleRoom<T> {
|
|
fn into(self: SimpleRoom<T>) -> Room {
|
|
Room {
|
|
zone: parse_ansi_markup(&self.zone).unwrap(),
|
|
secondary_zones: self
|
|
.secondary_zones
|
|
.into_iter()
|
|
.map(|sz| SecondaryZoneRecord {
|
|
zone: sz.zone,
|
|
short: parse_ansi_markup(&sz.short).unwrap(),
|
|
grid_coords: sz.grid_coords,
|
|
caption: sz.caption,
|
|
})
|
|
.collect(),
|
|
code: self.code,
|
|
name: parse_ansi_markup(&self.name).unwrap(),
|
|
short: parse_ansi_markup(&self.short).unwrap(),
|
|
grid_coords: self.grid_coords,
|
|
description: parse_ansi_markup(&self.description).unwrap(),
|
|
exits: self.exits.into_iter().map(|e| e.into()).collect(),
|
|
should_caption: self.should_caption,
|
|
repel_npc: self.repel_npc,
|
|
item_flags: self.item_flags,
|
|
stock_list: self.stock_list,
|
|
rentable_dynzone: self.rentable_dynzone,
|
|
material_type: self.material_type,
|
|
has_power: self.has_power,
|
|
door_states: self.door_states,
|
|
wristpad_hack_allowed: self.wristpad_hack_allowed,
|
|
scan_code: self.scan_code,
|
|
journal: self.journal,
|
|
exit_trigger: self.effects.as_ref().map(|_fx| {
|
|
Box::new(RoomEffectExitTrigger)
|
|
as Box<(dyn RoomExitTrigger + std::marker::Send + Sync + 'static)>
|
|
}),
|
|
enter_trigger: self.effects.map(|fx| {
|
|
Box::new(RoomEffectEntryTrigger { effects: fx })
|
|
as Box<(dyn RoomEnterTrigger + std::marker::Send + Sync + 'static)>
|
|
}),
|
|
sell_trigger: None,
|
|
scavtable: self.scavtable,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, T: Default> Default for SimpleRoom<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
zone: "default".to_owned(),
|
|
secondary_zones: vec![],
|
|
code: "default".to_owned(),
|
|
name: "default".to_owned(),
|
|
short: "DF".to_owned(),
|
|
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
|
description: "default".to_owned(),
|
|
exits: vec![],
|
|
should_caption: true,
|
|
repel_npc: false,
|
|
item_flags: vec![],
|
|
stock_list: vec![],
|
|
rentable_dynzone: vec![],
|
|
material_type: MaterialType::Normal,
|
|
has_power: false,
|
|
door_states: None,
|
|
wristpad_hack_allowed: None,
|
|
scan_code: None,
|
|
journal: None,
|
|
scavtable: ScavtableType::Nothing,
|
|
effects: None,
|
|
extra: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
static STATIC_ROOM_LIST: OnceCell<Vec<Room>> = OnceCell::new();
|
|
pub fn room_list() -> &'static Vec<Room> {
|
|
STATIC_ROOM_LIST.get_or_init(|| {
|
|
let mut rooms = repro_xv::room_list();
|
|
rooms.append(&mut melbs::room_list());
|
|
rooms.append(&mut cok_murl::room_list());
|
|
rooms.append(&mut chonkers::room_list());
|
|
rooms.append(&mut special::room_list());
|
|
rooms.append(&mut computer_museum::room_list());
|
|
rooms.append(&mut general_hospital::room_list());
|
|
rooms.append(&mut melbs_sewers::room_list());
|
|
rooms.append(&mut northern_radfields::room_list());
|
|
rooms.into_iter().collect()
|
|
})
|
|
}
|
|
|
|
static STATIC_ROOM_MAP_BY_CODE: OnceCell<BTreeMap<&'static str, &'static Room>> = 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.as_str(), r)).collect())
|
|
}
|
|
|
|
static STATIC_ROOM_MAP_BY_ZLOC: OnceCell<
|
|
BTreeMap<(&'static str, &'static GridCoords), &'static Room>,
|
|
> = 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.as_str(), &r.grid_coords), r))
|
|
.chain(room_list().iter().flat_map(|r| {
|
|
r.secondary_zones
|
|
.iter()
|
|
.map(|sz| ((sz.zone.as_str(), &sz.grid_coords), r))
|
|
.collect::<Vec<((&'static str, &'static GridCoords), &'static Room)>>()
|
|
}))
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
|
Box::new(room_list().iter().map(|r| StaticItem {
|
|
item_code: r.code.clone(),
|
|
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()),
|
|
location: format!("zone/{}", r.zone),
|
|
is_static: true,
|
|
flags: r.item_flags.clone(),
|
|
door_states: r.door_states.clone(),
|
|
..Item::default()
|
|
}),
|
|
extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())),
|
|
}))
|
|
}
|
|
|
|
pub fn resolve_exit(room: &Room, exit: &Exit) -> Option<&'static Room> {
|
|
match exit.target {
|
|
ExitTarget::Custom(ref 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),
|
|
}
|
|
}
|
|
|
|
pub async fn refresh_room_exits(trans: &DBTrans, template: &Item) -> DResult<()> {
|
|
let mut target_mut = match trans
|
|
.find_item_by_type_code(&template.item_type, &template.item_code)
|
|
.await?
|
|
{
|
|
None => return Ok(()),
|
|
Some(v) => (*v).clone(),
|
|
};
|
|
let mut need_save: bool = false;
|
|
if let None = target_mut.door_states.as_ref() {
|
|
target_mut.door_states = Some(BTreeMap::new());
|
|
}
|
|
if let Some(target_states) = target_mut.door_states.as_mut() {
|
|
if let Some(template_states) = template.door_states.as_ref() {
|
|
let target_keys = target_states
|
|
.keys()
|
|
.map(|v| v.clone())
|
|
.collect::<BTreeSet<Direction>>();
|
|
let template_keys = template_states
|
|
.keys()
|
|
.map(|v| v.clone())
|
|
.collect::<BTreeSet<Direction>>();
|
|
for extra in &target_keys - &template_keys {
|
|
target_states.remove(&extra);
|
|
need_save = true;
|
|
}
|
|
for missing in &template_keys - &target_keys {
|
|
if let Some(template) = template_states.get(&missing) {
|
|
target_states.insert(missing, template.clone());
|
|
need_save = true;
|
|
}
|
|
}
|
|
for same in template_keys.intersection(&target_keys) {
|
|
if let Some(target) = target_states.get_mut(&same) {
|
|
if let Some(template) = template_states.get(&same) {
|
|
if &template.description != &target.description {
|
|
// We do this whole thing so we preserve the open state,
|
|
// but change the description.
|
|
target.description = template.description.clone();
|
|
need_save = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if need_save {
|
|
trans.save_item_model(&target_mut).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn check_for_exit_action(
|
|
ctx: &mut QueuedCommandContext<'_>,
|
|
exit_type: &str,
|
|
exit_code: &str,
|
|
) -> UResult<()> {
|
|
if exit_type != "room" {
|
|
return Ok(());
|
|
}
|
|
let room = match room_map_by_code().get(exit_code) {
|
|
Some(r) => r,
|
|
_ => return Ok(()),
|
|
};
|
|
match room.exit_trigger {
|
|
Some(ref trigger) => trigger.handle_exit(ctx, room).await?,
|
|
_ => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn check_for_enter_action(ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
|
|
let room_code = match ctx.item.location.split_once("/") {
|
|
Some((loc_type, _)) if loc_type != "room" => return Ok(()),
|
|
Some((_, room_code)) => room_code,
|
|
_ => return Ok(()),
|
|
};
|
|
let room = match room_map_by_code().get(room_code) {
|
|
Some(r) => r,
|
|
_ => return Ok(()),
|
|
};
|
|
match room.journal {
|
|
Some(ref journal) if ctx.item.item_type == "player" => {
|
|
if let Some(mut user) = ctx.trans.find_by_username(&ctx.item.item_code).await? {
|
|
if award_journal_if_needed(&ctx.trans, &mut user, &mut ctx.item, journal.clone())
|
|
.await?
|
|
{
|
|
ctx.trans.save_user_model(&user).await?;
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
match room.enter_trigger {
|
|
Some(ref trigger) => trigger.handle_enter(ctx, room).await?,
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::super::scavtable::scavtable_map;
|
|
use super::*;
|
|
use itertools::Itertools;
|
|
|
|
#[test]
|
|
fn room_zones_should_exist() {
|
|
for room in room_list() {
|
|
zone_details().get(room.zone.as_str()).expect(&format!(
|
|
"zone {} for room {} should exist",
|
|
room.zone, room.code
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn room_shorts_should_be_display_length_2() {
|
|
assert_eq!(
|
|
room_list()
|
|
.iter()
|
|
.map(|r| (
|
|
r.code.as_str(),
|
|
ansi::strip_special_characters(r.short.as_str())
|
|
))
|
|
.filter(|(_c, s)| s.len() != 2)
|
|
.collect::<Vec<(&'static str, String)>>(),
|
|
vec![]
|
|
);
|
|
}
|
|
|
|
#[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"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grid_coords_should_be_unique_in_zone() {
|
|
let mut roomlist: Vec<&'static Room> = room_list().iter().collect();
|
|
roomlist
|
|
.sort_unstable_by(|a, b| a.grid_coords.cmp(&b.grid_coords).then(a.zone.cmp(&b.zone)));
|
|
let dups: Vec<Vec<(&'static str, &GridCoords, &'static str)>> = roomlist
|
|
.iter()
|
|
.group_by(|x| (&x.grid_coords, x.zone.as_str()))
|
|
.into_iter()
|
|
.map(|((coord, zone), rg)| {
|
|
rg.map(|r| (r.name.as_str(), coord, zone))
|
|
.collect::<Vec<(&str, &GridCoords, &str)>>()
|
|
})
|
|
.filter(|x| x.len() > 1)
|
|
.collect();
|
|
assert_eq!(dups, Vec::<Vec<(&str, &GridCoords, &str)>>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_direction_should_work() {
|
|
assert_eq!(Direction::parse("southeast"), Some(Direction::SOUTHEAST));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_and_describe_direction_should_roundtrip() {
|
|
let examples = vec![
|
|
"north",
|
|
"south",
|
|
"east",
|
|
"west",
|
|
"northeast",
|
|
"southeast",
|
|
"northwest",
|
|
"southwest",
|
|
"up",
|
|
"down",
|
|
"in there",
|
|
];
|
|
for example in examples {
|
|
assert_eq!(
|
|
Direction::parse(example).map(|v| v.describe()),
|
|
Some(example.to_owned())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rooms_reference_valid_scavtables() {
|
|
let bad_scav_rooms: Vec<String> = room_list()
|
|
.iter()
|
|
.filter_map(|r| match scavtable_map().get(&r.scavtable) {
|
|
Some(_) => None,
|
|
None => Some(r.code.clone()),
|
|
})
|
|
.collect::<Vec<String>>();
|
|
assert_eq!(bad_scav_rooms, Vec::<String>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn exits_resolve() {
|
|
let rl = room_list();
|
|
let unresolved_exits: Vec<String> = rl
|
|
.iter()
|
|
.flat_map(|r: &'static Room| {
|
|
let r2: &'static Room = r;
|
|
r.exits.iter().map(move |ex| (r2, ex))
|
|
})
|
|
.filter_map(|(r, ex)| match resolve_exit(r, ex) {
|
|
Some(_) => None,
|
|
None => Some(format!("{} {}", r.code, &ex.direction.describe())),
|
|
})
|
|
.collect::<Vec<String>>();
|
|
assert_eq!(unresolved_exits, Vec::<String>::new());
|
|
}
|
|
}
|