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,
|
||||
}
|
||||
|
||||
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)]
|
||||
struct AnsiEvent<'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
|
||||
/// columns as specified, and adding a gutter.
|
||||
pub fn flow_around(col1: &str, col1_width: u64, gutter: &str,
|
||||
col2: &str, col2_width: u64) -> String {
|
||||
"not yet".to_owned()
|
||||
pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
||||
col2: &str, col2_width: usize) -> String {
|
||||
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)]
|
||||
@ -225,5 +347,37 @@ mod test {
|
||||
assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
|
||||
"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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -284,6 +284,17 @@ impl DBTrans {
|
||||
&[&item_type, &item_code]).await?;
|
||||
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<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
|
@ -72,6 +72,14 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"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> {
|
||||
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 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;
|
||||
#[async_trait]
|
||||
impl UserVerb for Verb {
|
||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
|
||||
let user = ctx.user_dat.as_ref()
|
||||
.ok_or_else(|| UserError("Not logged in".to_owned()))?;
|
||||
let player_item = get_player_item_or_fail(ctx).await?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use crate::models::item::Item;
|
||||
use std::collections::{BTreeSet, BTreeMap};
|
||||
use log::info;
|
||||
|
||||
mod room;
|
||||
pub mod room;
|
||||
|
||||
pub struct StaticItem {
|
||||
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())
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
|
||||
pub struct GridCoords {
|
||||
x: i64,
|
||||
y: i64,
|
||||
z: i64
|
||||
pub x: i64,
|
||||
pub y: i64,
|
||||
pub z: i64
|
||||
}
|
||||
|
||||
pub enum ExitType {
|
||||
@ -66,6 +67,7 @@ pub struct Room {
|
||||
pub short: &'static str,
|
||||
pub grid_coords: GridCoords,
|
||||
pub description: &'static str,
|
||||
pub description_less_explicit: Option<&'static str>,
|
||||
pub exits: Vec<Exit>
|
||||
}
|
||||
|
||||
@ -76,19 +78,22 @@ pub fn room_list() -> &'static Vec<Room> {
|
||||
Room {
|
||||
zone: "repro_xv",
|
||||
code: "repro_xv_chargen",
|
||||
name: "Choice Room",
|
||||
name: ansi!("<yellow>Choice Room<reset>"),
|
||||
short: ansi!("<green>CR<reset>"),
|
||||
description: "A room brightly lit in unnaturally white light, covered in sparkling \
|
||||
white tiles from floor to \
|
||||
ceiling. A loudspeaker plays a message on loop:\r\n\
|
||||
\t\"Citizen, you are here because your memory has been wiped and you \
|
||||
are ready to start a fresh life. As a being enhanced by Gazos-Murlison \
|
||||
Co technology, the emperor has granted you the power to choose 14 points \
|
||||
of upgrades to yourself. Choose wisely, as it will impact who you end up \
|
||||
being, and you would need to completely wipe your brain again to change \
|
||||
them. Talk to Statbot to spend your 14 points and create your body.\"\r\n\
|
||||
[Try <bold>\"statbot hi<reset>, to send hi to statbot - the \" means to \
|
||||
whisper to a particular person in the room]",
|
||||
description: ansi!(
|
||||
"A room brightly lit in unnaturally white light, covered in sparkling\n\
|
||||
white tiles from floor to ceiling. A loudspeaker plays a message on\n\
|
||||
loop:\r\n\
|
||||
\t<blue>\"Citizen, you are here because your memory has been wiped and\n\
|
||||
you are ready to start a fresh life. As a being enhanced by \
|
||||
Gazos-Murlison Co technology, the emperor has granted you the power \
|
||||
to choose 14 points of upgrades to yourself. Choose wisely, as it \
|
||||
will impact who you end up being, and you would need to completely \
|
||||
wipe your brain again to change them. Talk to Statbot to spend your \
|
||||
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 },
|
||||
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())
|
||||
}
|
||||
|
||||
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>> {
|
||||
Box::new(room_list().iter().map(|r| StaticItem {
|
||||
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_type: "room".to_owned(),
|
||||
display: r.description.to_owned(),
|
||||
location: r.code.to_owned(),
|
||||
location: format!("room/{}", r.code),
|
||||
is_static: true,
|
||||
..Item::default()
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user