forked from blasthavers/blastmud
Implement basic map and some of the look functionality
Only covering current location look now.
This commit is contained in:
parent
1a46405a49
commit
86278e890d
160
ansi/src/lib.rs
160
ansi/src/lib.rs
@ -20,6 +20,24 @@ struct AnsiState {
|
|||||||
strike: bool,
|
strike: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnsiState {
|
||||||
|
fn restore_ansi(self: &Self) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
if !(self.bold && self.underline && self.strike &&
|
||||||
|
self.background != 0 && self.foreground != 0) {
|
||||||
|
buf.push_str(ansi!("<reset>"));
|
||||||
|
}
|
||||||
|
if self.bold { buf.push_str(ansi!("<bold>")); }
|
||||||
|
if self.underline { buf.push_str(ansi!("<under>")); }
|
||||||
|
if self.strike { buf.push_str(ansi!("<strike>")); }
|
||||||
|
if self.background != 0 {
|
||||||
|
buf.push_str(&format!("\x1b[{}m", 39 + self.background)); }
|
||||||
|
if self.foreground != 0 {
|
||||||
|
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground)); }
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
struct AnsiEvent<'l> (
|
struct AnsiEvent<'l> (
|
||||||
AnsiParseToken<'l>,
|
AnsiParseToken<'l>,
|
||||||
@ -187,9 +205,113 @@ pub fn limit_special_characters(input: &str) -> String {
|
|||||||
|
|
||||||
/// Flows a second column around a first column, limiting the width of both
|
/// Flows a second column around a first column, limiting the width of both
|
||||||
/// columns as specified, and adding a gutter.
|
/// columns as specified, and adding a gutter.
|
||||||
pub fn flow_around(col1: &str, col1_width: u64, gutter: &str,
|
pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
||||||
col2: &str, col2_width: u64) -> String {
|
col2: &str, col2_width: usize) -> String {
|
||||||
"not yet".to_owned()
|
let mut it1 = AnsiIterator::new(col1).peekable();
|
||||||
|
let mut it2 = AnsiIterator::new(col2).peekable();
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
// Phase 1: col1 still has data, so flow col2 around col1.
|
||||||
|
'around_rows: loop {
|
||||||
|
match it1.peek() {
|
||||||
|
None => break 'around_rows,
|
||||||
|
Some(AnsiEvent(_, st)) => buf.push_str(&st.restore_ansi())
|
||||||
|
}
|
||||||
|
let mut fill_needed: usize = 0;
|
||||||
|
let mut skip_nl = true;
|
||||||
|
'col_data: for i in 0..col1_width {
|
||||||
|
'until_move_forward: loop {
|
||||||
|
match it1.next() {
|
||||||
|
None | Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
|
fill_needed = col1_width - i;
|
||||||
|
skip_nl = false;
|
||||||
|
break 'col_data;
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
|
||||||
|
buf.push(c);
|
||||||
|
break 'until_move_forward;
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
|
buf.push_str(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there is a newline (optionally preceded by 1+ control characters),
|
||||||
|
// and we didn't just read one, we should skip it, since we broke to a
|
||||||
|
// new line anyway. It is safe to eat any control characters since we will
|
||||||
|
// restore_ansi() anyway.
|
||||||
|
if skip_nl {
|
||||||
|
loop {
|
||||||
|
match it1.peek() {
|
||||||
|
None => break,
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
||||||
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
|
if fill_needed > 0 { buf.push_str(s); }
|
||||||
|
it1.next();
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
|
it1.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..fill_needed { buf.push(' '); }
|
||||||
|
|
||||||
|
buf.push_str(gutter);
|
||||||
|
|
||||||
|
if let Some(AnsiEvent(_, st)) = it2.peek() {
|
||||||
|
buf.push_str(&st.restore_ansi())
|
||||||
|
}
|
||||||
|
skip_nl = true;
|
||||||
|
'col_data: for _ in 0..col2_width {
|
||||||
|
'until_move_forward: loop {
|
||||||
|
match it2.next() {
|
||||||
|
None | Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
|
skip_nl = false;
|
||||||
|
break 'col_data;
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
|
||||||
|
buf.push(c);
|
||||||
|
break 'until_move_forward;
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
|
buf.push_str(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skip_nl {
|
||||||
|
loop {
|
||||||
|
match it2.peek() {
|
||||||
|
None => break,
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
||||||
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
|
if fill_needed > 0 { buf.push_str(s); }
|
||||||
|
it2.next();
|
||||||
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
|
it2.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now just copy anything left in it2 over.
|
||||||
|
for AnsiEvent(e, _) in it2 {
|
||||||
|
match e {
|
||||||
|
AnsiParseToken::Character(c) => buf.push(c),
|
||||||
|
AnsiParseToken::Newline => buf.push('\n'),
|
||||||
|
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -226,4 +348,36 @@ mod test {
|
|||||||
"Test5fing");
|
"Test5fing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flow_around_works_for_plain_text() {
|
||||||
|
let str1 = " /\\ /\\\n\
|
||||||
|
/--------\\\n\
|
||||||
|
| () () |\n\
|
||||||
|
| |\n\
|
||||||
|
| /\\ |\n\
|
||||||
|
| \\ / |\n\
|
||||||
|
| -(--)- |\n\
|
||||||
|
| / \\ |\n\
|
||||||
|
\\--------/\n\
|
||||||
|
A very poor rendition of a cat! Meow.";
|
||||||
|
let str2 = "Hello world, this is the second column for this test. It starts with a rather long line that will wrap.\n\
|
||||||
|
And here is a shorter line.\n\
|
||||||
|
All of this should by nicely wrapped, even if it is exactly the len\n\
|
||||||
|
gth of column 2!\n\
|
||||||
|
\n\
|
||||||
|
But double newlines should come up as blank lines.\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
Blah\n\
|
||||||
|
And once we get to the bottom of column 1, column 2 should just get written\n\
|
||||||
|
out normally, not in the previous column.";
|
||||||
|
// This has a lot of unnecessary resets, but that is expected with the algorithm right now.
|
||||||
|
let expected = "\u{1b}[0m /\\ /\\ | \u{1b}[0mHello world, this is the second column for this test. It starts wit\n\u{1b}[0m/--------\\ | \u{1b}[0mh a rather long line that will wrap.\n\u{1b}[0m| () () | | \u{1b}[0mAnd here is a shorter line.\n\u{1b}[0m| | | \u{1b}[0mAll of this should by nicely wrapped, even if it is exactly the len\n\u{1b}[0m| /\\ | | \u{1b}[0mgth of column 2!\n\u{1b}[0m| \\ / | | \u{1b}[0m\n\u{1b}[0m| -(--)- | | \u{1b}[0mBut double newlines should come up as blank lines.\n\u{1b}[0m| / \\ | | \u{1b}[0mBlah\n\u{1b}[0m\\--------/ | \u{1b}[0mBlah\n\u{1b}[0mA very poo | \u{1b}[0mBlah\n\u{1b}[0mr renditio | \u{1b}[0mBlah\n\u{1b}[0mn of a cat | \u{1b}[0mBlah\n\u{1b}[0m! Meow. | \u{1b}[0mBlah\nBlah\nAnd once we get to the bottom of column 1, column 2 should just get written\nout normally, not in the previous column.";
|
||||||
|
assert_eq!(flow_around(str1, 10, " | ", str2, 67), expected);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -285,6 +285,17 @@ impl DBTrans {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_item_by_type_code(self: &Self, item_type: &str, item_code: &str) ->
|
||||||
|
DResult<Option<Item>> {
|
||||||
|
if let Some(item) = self.pg_trans()?.query_opt(
|
||||||
|
"SELECT details FROM items WHERE \
|
||||||
|
details->>'item_type' = $1 AND \
|
||||||
|
details->>'item_code' = $2", &[&item_type, &item_code]).await? {
|
||||||
|
return Ok(serde_json::from_value(item.get("details"))?);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn commit(mut self: Self) -> DResult<()> {
|
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||||
if let Some(trans) = trans_opt {
|
if let Some(trans) = trans_opt {
|
||||||
|
@ -72,6 +72,14 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
"look" => look::VERB,
|
"look" => look::VERB,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn explicit_if_allowed<'l>(ctx: &VerbContext, explicit: &'l str, non_explicit: Option<&'l str>) -> &'l str {
|
||||||
|
if ctx.session_dat.less_explicit_mode {
|
||||||
|
non_explicit.unwrap_or(explicit)
|
||||||
|
} else {
|
||||||
|
explicit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
|
||||||
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
|
||||||
|
|
||||||
|
@ -1,12 +1,81 @@
|
|||||||
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError};
|
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, explicit_if_allowed};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use ansi::{ansi, flow_around};
|
||||||
|
use crate::models::{user::User, item::Item};
|
||||||
|
use crate::static_content::room;
|
||||||
|
|
||||||
|
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
|
||||||
|
ctx.user_dat.as_ref()
|
||||||
|
.ok_or_else(|| UserError("Not logged in".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<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()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let my_loc = &room.grid_coords;
|
||||||
|
let min_x = my_loc.x - (width as i64) / 2;
|
||||||
|
let max_x = min_x + (width as i64);
|
||||||
|
let min_y = my_loc.y - (height as i64) / 2;
|
||||||
|
let max_y = min_y + (height as i64);
|
||||||
|
for x in min_x..max_x {
|
||||||
|
for y in min_y..max_y {
|
||||||
|
if my_loc.x == x && my_loc.y == y {
|
||||||
|
buf.push_str(ansi!("<bgblue><red>()<reset>"))
|
||||||
|
} else {
|
||||||
|
buf.push_str(room::room_map_by_loc()
|
||||||
|
.get(&room::GridCoords { x, y, z: my_loc.z })
|
||||||
|
.map(|r| r.short)
|
||||||
|
.unwrap_or(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> {
|
||||||
|
ctx.trans.queue_for_session(
|
||||||
|
ctx.session,
|
||||||
|
Some(explicit_if_allowed(
|
||||||
|
ctx,
|
||||||
|
&item.display,
|
||||||
|
item.display_less_explicit.as_ref().map(|s|&**s)))
|
||||||
|
).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn describe_room(ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()> {
|
||||||
|
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, " ",
|
||||||
|
&format!("{} ({})\n{}\n", room.name, zone,
|
||||||
|
explicit_if_allowed(ctx, room.description,
|
||||||
|
room.description_less_explicit)), 68))
|
||||||
|
).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Verb;
|
pub struct Verb;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserVerb for Verb {
|
impl UserVerb for Verb {
|
||||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
|
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
|
||||||
let user = ctx.user_dat.as_ref()
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
.ok_or_else(|| UserError("Not logged in".to_owned()))?;
|
let (loctype, loccode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
|
||||||
|
if loctype != "room" {
|
||||||
|
let item = ctx.trans.find_item_by_type_code(loctype, loccode).await?
|
||||||
|
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?;
|
||||||
|
describe_normal_item(ctx, &item).await?;
|
||||||
|
} else {
|
||||||
|
let room =
|
||||||
|
room::room_map_by_code().get(loccode).ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
|
||||||
|
describe_room(ctx, &room).await?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use crate::models::item::Item;
|
|||||||
use std::collections::{BTreeSet, BTreeMap};
|
use std::collections::{BTreeSet, BTreeMap};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
mod room;
|
pub mod room;
|
||||||
|
|
||||||
pub struct StaticItem {
|
pub struct StaticItem {
|
||||||
pub item_code: &'static str,
|
pub item_code: &'static str,
|
||||||
|
@ -23,10 +23,11 @@ pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> {
|
|||||||
).into_iter().map(|x|(x.code, x)).collect())
|
).into_iter().map(|x|(x.code, x)).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
|
||||||
pub struct GridCoords {
|
pub struct GridCoords {
|
||||||
x: i64,
|
pub x: i64,
|
||||||
y: i64,
|
pub y: i64,
|
||||||
z: i64
|
pub z: i64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ExitType {
|
pub enum ExitType {
|
||||||
@ -66,6 +67,7 @@ pub struct Room {
|
|||||||
pub short: &'static str,
|
pub short: &'static str,
|
||||||
pub grid_coords: GridCoords,
|
pub grid_coords: GridCoords,
|
||||||
pub description: &'static str,
|
pub description: &'static str,
|
||||||
|
pub description_less_explicit: Option<&'static str>,
|
||||||
pub exits: Vec<Exit>
|
pub exits: Vec<Exit>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,19 +78,22 @@ pub fn room_list() -> &'static Vec<Room> {
|
|||||||
Room {
|
Room {
|
||||||
zone: "repro_xv",
|
zone: "repro_xv",
|
||||||
code: "repro_xv_chargen",
|
code: "repro_xv_chargen",
|
||||||
name: "Choice Room",
|
name: ansi!("<yellow>Choice Room<reset>"),
|
||||||
short: ansi!("<green>CR<reset>"),
|
short: ansi!("<green>CR<reset>"),
|
||||||
description: "A room brightly lit in unnaturally white light, covered in sparkling \
|
description: ansi!(
|
||||||
white tiles from floor to \
|
"A room brightly lit in unnaturally white light, covered in sparkling\n\
|
||||||
ceiling. A loudspeaker plays a message on loop:\r\n\
|
white tiles from floor to ceiling. A loudspeaker plays a message on\n\
|
||||||
\t\"Citizen, you are here because your memory has been wiped and you \
|
loop:\r\n\
|
||||||
are ready to start a fresh life. As a being enhanced by Gazos-Murlison \
|
\t<blue>\"Citizen, you are here because your memory has been wiped and\n\
|
||||||
Co technology, the emperor has granted you the power to choose 14 points \
|
you are ready to start a fresh life. As a being enhanced by \
|
||||||
of upgrades to yourself. Choose wisely, as it will impact who you end up \
|
Gazos-Murlison Co technology, the emperor has granted you the power \
|
||||||
being, and you would need to completely wipe your brain again to change \
|
to choose 14 points of upgrades to yourself. Choose wisely, as it \
|
||||||
them. Talk to Statbot to spend your 14 points and create your body.\"\r\n\
|
will impact who you end up being, and you would need to completely \
|
||||||
[Try <bold>\"statbot hi<reset>, to send hi to statbot - the \" means to \
|
wipe your brain again to change them. Talk to Statbot to spend your \
|
||||||
whisper to a particular person in the room]",
|
14 points and create your body.\"<reset>\n\
|
||||||
|
[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: 2 },
|
grid_coords: GridCoords { x: 0, y: 0, z: 2 },
|
||||||
exits: vec!()
|
exits: vec!()
|
||||||
},
|
},
|
||||||
@ -101,6 +106,12 @@ pub fn room_map_by_code() -> &'static BTreeMap<&'static str, &'static Room> {
|
|||||||
|| room_list().iter().map(|r| (r.code, r)).collect())
|
|| room_list().iter().map(|r| (r.code, r)).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static STATIC_ROOM_MAP_BY_LOC: OnceCell<BTreeMap<&'static GridCoords, &'static Room>> = OnceCell::new();
|
||||||
|
pub fn room_map_by_loc() -> &'static BTreeMap<&'static GridCoords, &'static Room> {
|
||||||
|
STATIC_ROOM_MAP_BY_LOC.get_or_init(
|
||||||
|
|| room_list().iter().map(|r| (&r.grid_coords, r)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||||
Box::new(room_list().iter().map(|r| StaticItem {
|
Box::new(room_list().iter().map(|r| StaticItem {
|
||||||
item_code: r.code,
|
item_code: r.code,
|
||||||
@ -108,7 +119,7 @@ pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
|||||||
item_code: r.code.to_owned(),
|
item_code: r.code.to_owned(),
|
||||||
item_type: "room".to_owned(),
|
item_type: "room".to_owned(),
|
||||||
display: r.description.to_owned(),
|
display: r.description.to_owned(),
|
||||||
location: r.code.to_owned(),
|
location: format!("room/{}", r.code),
|
||||||
is_static: true,
|
is_static: true,
|
||||||
..Item::default()
|
..Item::default()
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user