Implement basic map and some of the look functionality

Only covering current location look now.
This commit is contained in:
Condorra 2022-12-29 00:37:14 +11:00
parent 1a46405a49
commit 86278e890d
6 changed files with 276 additions and 23 deletions

View File

@ -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)]
@ -225,5 +347,37 @@ mod test {
assert_eq!(limit_special_characters("Test\x1b[5;5fing"), assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
"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);
}
} }

View File

@ -284,6 +284,17 @@ impl DBTrans {
&[&item_type, &item_code]).await?; &[&item_type, &item_code]).await?;
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));

View File

@ -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);

View File

@ -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(())
} }
} }

View File

@ -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,

View File

@ -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()
}) })