Add more content + concept of fixed items.

This commit is contained in:
Condorra 2023-01-01 00:09:25 +11:00
parent 92814a4175
commit 1c3d2456a4
10 changed files with 455 additions and 36 deletions

View File

@ -10,11 +10,13 @@ use once_cell::sync::OnceCell;
use std::sync::Arc;
mod agree;
mod describe;
mod help;
mod ignore;
mod less_explicit_mode;
mod login;
mod look;
mod movement;
pub mod parsing;
mod quit;
mod register;
@ -71,8 +73,31 @@ static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
};
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
// Movement comments first:
"north" => movement::VERB,
"n" => movement::VERB,
"northeast" => movement::VERB,
"ne" => movement::VERB,
"east" => movement::VERB,
"e" => movement::VERB,
"southeast" => movement::VERB,
"se" => movement::VERB,
"south" => movement::VERB,
"s" => movement::VERB,
"southwest" => movement::VERB,
"sw" => movement::VERB,
"west" => movement::VERB,
"w" => movement::VERB,
"northwest" => movement::VERB,
"nw" => movement::VERB,
"up" => movement::VERB,
"down" => movement::VERB,
// Other commands (alphabetical except aliases grouped):
"describe" => describe::VERB,
"l" => look::VERB,
"look" => look::VERB,
"read" => look::VERB,
"-" => whisper::VERB,
"whisper" => whisper::VERB,
};
@ -164,7 +189,7 @@ pub fn get_user_or_fail_mut<'l>(ctx: &'l mut VerbContext) -> UResult<&'l mut Use
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
Ok(ctx.trans.find_item_by_type_code(
"player", &get_user_or_fail(ctx)?.username.to_lowercase()).await?
.ok_or_else(|| UserError("Your player is gone, you'll need to re-register or ask an admin".to_owned()))?)
.ok_or_else(|| UserError("Your character is gone, you'll need to re-register or ask an admin".to_owned()))?)
}
pub async fn search_item_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) ->

View File

@ -0,0 +1,41 @@
use super::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
parsing::parse_to_space,
user_error,
get_player_item_or_fail
};
use async_trait::async_trait;
use ansi::{ansi, ignore_special_characters};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (me, remaining) = parse_to_space(remaining);
let (as_word, remaining) = parse_to_space(remaining);
let remaining = ignore_special_characters(remaining.trim());
if me != "me" || as_word != "as" || remaining == "" {
user_error(ansi!("Try <bold>describe me as <lt>something><reset>").to_owned())?;
}
if remaining.len() < 40 {
user_error(format!("That's too short by {} characters.", 40 - remaining.len()))?;
}
if remaining.len() > 255 {
user_error(format!("That's too short by {} characters.", remaining.len() - 255))?;
}
let mut item = (*get_player_item_or_fail(ctx).await?).clone();
item.details = Some(remaining);
ctx.trans.save_item_model(&item).await?;
ctx.trans.queue_for_session(ctx.session, Some(ansi!("<green>Character description updated.<reset>\n"))).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -23,7 +23,14 @@ pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
} else {
buf.push_str(room::room_map_by_zloc()
.get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z }))
.map(|r| r.short)
.map(|r| if room.zone == r.zone {
r.short
} else {
r.secondary_zones.iter()
.find(|sz| sz.zone == room.zone)
.map(|sz| sz.short)
.expect("Secondary zone missing")
})
.unwrap_or(" "));
}
}
@ -55,8 +62,8 @@ pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item,
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
ctx.trans.queue_for_session(
ctx.session,
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
&word_wrap(&format!("{} ({})\n{}.{}\n{}\n",
Some(&flow_around(&render_map(room, 5, 5), 10, ansi!("<reset> "),
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat),
zone,
item.details_for_session(

View File

@ -0,0 +1,48 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail,
look
};
use async_trait::async_trait;
use crate::static_content::room::{self, Direction, ExitType};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> {
let dir = Direction::parse(verb).ok_or_else(|| UserError("Unknown direction".to_owned()))?;
if remaining.trim() != "" {
user_error("Movement commands don't take extra data at the end.".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
// Fix this when we have planes / boats / roomkits.
user_error("Navigating outside rooms not yet supported.".to_owned())?
}
let room = room::room_map_by_code().get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *dir)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
// Ideally we would queue if we were already moving rather than insta-move.
match exit.exit_type {
ExitType::Free => {}
ExitType::Blocked(blocker) => {
if !blocker.attempt_exit(ctx, &player_item, exit).await? {
return Ok(());
}
}
}
let new_room =
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
let mut new_player_item = (*player_item).clone();
new_player_item.location = format!("{}/{}", "room", new_room.code);
ctx.trans.save_item_model(&new_player_item).await?;
look::VERB.handle(ctx, verb, remaining).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,7 +1,6 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use std::collections::BTreeMap;
use crate::static_content::npc::statbot::StatbotState;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UserTermData {
@ -76,7 +75,6 @@ pub struct User {
pub terms: UserTermData,
pub experience: UserExperienceData,
pub statbot: Option<StatbotState>,
pub raw_skills: BTreeMap<SkillType, u16>,
pub raw_stats: BTreeMap<StatType, u16>,
// Reminder: Consider backwards compatibility when updating this. New fields should generally
@ -115,7 +113,6 @@ impl Default for User {
banned_until: None,
abandoned_at: None,
chargen_last_completed_at: None,
statbot: None,
terms: UserTermData::default(),
experience: UserExperienceData::default(),

View File

@ -6,6 +6,7 @@ use log::info;
pub mod room;
pub mod npc;
mod fixed_item;
pub struct StaticItem {
pub item_code: &'static str,
@ -28,6 +29,10 @@ fn static_item_registry() -> Vec<StaticItemTypeGroup> {
item_type: "room",
items: || room::room_static_items()
},
StaticItemTypeGroup {
item_type: "fixed_item",
items: || fixed_item::static_items()
},
)
}

View File

@ -0,0 +1,60 @@
// For things like signs that don't do much except stay where they are and carry a description.
use super::StaticItem;
use once_cell::sync::OnceCell;
use crate::models::item::{Item, Pronouns};
use ansi::ansi;
pub struct FixedItem {
pub code: &'static str,
pub name: &'static str,
pub description: &'static str,
pub description_less_explicit: Option<&'static str>,
pub location: &'static str,
pub proper_noun: bool
}
fn fixed_item_list() -> &'static Vec<FixedItem> {
static FIXED_ITEM_LIST: OnceCell<Vec<FixedItem>> = OnceCell::new();
FIXED_ITEM_LIST.get_or_init(|| vec!(
FixedItem {
code: "repro_xv_updates_red_poster",
name: ansi!("red poster"),
description:
"A larger faded poster with a thick red border. It says:\n\
\t\"Dear newly memory wiped citizen! Welcome to Melbs! A lot \
has changed since the memories your implant is based on were \
created. The global Gazos-Murlison Co empire fell in a nuclear \
attack, and most cities of the world were destroyed. \
A few cities around Australia, like this one, took some fallout \
but survived. The few remaining cities are now all independently \
run. I was a young governor under the empire, and I now rule inner \
Melbs as the King. I have gotten all the fallout out from the inner city, \
and I have a robot police force to keep you safe from the worst baddies, \
but be warned - there still are some dangers near by, and the world \
further out, outside my realm, is a dangerous and radioactive place.\"",
description_less_explicit: None,
location: "room/repro_xv_updates",
proper_noun: false
}
))
}
pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
Box::new(fixed_item_list().iter().map(|r| StaticItem {
item_code: r.code,
initial_item: Box::new(|| Item {
item_code: r.code.to_owned(),
item_type: "fixed_item".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: r.location.to_owned(),
is_static: true,
pronouns: Pronouns {
is_proper: r.proper_noun,
..Pronouns::default_inanimate()
},
..Item::default()
})
}))
}

View File

@ -1,4 +1,5 @@
use super::NPCMessageHandler;
use super::super::room::{ExitBlocker, Exit};
use crate::message_handler::user_commands::{
VerbContext, UResult,
get_user_or_fail,
@ -12,12 +13,11 @@ use crate::models::{
session::Session
};
use ansi::ansi;
use serde::{Serialize, Deserialize};
use nom::character::complete::u8;
pub struct StatbotMessageHandler;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum StatbotState {
Brains,
Senses,
@ -40,6 +40,15 @@ async fn reply(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> {
Ok(())
}
async fn shout(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> {
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(ansi!("Statbot shouts in a stern mechanical voice: <red>\"{}\"<reset>\n"),
msg))
).await?;
Ok(())
}
fn work_out_state(user: &User, item: &Item) -> StatbotState {
if !user.raw_stats.contains_key(&StatType::Brains) {
return StatbotState::Brains;
@ -81,7 +90,7 @@ fn points_left(user: &User) -> u16 {
(62 - (brn + sen + brw + refl + end + col) as i16).max(0) as u16
}
fn next_action_text(session: &Session, user: &User) -> String {
fn next_action_text(session: &Session, user: &User, item: &Item) -> String {
let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8);
let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8);
let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8);
@ -90,12 +99,11 @@ fn next_action_text(session: &Session, user: &User) -> String {
let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8);
let summary = format!("Brains: {}, Senses: {}, Brawn: {}, Reflexes: {}, Endurance: {}, Cool: {}. To spend: {}", brn, sen, brw, refl, end, col, points_left(user));
let st = user.statbot.as_ref().unwrap_or(&StatbotState::Brains);
let st = work_out_state(user, item);
match st {
StatbotState::Brains => ansi!(
"I am Statbot, a robot servant of the empire, put here to help you choose \
how your body will function. The base body has 8 each of brains, senses, \
"The base body has 8 each of brains, senses, \
brawn, reflexes, endurance and cool - but you get 14 points of improvement. \
Each point spent lifts that stat by one. Your first job is to choose how much \
brainpower you will have. If you choose 8, you don't spend any points. There \
@ -202,14 +210,13 @@ async fn stat_command(ctx: &mut VerbContext<'_>, item: &Item,
{
let user_mut = get_user_or_fail_mut(ctx)?;
user_mut.raw_stats.insert(stat.clone(), statno as u16);
user_mut.statbot = Some(work_out_state(user_mut, item));
}
let user: &User = get_user_or_fail(ctx)?;
ctx.trans.save_user_model(user).await?;
let mut item_updated = item.clone();
item_updated.total_stats = user.raw_stats.clone();
ctx.trans.save_item_model(&item_updated).await?;
reply(ctx, &next_action_text(&ctx.session_dat, user)).await?;
reply(ctx, &next_action_text(&ctx.session_dat, user, &item_updated)).await?;
}
}
@ -234,14 +241,9 @@ async fn sex_command(ctx: &mut VerbContext<'_>, item: &Item,
Sex::Female => Pronouns::default_female()
};
item_updated.sex = Some(choice);
{
let user_mut = get_user_or_fail_mut(ctx)?;
user_mut.statbot = Some(work_out_state(user_mut, &item_updated));
}
let user: &User = get_user_or_fail(ctx)?;
ctx.trans.save_user_model(user).await?;
ctx.trans.save_item_model(&item_updated).await?;
reply(ctx, &next_action_text(&ctx.session_dat, user)).await?;
reply(ctx, &next_action_text(&ctx.session_dat, user, &item_updated)).await?;
Ok(())
}
@ -264,9 +266,30 @@ impl NPCMessageHandler for StatbotMessageHandler {
"cool" | "col" => stat_command(ctx, source, &StatType::Cool, &arg).await?,
"sex" => sex_command(ctx, source, &arg).await?,
_ => {
reply(ctx, &next_action_text(&ctx.session_dat, get_user_or_fail(ctx)?)).await?;
reply(ctx, &next_action_text(&ctx.session_dat, get_user_or_fail(ctx)?, source)).await?;
}
}
Ok(())
}
}
pub struct ChoiceRoomBlocker;
#[async_trait]
impl ExitBlocker for ChoiceRoomBlocker {
// 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<bool> {
let user = get_user_or_fail(ctx)?;
if work_out_state(user, player) == StatbotState::Done {
Ok(true)
} else {
shout(ctx, &format!(ansi!("YOU SHALL NOT PASS UNTIL YOU DO AS I SAY! <blue>{}"),
&next_action_text(&ctx.session_dat, user, player))).await?;
Ok(false)
}
}
}

View File

@ -2,6 +2,10 @@ use super::StaticItem;
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use ansi::ansi;
use async_trait::async_trait;
use crate::message_handler::user_commands::{
UResult, VerbContext
};
use crate::models::item::Item;
pub struct Zone {
@ -48,8 +52,20 @@ impl GridCoords {
}
}
#[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<bool>;
}
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...
}
@ -96,6 +112,8 @@ impl Direction {
"southeast" | "se" => Some(&Direction::SOUTHEAST),
"northwest" | "nw" => Some(&Direction::NORTHEAST),
"southwest" | "sw" => Some(&Direction::SOUTHWEST),
"up" => Some(&Direction::UP),
"down" => Some(&Direction::DOWN),
_ => None
}
}
@ -103,18 +121,25 @@ impl Direction {
pub enum ExitTarget {
UseGPS,
#[allow(dead_code)]
Custom(&'static str)
}
pub struct Exit {
pub direction: Direction,
pub target: ExitTarget,
pub exit_type: ExitType
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<SecondaryZoneRecord>,
pub code: &'static str,
pub name: &'static str,
pub short: &'static str,
@ -130,8 +155,9 @@ pub fn room_list() -> &'static Vec<Room> {
|| vec!(
Room {
zone: "repro_xv",
secondary_zones: vec!(),
code: "repro_xv_chargen",
name: ansi!("<yellow>Choice Room<reset>"),
name: ansi!("Choice Room"),
short: ansi!("<bgwhite><green>CR<reset>"),
description: ansi!(
"A room brightly lit in unnaturally white light, covered in sparkling \
@ -147,7 +173,26 @@ pub fn room_list() -> &'static Vec<Room> {
[Try <bold>-statbot hi<reset>, 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 },
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!("<bgwhite><green>UC<reset>"),
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. <bold>[Hint: Try reading the posters here.]<reset>"),
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: -1 },
exits: vec!(Exit {
direction: Direction::EAST,
target: ExitTarget::UseGPS,
@ -156,8 +201,9 @@ pub fn room_list() -> &'static Vec<Room> {
},
Room {
zone: "repro_xv",
secondary_zones: vec!(),
code: "repro_xv_respawn",
name: ansi!("<yellow>Body Factory<reset>"),
name: ansi!("Body Factory"),
short: ansi!("<bgmagenta><white>BF<reset>"),
description: ansi!(
"A room filled with glass vats full of clear fluids, with bodies of \
@ -166,8 +212,169 @@ pub fn room_list() -> &'static Vec<Room> {
have no body. But you sense you could go <bold>up<reset> and attach \
your memories to a body matching your current stats"),
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: 1 },
exits: vec!()
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!("<bgmagenta><white>RL<reset>"),
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_500"),
exit_type: ExitType::Free
})
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_kingst_200",
name: "King Street - 200 block",
short: ansi!("<yellow>||<reset>"),
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::SOUTH,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
)
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_kingst_300",
name: "King Street - 300 block",
short: ansi!("<yellow>||<reset>"),
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_400",
name: "King Street - 400 block",
short: ansi!("<yellow>||<reset>"),
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: -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_500",
name: ansi!("King Street - 500 block"),
short: ansi!("<yellow>||<reset>"),
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::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_600",
name: "King Street - 600 block",
short: ansi!("<yellow>||<reset>"),
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: 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_700",
name: "King Street - 700 block",
short: ansi!("<yellow>||<reset>"),
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
},
)
},
).into_iter().collect())
}
@ -182,7 +389,13 @@ static STATIC_ROOM_MAP_BY_ZLOC: OnceCell<BTreeMap<(&'static str, &'static GridCo
&'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, &r.grid_coords), r)).collect())
|| 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::<Vec<((&'static str, &'static GridCoords), &'static Room)>>()))
.collect())
}
pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {

View File

@ -1,6 +1,6 @@
# Blastmud setting guidelines for developers
## The Empire (500-200 years ago)
About 500 years ago, a group of powerful tech corporation CEOs from around the world grew dissatisfied with the state of the
## The Empire (300-50 years ago)
About 300 years ago, a group of powerful tech corporation CEOs from around the world grew dissatisfied with the state of the
world. Nations were always fighting, wasting resources and creating an unstable business environment. Some countries decided
to nationalise assets, and others imposed heavy taxes to help the poor (which was a bad thing from the perspective of the
wealthy CEOs).
@ -43,8 +43,8 @@ there were not separate races any more. With no national boundaries or nationali
The Empire mostly allowed people to do their own thing, subject to the rules enforced by the wrist-pad and by a network
of automated defence systems that make AI-driven judgements and dispensed lethal justice.
## The Smiting Shadows (220-195 years ago)
About 220 years ago, a group of hackers discovered an archive of radical right-wing videos from before the Empire years,
## The Smiting Shadows (50-70 years ago)
About 70 years ago, a group of hackers discovered an archive of radical right-wing videos from before the Empire years,
and circulated them (which was not initially against the rules). They became radicalised by the videos, but were
frustrated by their inability to act to violently take down the Empire due to their wristbands. After years of
trying, they found a way to modify the wristpads to make them think everyone had consented to fight with them,
@ -53,7 +53,7 @@ and hence allow violence against anyone.
They immediately started building an underground city, and building and stockpiling conventional and nuclear
weapons, ready to take down the Empire.
About 201 years ago, the Emperor learned of the plans, and ordered that all wristpads be updated to block
About 51 years ago, the Emperor learned of the plans, and ordered that all wristpads be updated to block
the exploit, and also that those with already exploited wristpads be killed. The Smiting Shadows, now exposed,
went full on aggressive, but was losing the battle. Finally, backed into a corner, the Grand Umbra of the
Smiting Shadows (i.e. the leader) ordered that a barrage of nuclear warheads be launched to their targets all
@ -71,7 +71,7 @@ PCs). The ability to create new hacked wristpads for someone who doesn't already
to the patches released by the Empire in its last days (i.e. no player can have it), but many hostile
NPCs derive from these scattered Smiting Shadow members.
## The post-apocalyptic period (200 years ago - present)
## The post-apocalyptic period (50 years ago - present)
The Empire was smashed, and some smaller relatively disorganised governments rule local regions.
Much of the world is radioactive.