Add more content + concept of fixed items.
This commit is contained in:
parent
92814a4175
commit
1c3d2456a4
@ -10,11 +10,13 @@ use once_cell::sync::OnceCell;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod agree;
|
mod agree;
|
||||||
|
mod describe;
|
||||||
mod help;
|
mod help;
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod less_explicit_mode;
|
mod less_explicit_mode;
|
||||||
mod login;
|
mod login;
|
||||||
mod look;
|
mod look;
|
||||||
|
mod movement;
|
||||||
pub mod parsing;
|
pub mod parsing;
|
||||||
mod quit;
|
mod quit;
|
||||||
mod register;
|
mod register;
|
||||||
@ -71,8 +73,31 @@ static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static REGISTERED_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,
|
"l" => look::VERB,
|
||||||
"look" => look::VERB,
|
"look" => look::VERB,
|
||||||
|
"read" => look::VERB,
|
||||||
"-" => whisper::VERB,
|
"-" => whisper::VERB,
|
||||||
"whisper" => 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>> {
|
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
|
||||||
Ok(ctx.trans.find_item_by_type_code(
|
Ok(ctx.trans.find_item_by_type_code(
|
||||||
"player", &get_user_or_fail(ctx)?.username.to_lowercase()).await?
|
"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>) ->
|
pub async fn search_item_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) ->
|
||||||
|
41
blastmud_game/src/message_handler/user_commands/describe.rs
Normal file
41
blastmud_game/src/message_handler/user_commands/describe.rs
Normal 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;
|
@ -23,7 +23,14 @@ pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
|
|||||||
} else {
|
} else {
|
||||||
buf.push_str(room::room_map_by_zloc()
|
buf.push_str(room::room_map_by_zloc()
|
||||||
.get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z }))
|
.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(" "));
|
.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");
|
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
|
||||||
ctx.trans.queue_for_session(
|
ctx.trans.queue_for_session(
|
||||||
ctx.session,
|
ctx.session,
|
||||||
Some(&flow_around(&render_map(room, 5, 5), 10, " ",
|
Some(&flow_around(&render_map(room, 5, 5), 10, ansi!("<reset> "),
|
||||||
&word_wrap(&format!("{} ({})\n{}.{}\n{}\n",
|
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
|
||||||
item.display_for_session(&ctx.session_dat),
|
item.display_for_session(&ctx.session_dat),
|
||||||
zone,
|
zone,
|
||||||
item.details_for_session(
|
item.details_for_session(
|
||||||
|
48
blastmud_game/src/message_handler/user_commands/movement.rs
Normal file
48
blastmud_game/src/message_handler/user_commands/movement.rs
Normal 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;
|
@ -1,7 +1,6 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use crate::static_content::npc::statbot::StatbotState;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct UserTermData {
|
pub struct UserTermData {
|
||||||
@ -76,7 +75,6 @@ pub struct User {
|
|||||||
|
|
||||||
pub terms: UserTermData,
|
pub terms: UserTermData,
|
||||||
pub experience: UserExperienceData,
|
pub experience: UserExperienceData,
|
||||||
pub statbot: Option<StatbotState>,
|
|
||||||
pub raw_skills: BTreeMap<SkillType, u16>,
|
pub raw_skills: BTreeMap<SkillType, u16>,
|
||||||
pub raw_stats: BTreeMap<StatType, u16>,
|
pub raw_stats: BTreeMap<StatType, u16>,
|
||||||
// Reminder: Consider backwards compatibility when updating this. New fields should generally
|
// Reminder: Consider backwards compatibility when updating this. New fields should generally
|
||||||
@ -115,7 +113,6 @@ impl Default for User {
|
|||||||
banned_until: None,
|
banned_until: None,
|
||||||
abandoned_at: None,
|
abandoned_at: None,
|
||||||
chargen_last_completed_at: None,
|
chargen_last_completed_at: None,
|
||||||
statbot: None,
|
|
||||||
|
|
||||||
terms: UserTermData::default(),
|
terms: UserTermData::default(),
|
||||||
experience: UserExperienceData::default(),
|
experience: UserExperienceData::default(),
|
||||||
|
@ -6,6 +6,7 @@ use log::info;
|
|||||||
|
|
||||||
pub mod room;
|
pub mod room;
|
||||||
pub mod npc;
|
pub mod npc;
|
||||||
|
mod fixed_item;
|
||||||
|
|
||||||
pub struct StaticItem {
|
pub struct StaticItem {
|
||||||
pub item_code: &'static str,
|
pub item_code: &'static str,
|
||||||
@ -28,6 +29,10 @@ fn static_item_registry() -> Vec<StaticItemTypeGroup> {
|
|||||||
item_type: "room",
|
item_type: "room",
|
||||||
items: || room::room_static_items()
|
items: || room::room_static_items()
|
||||||
},
|
},
|
||||||
|
StaticItemTypeGroup {
|
||||||
|
item_type: "fixed_item",
|
||||||
|
items: || fixed_item::static_items()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
60
blastmud_game/src/static_content/fixed_item.rs
Normal file
60
blastmud_game/src/static_content/fixed_item.rs
Normal 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()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
use super::NPCMessageHandler;
|
use super::NPCMessageHandler;
|
||||||
|
use super::super::room::{ExitBlocker, Exit};
|
||||||
use crate::message_handler::user_commands::{
|
use crate::message_handler::user_commands::{
|
||||||
VerbContext, UResult,
|
VerbContext, UResult,
|
||||||
get_user_or_fail,
|
get_user_or_fail,
|
||||||
@ -12,12 +13,11 @@ use crate::models::{
|
|||||||
session::Session
|
session::Session
|
||||||
};
|
};
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use nom::character::complete::u8;
|
use nom::character::complete::u8;
|
||||||
|
|
||||||
pub struct StatbotMessageHandler;
|
pub struct StatbotMessageHandler;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Eq, PartialEq, Clone, Debug)]
|
||||||
pub enum StatbotState {
|
pub enum StatbotState {
|
||||||
Brains,
|
Brains,
|
||||||
Senses,
|
Senses,
|
||||||
@ -40,6 +40,15 @@ async fn reply(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> {
|
|||||||
Ok(())
|
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 {
|
fn work_out_state(user: &User, item: &Item) -> StatbotState {
|
||||||
if !user.raw_stats.contains_key(&StatType::Brains) {
|
if !user.raw_stats.contains_key(&StatType::Brains) {
|
||||||
return StatbotState::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
|
(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 brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8);
|
||||||
let sen = user.raw_stats.get(&StatType::Senses).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);
|
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 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 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 {
|
match st {
|
||||||
StatbotState::Brains => ansi!(
|
StatbotState::Brains => ansi!(
|
||||||
"I am Statbot, a robot servant of the empire, put here to help you choose \
|
"The base body has 8 each of brains, senses, \
|
||||||
how your body will function. The base body has 8 each of brains, senses, \
|
|
||||||
brawn, reflexes, endurance and cool - but you get 14 points of improvement. \
|
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 \
|
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 \
|
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)?;
|
let user_mut = get_user_or_fail_mut(ctx)?;
|
||||||
user_mut.raw_stats.insert(stat.clone(), statno as u16);
|
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)?;
|
let user: &User = get_user_or_fail(ctx)?;
|
||||||
ctx.trans.save_user_model(user).await?;
|
ctx.trans.save_user_model(user).await?;
|
||||||
let mut item_updated = item.clone();
|
let mut item_updated = item.clone();
|
||||||
item_updated.total_stats = user.raw_stats.clone();
|
item_updated.total_stats = user.raw_stats.clone();
|
||||||
ctx.trans.save_item_model(&item_updated).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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,14 +241,9 @@ async fn sex_command(ctx: &mut VerbContext<'_>, item: &Item,
|
|||||||
Sex::Female => Pronouns::default_female()
|
Sex::Female => Pronouns::default_female()
|
||||||
};
|
};
|
||||||
item_updated.sex = Some(choice);
|
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)?;
|
let user: &User = get_user_or_fail(ctx)?;
|
||||||
ctx.trans.save_user_model(user).await?;
|
|
||||||
ctx.trans.save_item_model(&item_updated).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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,9 +266,30 @@ impl NPCMessageHandler for StatbotMessageHandler {
|
|||||||
"cool" | "col" => stat_command(ctx, source, &StatType::Cool, &arg).await?,
|
"cool" | "col" => stat_command(ctx, source, &StatType::Cool, &arg).await?,
|
||||||
"sex" => sex_command(ctx, source, &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(())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,10 @@ use super::StaticItem;
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::message_handler::user_commands::{
|
||||||
|
UResult, VerbContext
|
||||||
|
};
|
||||||
use crate::models::item::Item;
|
use crate::models::item::Item;
|
||||||
|
|
||||||
pub struct Zone {
|
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 {
|
pub enum ExitType {
|
||||||
Free, // Anyone can just walk it.
|
Free, // Anyone can just walk it.
|
||||||
|
Blocked(&'static (dyn ExitBlocker + Sync + Send)), // Custom code about who can pass.
|
||||||
// Future ideas: Doors with locks, etc...
|
// Future ideas: Doors with locks, etc...
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +112,8 @@ impl Direction {
|
|||||||
"southeast" | "se" => Some(&Direction::SOUTHEAST),
|
"southeast" | "se" => Some(&Direction::SOUTHEAST),
|
||||||
"northwest" | "nw" => Some(&Direction::NORTHEAST),
|
"northwest" | "nw" => Some(&Direction::NORTHEAST),
|
||||||
"southwest" | "sw" => Some(&Direction::SOUTHWEST),
|
"southwest" | "sw" => Some(&Direction::SOUTHWEST),
|
||||||
|
"up" => Some(&Direction::UP),
|
||||||
|
"down" => Some(&Direction::DOWN),
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,18 +121,25 @@ impl Direction {
|
|||||||
|
|
||||||
pub enum ExitTarget {
|
pub enum ExitTarget {
|
||||||
UseGPS,
|
UseGPS,
|
||||||
#[allow(dead_code)]
|
|
||||||
Custom(&'static str)
|
Custom(&'static str)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Exit {
|
pub struct Exit {
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub target: ExitTarget,
|
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 struct Room {
|
||||||
pub zone: &'static str,
|
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 code: &'static str,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub short: &'static str,
|
pub short: &'static str,
|
||||||
@ -130,8 +155,9 @@ pub fn room_list() -> &'static Vec<Room> {
|
|||||||
|| vec!(
|
|| vec!(
|
||||||
Room {
|
Room {
|
||||||
zone: "repro_xv",
|
zone: "repro_xv",
|
||||||
|
secondary_zones: vec!(),
|
||||||
code: "repro_xv_chargen",
|
code: "repro_xv_chargen",
|
||||||
name: ansi!("<yellow>Choice Room<reset>"),
|
name: ansi!("Choice Room"),
|
||||||
short: ansi!("<bgwhite><green>CR<reset>"),
|
short: ansi!("<bgwhite><green>CR<reset>"),
|
||||||
description: ansi!(
|
description: ansi!(
|
||||||
"A room brightly lit in unnaturally white light, covered in sparkling \
|
"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 \
|
[Try <bold>-statbot hi<reset>, to send hi to statbot - the - means \
|
||||||
to whisper to a particular person in the room]"),
|
to whisper to a particular person in the room]"),
|
||||||
description_less_explicit: None,
|
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 {
|
exits: vec!(Exit {
|
||||||
direction: Direction::EAST,
|
direction: Direction::EAST,
|
||||||
target: ExitTarget::UseGPS,
|
target: ExitTarget::UseGPS,
|
||||||
@ -156,8 +201,9 @@ pub fn room_list() -> &'static Vec<Room> {
|
|||||||
},
|
},
|
||||||
Room {
|
Room {
|
||||||
zone: "repro_xv",
|
zone: "repro_xv",
|
||||||
|
secondary_zones: vec!(),
|
||||||
code: "repro_xv_respawn",
|
code: "repro_xv_respawn",
|
||||||
name: ansi!("<yellow>Body Factory<reset>"),
|
name: ansi!("Body Factory"),
|
||||||
short: ansi!("<bgmagenta><white>BF<reset>"),
|
short: ansi!("<bgmagenta><white>BF<reset>"),
|
||||||
description: ansi!(
|
description: ansi!(
|
||||||
"A room filled with glass vats full of clear fluids, with bodies of \
|
"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 \
|
have no body. But you sense you could go <bold>up<reset> and attach \
|
||||||
your memories to a body matching your current stats"),
|
your memories to a body matching your current stats"),
|
||||||
description_less_explicit: None,
|
description_less_explicit: None,
|
||||||
grid_coords: GridCoords { x: 1, y: 0, z: 1 },
|
grid_coords: GridCoords { x: 2, y: 0, z: -1 },
|
||||||
exits: vec!()
|
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())
|
).into_iter().collect())
|
||||||
}
|
}
|
||||||
@ -182,7 +389,13 @@ static STATIC_ROOM_MAP_BY_ZLOC: OnceCell<BTreeMap<(&'static str, &'static GridCo
|
|||||||
&'static Room>> = OnceCell::new();
|
&'static Room>> = OnceCell::new();
|
||||||
pub fn room_map_by_zloc() -> &'static BTreeMap<(&'static str, &'static GridCoords), &'static Room> {
|
pub fn room_map_by_zloc() -> &'static BTreeMap<(&'static str, &'static GridCoords), &'static Room> {
|
||||||
STATIC_ROOM_MAP_BY_ZLOC.get_or_init(
|
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>> {
|
pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Blastmud setting guidelines for developers
|
# Blastmud setting guidelines for developers
|
||||||
## The Empire (500-200 years ago)
|
## The Empire (300-50 years ago)
|
||||||
About 500 years ago, a group of powerful tech corporation CEOs from around the world grew dissatisfied with the state of the
|
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
|
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
|
to nationalise assets, and others imposed heavy taxes to help the poor (which was a bad thing from the perspective of the
|
||||||
wealthy CEOs).
|
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
|
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.
|
of automated defence systems that make AI-driven judgements and dispensed lethal justice.
|
||||||
|
|
||||||
## The Smiting Shadows (220-195 years ago)
|
## The Smiting Shadows (50-70 years ago)
|
||||||
About 220 years ago, a group of hackers discovered an archive of radical right-wing videos from before the Empire years,
|
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
|
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
|
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,
|
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
|
They immediately started building an underground city, and building and stockpiling conventional and nuclear
|
||||||
weapons, ready to take down the Empire.
|
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,
|
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
|
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
|
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
|
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.
|
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.
|
The Empire was smashed, and some smaller relatively disorganised governments rule local regions.
|
||||||
Much of the world is radioactive.
|
Much of the world is radioactive.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user