13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
16 <pre id="terminal"></pre>
17 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
19 <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal / curses client)
20 <h3>button controls for mouse players</h3>
21 <table style="float: left">
23 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
24 <td style="text-align: center"><button id="move_up">up</button></td>
25 <td><button id="move_upright">up-right</button></td>
28 <td style="text-align: right;"><button id="move_left">left</button></td>
29 <td stlye="text-align: center;">move</td>
30 <td><button id="move_right">right</button></td>
33 <td><button id="move_downleft">down-left</button></td>
34 <td style="text-align: center"><button id="move_down">down</button></td>
35 <td><button id="move_downright">down-right</button></td>
40 <td><button id="help">help</button></td>
43 <td><button id="switch_to_chat">chat mode</button><br /></td>
45 <td><button id="switch_to_study">study mode</button></td>
46 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
48 <td><button id="switch_to_play">play mode</button></td>
52 <td><button id="take_thing">take thing</button></td>
53 <td><button id="switch_to_edit">change tile</button></td>
54 <td><button id="switch_to_admin">become admin</button></td>
57 <td><button id="drop_thing">drop thing</button></td>
58 <td><button id="switch_to_password">change tile editing password</button></td>
59 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
62 <td><button id="flatten">flatten surroundings</button></td>
63 <td><button id="switch_to_annotate">annotate tile</button></td>
64 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
67 <td><button id="teleport">teleport</button></td>
68 <td><button id="switch_to_portal">edit portal link</button></td>
74 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
76 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
77 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
78 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
79 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
80 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
81 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
82 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
83 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
84 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
85 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
86 <li>help: <input id="key_help" type="text" value="h" />
87 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
88 <li>teleport: <input id="key_teleport" type="text" value="p" />
89 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
90 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
91 <li><input id="key_switch_to_chat" type="text" value="t" />
92 <li><input id="key_switch_to_play" type="text" value="p" />
93 <li><input id="key_switch_to_study" type="text" value="?" />
94 <li><input id="key_switch_to_edit" type="text" value="m" />
95 <li><input id="key_switch_to_password" type="text" value="P" />
96 <li><input id="key_switch_to_admin" type="text" value="A" />
97 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
98 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
99 <li><input id="key_switch_to_annotate" type="text" value="M" />
100 <li><input id="key_switch_to_portal" type="text" value="T" />
101 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
106 let websocket_location = "wss://plomlompom.com/rogue_chat/";
107 //let websocket_location = "ws://localhost:8000/";
112 'long': 'This mode allows you to interact with the map.'
116 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it.'},
118 'short': 'terrain edit',
119 'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
122 'short': 'change tiles control password',
123 'long': 'This mode is the first of two steps to change the password for a tile control character. First enter the tile control character for which you want to change the password!'
126 'short': 'change tiles control password',
127 'long': 'This mode is the second of two steps to change the password for a tile control character. Enter the new password for the tile control character you chose.'
129 'control_tile_type': {
130 'short': 'change tiles control',
131 'long': 'This mode is the first of two steps to change tile control areas on the map. First enter the tile control character you want to write.'
133 'control_tile_draw': {
134 'short': 'change tiles control',
135 'long': 'This mode is the second of two steps to change tile control areas on the map. Move cursor around the map to draw selected tile control character'
138 'short': 'annotate tile',
139 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.'
142 'short': 'edit portal',
143 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
147 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:'
151 'long': 'Pick your player name.'
153 'waiting_for_server': {
154 'short': 'waiting for server response',
155 'long': 'Waiting for a server response.'
158 'short': 'waiting for server response',
159 'long': 'Waiting for a server response.'
162 'short': 'map edit password',
163 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.'
166 'short': 'become admin',
167 'long': 'This mode allows you to become admin if you know an admin password.'
171 let rows_selector = document.getElementById("n_rows");
172 let cols_selector = document.getElementById("n_cols");
173 let key_selectors = document.querySelectorAll('[id^="key_"]');
175 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
176 const action = key_switch_selector.id.slice("key_switch_to_".length);
177 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
180 function restore_selector_value(selector) {
181 let stored_selection = window.localStorage.getItem(selector.id);
182 if (stored_selection) {
183 selector.value = stored_selection;
186 restore_selector_value(rows_selector);
187 restore_selector_value(cols_selector);
188 for (let key_selector of key_selectors) {
189 restore_selector_value(key_selector);
195 initialize: function() {
196 this.rows = rows_selector.value;
197 this.cols = cols_selector.value;
198 this.pre_el = document.getElementById("terminal");
199 this.pre_el.style.color = this.foreground;
200 this.pre_el.style.backgroundColor = this.background;
203 for (let y = 0, x = 0; y <= this.rows; x++) {
204 if (x == this.cols) {
207 this.content.push(line);
209 if (y == this.rows) {
216 blink_screen: function() {
217 this.pre_el.style.color = this.background;
218 this.pre_el.style.backgroundColor = this.foreground;
220 this.pre_el.style.color = this.foreground;
221 this.pre_el.style.backgroundColor = this.background;
224 refresh: function() {
225 function escapeHTML(str) {
227 replace(/&/g, '&').
228 replace(/</g, '<').
229 replace(/>/g, '>').
230 replace(/'/g, ''').
231 replace(/"/g, '"');
233 let pre_content = '';
234 for (let y = 0; y < this.rows; y++) {
235 let line = this.content[y].join('');
237 if (y in tui.links) {
239 for (let span of tui.links[y]) {
240 chunks.push(escapeHTML(line.slice(start_x, span[0])));
241 chunks.push('<a href="');
242 chunks.push(escapeHTML(span[2]));
244 chunks.push(escapeHTML(line.slice(span[0], span[1])));
248 chunks.push(escapeHTML(line.slice(start_x)));
250 chunks = [escapeHTML(line)];
252 for (const chunk of chunks) {
253 pre_content += chunk;
257 this.pre_el.innerHTML = pre_content;
259 write: function(start_y, start_x, msg) {
260 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
261 this.content[start_y][x] = msg[i];
264 drawBox: function(start_y, start_x, height, width) {
265 let end_y = start_y + height;
266 let end_x = start_x + width;
267 for (let y = start_y, x = start_x; y < this.rows; x++) {
275 this.content[y][x] = ' ';
279 terminal.initialize();
282 tokenize: function(str) {
287 for (let i = 0; i < str.length; i++) {
293 } else if (c == '\\') {
295 } else if (c == '"') {
300 } else if (c == '"') {
302 } else if (c === ' ') {
303 if (token.length > 0) {
311 if (token.length > 0) {
316 parse_yx: function(position_string) {
317 let coordinate_strings = position_string.split(',')
318 let position = [0, 0];
319 position[0] = parseInt(coordinate_strings[0].slice(2));
320 position[1] = parseInt(coordinate_strings[1].slice(2));
332 init: function(url) {
334 this.websocket = new WebSocket(this.url);
335 this.websocket.onopen = function(event) {
336 server.connected = true;
337 game.thing_types = {};
339 server.send(['TASKS']);
340 server.send(['TERRAINS']);
341 server.send(['THING_TYPES']);
342 tui.log_msg("@ server connected! :)");
343 tui.switch_mode('login');
345 this.websocket.onclose = function(event) {
346 server.connected = false;
347 tui.switch_mode('waiting_for_server');
348 tui.log_msg("@ server disconnected :(");
350 this.websocket.onmessage = this.handle_event;
352 reconnect_to: function(url) {
353 this.websocket.close();
356 send: function(tokens) {
357 this.websocket.send(unparser.untokenize(tokens));
359 handle_event: function(event) {
360 let tokens = parser.tokenize(event.data);
361 if (tokens[0] === 'TURN') {
362 game.turn_complete = false;
363 explorer.empty_info_db();
366 game.turn = parseInt(tokens[1]);
367 } else if (tokens[0] === 'THING') {
368 let t = game.get_thing(tokens[3], true);
369 t.position = parser.parse_yx(tokens[1]);
371 } else if (tokens[0] === 'THING_NAME') {
372 let t = game.get_thing(tokens[1], false);
376 } else if (tokens[0] === 'THING_CHAR') {
377 let t = game.get_thing(tokens[1], false);
379 t.player_char = tokens[2];
381 } else if (tokens[0] === 'TASKS') {
382 game.tasks = tokens[1].split(',');
383 tui.mode_edit.legal = game.tasks.includes('WRITE');
384 } else if (tokens[0] === 'THING_TYPE') {
385 game.thing_types[tokens[1]] = tokens[2]
386 } else if (tokens[0] === 'TERRAIN') {
387 game.terrains[tokens[1]] = tokens[2]
388 } else if (tokens[0] === 'MAP') {
389 game.map_geometry = tokens[1];
391 game.map_size = parser.parse_yx(tokens[2]);
393 } else if (tokens[0] === 'FOV') {
395 } else if (tokens[0] === 'MAP_CONTROL') {
396 game.map_control = tokens[1]
397 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
398 game.turn_complete = true;
399 if (tui.mode.name == 'post_login_wait') {
400 tui.switch_mode('play');
401 } else if (tui.mode.name == 'study') {
402 explorer.query_info();
405 } else if (tokens[0] === 'CHAT') {
406 tui.log_msg('# ' + tokens[1], 1);
407 } else if (tokens[0] === 'PLAYER_ID') {
408 game.player_id = parseInt(tokens[1]);
409 } else if (tokens[0] === 'LOGIN_OK') {
410 this.send(['GET_GAMESTATE']);
411 tui.switch_mode('post_login_wait');
412 } else if (tokens[0] === 'PORTAL') {
413 let position = parser.parse_yx(tokens[1]);
414 game.portals[position] = tokens[2];
415 } else if (tokens[0] === 'ANNOTATION_HINT') {
416 let position = parser.parse_yx(tokens[1]);
417 explorer.info_hints = explorer.info_hints.concat([position]);
418 } else if (tokens[0] === 'ANNOTATION') {
419 let position = parser.parse_yx(tokens[1]);
420 explorer.update_info_db(position, tokens[2]);
421 tui.restore_input_values();
423 } else if (tokens[0] === 'UNHANDLED_INPUT') {
424 tui.log_msg('? unknown command');
425 } else if (tokens[0] === 'PLAY_ERROR') {
426 tui.log_msg('? ' + tokens[1]);
427 terminal.blink_screen();
428 } else if (tokens[0] === 'ARGUMENT_ERROR') {
429 tui.log_msg('? syntax error: ' + tokens[1]);
430 } else if (tokens[0] === 'GAME_ERROR') {
431 tui.log_msg('? game error: ' + tokens[1]);
432 } else if (tokens[0] === 'PONG') {
435 tui.log_msg('? unhandled input: ' + event.data);
441 quote: function(str) {
443 for (let i = 0; i < str.length; i++) {
445 if (['"', '\\'].includes(c)) {
451 return quoted.join('');
453 to_yx: function(yx_coordinate) {
454 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
456 untokenize: function(tokens) {
457 let quoted_tokens = [];
458 for (let token of tokens) {
459 quoted_tokens.push(this.quote(token));
461 return quoted_tokens.join(" ");
466 constructor(name, has_input_prompt=false, shows_info=false,
467 is_intro=false, is_single_char_entry=false) {
469 this.short_desc = mode_helps[name].short;
470 this.available_modes = [];
471 this.has_input_prompt = has_input_prompt;
472 this.shows_info= shows_info;
473 this.is_intro = is_intro;
474 this.help_intro = mode_helps[name].long;
475 this.is_single_char_entry = is_single_char_entry;
478 *iter_available_modes() {
479 for (let mode_name of this.available_modes) {
480 let mode = tui['mode_' + mode_name];
484 let key = tui.keys['switch_to_' + mode.name];
488 list_available_modes() {
490 if (this.available_modes.length > 0) {
491 msg += 'Other modes available from here:\n';
492 for (let [mode, key] of this.iter_available_modes()) {
493 msg += '[' + key + '] – ' + mode.short_desc + '\n';
498 mode_switch_on_key(key_event) {
499 for (let [mode, key] of this.iter_available_modes()) {
500 if (key_event.key == key) {
501 event.preventDefault();
502 tui.switch_mode(mode.name);
514 window_width: terminal.cols / 2,
520 mode_waiting_for_server: new Mode('waiting_for_server',
522 mode_login: new Mode('login', true, false, true),
523 mode_post_login_wait: new Mode('post_login_wait'),
524 mode_chat: new Mode('chat', true),
525 mode_annotate: new Mode('annotate', true, true),
526 mode_play: new Mode('play'),
527 mode_study: new Mode('study', false, true),
528 mode_edit: new Mode('edit', false, false, false, true),
529 mode_control_pw_type: new Mode('control_pw_type',
530 false, false, false, true),
531 mode_portal: new Mode('portal', true, true),
532 mode_password: new Mode('password', true),
533 mode_admin: new Mode('admin', true),
534 mode_control_pw_pw: new Mode('control_pw_pw', true),
535 mode_control_tile_type: new Mode('control_tile_type',
536 false, false, false, true),
537 mode_control_tile_draw: new Mode('control_tile_draw'),
539 this.mode_play.available_modes = ["chat", "study", "edit",
540 "annotate", "portal",
544 this.mode_study.available_modes = ["chat", "play"]
545 this.mode_control_tile_draw.available_modes = ["play"]
546 this.mode = this.mode_waiting_for_server;
547 this.inputEl = document.getElementById("input");
548 this.inputEl.focus();
549 this.recalc_input_lines();
550 this.height_header = this.height_turn_line + this.height_mode_line;
551 this.log_msg("@ waiting for server connection ...");
554 init_keys: function() {
556 for (let key_selector of key_selectors) {
557 this.keys[key_selector.id.slice(4)] = key_selector.value;
559 if (game.map_geometry == 'Square') {
560 this.movement_keys = {
561 [this.keys.square_move_up]: 'UP',
562 [this.keys.square_move_left]: 'LEFT',
563 [this.keys.square_move_down]: 'DOWN',
564 [this.keys.square_move_right]: 'RIGHT'
566 document.getElementById("move_upright").hidden = true;
567 document.getElementById("move_upleft").hidden = true;
568 document.getElementById("move_downright").hidden = true;
569 document.getElementById("move_downleft").hidden = true;
570 document.getElementById("move_up").hidden = false;
571 document.getElementById("move_down").hidden = false;
572 } else if (game.map_geometry == 'Hex') {
573 document.getElementById("move_upright").hidden = false;
574 document.getElementById("move_upleft").hidden = false;
575 document.getElementById("move_downright").hidden = false;
576 document.getElementById("move_downleft").hidden = false;
577 document.getElementById("move_up").hidden = true;
578 document.getElementById("move_down").hidden = true;
579 this.movement_keys = {
580 [this.keys.hex_move_upleft]: 'UPLEFT',
581 [this.keys.hex_move_upright]: 'UPRIGHT',
582 [this.keys.hex_move_right]: 'RIGHT',
583 [this.keys.hex_move_downright]: 'DOWNRIGHT',
584 [this.keys.hex_move_downleft]: 'DOWNLEFT',
585 [this.keys.hex_move_left]: 'LEFT'
589 switch_mode: function(mode_name) {
590 this.inputEl.focus();
591 this.map_mode = 'terrain';
592 this.mode = this['mode_' + mode_name];
593 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
594 explorer.position = game.things[game.player_id].position;
595 if (this.mode.shows_info) {
596 explorer.query_info();
597 } else if (this.mode.name == 'control_tile_draw') {
598 explorer.send_tile_control_command();
599 this.map_mode = 'control';
603 this.restore_input_values();
604 for (let el of document.getElementsByTagName("button")) {
607 document.getElementById("help").disabled = false;
608 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
609 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
610 move_key.disabled = false;
613 if (!this.mode.is_intro && this.mode.name != 'play') {
614 document.getElementById("switch_to_play").disabled = false;
616 if (!this.mode.is_intro && this.mode.name != 'study') {
617 document.getElementById("switch_to_study").disabled = false;
619 if (!this.mode.is_intro && this.mode.name != 'chat') {
620 document.getElementById("switch_to_chat").disabled = false;
622 if (this.mode.name == 'login') {
623 if (this.login_name) {
624 server.send(['LOGIN', this.login_name]);
626 this.log_msg("? need login name");
628 } else if (this.mode.name == 'play') {
629 if (game.tasks.includes('PICK_UP')) {
630 document.getElementById("take_thing").disabled = false;
632 if (game.tasks.includes('DROP')) {
633 document.getElementById("drop_thing").disabled = false;
635 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
636 document.getElementById("flatten").disabled = false;
638 if (game.tasks.includes('MOVE')) {
640 document.getElementById("teleport").disabled = false;
641 document.getElementById("switch_to_annotate").disabled = false;
642 document.getElementById("switch_to_edit").disabled = false;
643 document.getElementById("switch_to_portal").disabled = false;
644 document.getElementById("switch_to_password").disabled = false;
645 document.getElementById("switch_to_admin").disabled = false;
646 document.getElementById("switch_to_control_pw_type").disabled = false;
647 document.getElementById("switch_to_control_tile_type").disabled = false;
648 } else if (this.mode.name == 'study') {
649 document.getElementById("toggle_map_mode").disabled = false;
650 } else if (this.mode.is_single_char_entry) {
651 this.show_help = true;
652 } else if (this.mode.name == 'admin') {
653 this.log_msg('@ enter admin password:')
654 } else if (this.mode.name == 'control_pw_pw') {
655 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
656 } else if (this.mode.name == 'control_pw_pw') {
657 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
661 offset_links: function(offset, links) {
662 for (let y in links) {
663 let real_y = offset[0] + parseInt(y);
664 if (!this.links[real_y]) {
665 this.links[real_y] = [];
667 for (let link of links[y]) {
668 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
669 this.links[real_y].push(offset_link);
673 restore_input_values: function() {
674 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
675 let info = explorer.info_db[explorer.position];
676 if (info != "(none)") {
677 this.inputEl.value = info;
678 this.recalc_input_lines();
680 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
681 let portal = game.portals[explorer.position]
682 this.inputEl.value = portal;
683 this.recalc_input_lines();
684 } else if (this.mode.name == 'password') {
685 this.inputEl.value = this.password;
686 this.recalc_input_lines();
689 empty_input: function(str) {
690 this.inputEl.value = "";
691 if (this.mode.has_input_prompt) {
692 this.recalc_input_lines();
694 this.height_input = 0;
697 recalc_input_lines: function() {
699 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
700 this.height_input = this.input_lines.length;
702 msg_into_lines_of_width: function(msg, width) {
703 function push_inner_link(y, end_x) {
704 if (!inner_links[y]) {
707 inner_links[y].push([url_start_x, end_x, url]);
709 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
712 for (const match of matches) {
713 const url = match[0];
714 const url_start = match.index;
715 const url_end = match.index + match[0].length;
716 link_data[url_start] = url;
717 url_ends.push(url_end);
721 let inner_links = {};
725 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
726 if (x >= width || msg[i] == "\n") {
728 push_inner_link(y, chunk.length);
734 if (msg[i] == "\n") {
739 if (msg[i] != "\n") {
742 if (i in link_data) {
746 } else if (url_ends.includes(i)) {
747 push_inner_link(y, x);
753 push_inner_link(lines.length - 1, chunk.length);
755 return [lines, inner_links];
757 log_msg: function(msg) {
759 while (this.log.length > 100) {
764 draw_map: function() {
765 let map_lines_split = [];
767 let map_content = game.map;
768 if (this.map_mode == 'control') {
769 map_content = game.map_control;
771 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
772 if (j == game.map_size[1]) {
773 map_lines_split.push(line);
777 line.push(map_content[i] + ' ');
779 map_lines_split.push(line);
780 if (this.map_mode == 'annotations') {
781 for (const coordinate of explorer.info_hints) {
782 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
784 } else if (this.map_mode == 'terrain') {
785 for (const p in game.portals) {
786 let coordinate = p.split(',')
787 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
789 let used_positions = [];
790 for (const thing_id in game.things) {
791 let t = game.things[thing_id];
792 let symbol = game.thing_types[t.type_];
795 meta_char = t.player_char;
797 if (used_positions.includes(t.position.toString())) {
800 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
801 used_positions.push(t.position.toString());
804 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
805 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
808 if (game.map_geometry == 'Square') {
809 for (let line_split of map_lines_split) {
810 map_lines.push(line_split.join(''));
812 } else if (game.map_geometry == 'Hex') {
814 for (let line_split of map_lines_split) {
815 map_lines.push(' '.repeat(indent) + line_split.join(''));
823 let window_center = [terminal.rows / 2, this.window_width / 2];
824 let player = game.things[game.player_id];
825 let center_position = [player.position[0], player.position[1]];
826 if (tui.mode.shows_info) {
827 center_position = [explorer.position[0], explorer.position[1]];
829 center_position[1] = center_position[1] * 2;
830 let offset = [center_position[0] - window_center[0],
831 center_position[1] - window_center[1]]
832 if (game.map_geometry == 'Hex' && offset[0] % 2) {
835 let term_y = Math.max(0, -offset[0]);
836 let term_x = Math.max(0, -offset[1]);
837 let map_y = Math.max(0, offset[0]);
838 let map_x = Math.max(0, offset[1]);
839 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
840 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
841 terminal.write(term_y, term_x, to_draw);
844 draw_mode_line: function() {
845 let help = 'hit [' + this.keys.help + '] for help';
846 if (this.mode.has_input_prompt) {
847 help = 'enter /help for help';
849 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
851 draw_turn_line: function(n) {
852 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
854 draw_history: function() {
855 let log_display_lines = [];
857 let y_offset_in_log = 0;
858 for (let line of this.log) {
859 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
861 log_display_lines = log_display_lines.concat(new_lines);
862 for (const y in link_data) {
863 const rel_y = y_offset_in_log + parseInt(y);
864 log_links[rel_y] = [];
865 for (let link of link_data[y]) {
866 log_links[rel_y].push(link);
869 y_offset_in_log += new_lines.length;
871 let i = log_display_lines.length - 1;
872 for (let y = terminal.rows - 1 - this.height_input;
873 y >= this.height_header && i >= 0;
875 terminal.write(y, this.window_width, log_display_lines[i]);
877 for (const key of Object.keys(log_links)) {
878 if (parseInt(key) <= i) {
879 delete log_links[key];
882 let offset = [terminal.rows - this.height_input - log_display_lines.length,
884 this.offset_links(offset, log_links);
886 draw_info: function() {
887 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
889 let offset = [this.height_header, this.window_width];
890 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
891 terminal.write(y, offset[1], lines[i]);
893 this.offset_links(offset, link_data);
895 draw_input: function() {
896 if (this.mode.has_input_prompt) {
897 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
898 terminal.write(y, this.window_width, this.input_lines[i]);
902 draw_help: function() {
903 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
904 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
905 if (this.mode.name == 'play') {
906 content += "Available actions:\n";
907 if (game.tasks.includes('MOVE')) {
908 content += "[" + movement_keys_desc + "] – move player\n";
910 if (game.tasks.includes('PICK_UP')) {
911 content += "[" + this.keys.take_thing + "] – take thing under player\n";
913 if (game.tasks.includes('DROP')) {
914 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
916 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
917 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
919 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
921 } else if (this.mode.name == 'study') {
922 content += "Available actions:\n";
923 content += '[' + movement_keys_desc + '] – move question mark\n';
924 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
926 } else if (this.mode.name == 'chat') {
927 content += '/nick NAME – re-name yourself to NAME\n';
928 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
929 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
931 content += this.mode.list_available_modes();
933 if (!this.mode.has_input_prompt) {
934 start_x = this.window_width
936 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
937 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
938 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
939 terminal.write(y, start_x, lines[i]);
942 full_refresh: function() {
944 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
945 if (this.mode.is_intro) {
949 if (game.turn_complete) {
951 this.draw_turn_line();
953 this.draw_mode_line();
954 if (this.mode.shows_info) {
961 if (this.show_help) {
973 this.map_control = "";
974 this.map_size = [0,0];
979 get_thing: function(id_, create_if_not_found=false) {
980 if (id_ in game.things) {
981 return game.things[id_];
982 } else if (create_if_not_found) {
983 let t = new Thing([0,0]);
984 game.things[id_] = t;
988 move: function(start_position, direction) {
989 let target = [start_position[0], start_position[1]];
990 if (direction == 'LEFT') {
992 } else if (direction == 'RIGHT') {
994 } else if (game.map_geometry == 'Square') {
995 if (direction == 'UP') {
997 } else if (direction == 'DOWN') {
1000 } else if (game.map_geometry == 'Hex') {
1001 let start_indented = start_position[0] % 2;
1002 if (direction == 'UPLEFT') {
1004 if (!start_indented) {
1007 } else if (direction == 'UPRIGHT') {
1009 if (start_indented) {
1012 } else if (direction == 'DOWNLEFT') {
1014 if (!start_indented) {
1017 } else if (direction == 'DOWNRIGHT') {
1019 if (start_indented) {
1024 if (target[0] < 0 || target[1] < 0 ||
1025 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1030 teleport: function() {
1031 let player = this.get_thing(game.player_id);
1032 if (player.position in this.portals) {
1033 server.reconnect_to(this.portals[player.position]);
1035 terminal.blink_screen();
1036 tui.log_msg('? not standing on portal')
1044 server.init(websocket_location);
1050 move: function(direction) {
1051 let target = game.move(this.position, direction);
1053 this.position = target
1054 if (tui.mode.shows_info) {
1056 } else if (tui.mode.name == 'control_tile_draw') {
1057 this.send_tile_control_command();
1060 terminal.blink_screen();
1063 update_info_db: function(yx, str) {
1064 this.info_db[yx] = str;
1065 if (tui.mode.name == 'study') {
1069 empty_info_db: function() {
1071 this.info_hints = [];
1072 if (tui.mode.name == 'study') {
1076 query_info: function() {
1077 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1079 get_info: function() {
1080 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1081 if (game.fov[position_i] != '.') {
1082 return 'outside field of view';
1085 let terrain_char = game.map[position_i]
1086 let terrain_desc = '?'
1087 if (game.terrains[terrain_char]) {
1088 terrain_desc = game.terrains[terrain_char];
1090 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1091 let protection = game.map_control[position_i];
1092 if (protection == '.') {
1093 protection = 'unprotected';
1095 info += 'PROTECTION: ' + protection + '\n';
1096 for (let t_id in game.things) {
1097 let t = game.things[t_id];
1098 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1099 let symbol = game.thing_types[t.type_];
1100 info += "THING: " + t.type_ + " / " + symbol;
1101 if (t.player_char) {
1102 info += t.player_char;
1105 info += " (" + t.name_ + ")";
1110 if (this.position in game.portals) {
1111 info += "PORTAL: " + game.portals[this.position] + "\n";
1113 if (this.position in this.info_db) {
1114 info += "ANNOTATIONS: " + this.info_db[this.position];
1116 info += 'waiting …';
1120 annotate: function(msg) {
1121 if (msg.length == 0) {
1122 msg = " "; // triggers annotation deletion
1124 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1126 set_portal: function(msg) {
1127 if (msg.length == 0) {
1128 msg = " "; // triggers portal deletion
1130 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1132 send_tile_control_command: function() {
1133 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1137 tui.inputEl.addEventListener('input', (event) => {
1138 if (tui.mode.has_input_prompt) {
1139 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1140 if (tui.inputEl.value.length > max_length) {
1141 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1143 tui.recalc_input_lines();
1144 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1145 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1146 tui.switch_mode('play');
1147 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1148 tui.tile_control_char = tui.inputEl.value[0];
1149 tui.switch_mode('control_pw_pw');
1150 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1151 tui.tile_control_char = tui.inputEl.value[0];
1152 tui.switch_mode('control_tile_draw');
1156 tui.inputEl.addEventListener('keydown', (event) => {
1157 tui.show_help = false;
1158 if (event.key == 'Enter') {
1159 event.preventDefault();
1161 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1162 tui.show_help = true;
1164 tui.restore_input_values();
1165 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1166 && !tui.mode.is_single_char_entry) {
1167 tui.show_help = true;
1168 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1169 tui.login_name = tui.inputEl.value;
1170 server.send(['LOGIN', tui.inputEl.value]);
1172 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1173 if (tui.inputEl.value.length == 0) {
1174 tui.log_msg('@ aborted');
1176 server.send(['SET_MAP_CONTROL_PASSWORD',
1177 tui.tile_control_char, tui.inputEl.value]);
1179 tui.switch_mode('play');
1180 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1181 explorer.set_portal(tui.inputEl.value);
1182 tui.switch_mode('play');
1183 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1184 explorer.annotate(tui.inputEl.value);
1185 tui.switch_mode('play');
1186 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1187 if (tui.inputEl.value.length == 0) {
1188 tui.inputEl.value = " ";
1190 tui.password = tui.inputEl.value
1191 tui.switch_mode('play');
1192 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1193 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1194 tui.switch_mode('play');
1195 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1196 let tokens = parser.tokenize(tui.inputEl.value);
1197 if (tokens.length > 0 && tokens[0].length > 0) {
1198 if (tui.inputEl.value[0][0] == '/') {
1199 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1200 tui.switch_mode('play');
1201 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1202 tui.switch_mode('study');
1203 } else if (tokens[0].slice(1) == 'nick') {
1204 if (tokens.length > 1) {
1205 server.send(['NICK', tokens[1]]);
1207 tui.log_msg('? need new name');
1210 tui.log_msg('? unknown command');
1213 server.send(['ALL', tui.inputEl.value]);
1215 } else if (tui.inputEl.valuelength > 0) {
1216 server.send(['ALL', tui.inputEl.value]);
1219 } else if (tui.mode.name == 'play') {
1220 if (tui.mode.mode_switch_on_key(event)) {
1222 } else if (event.key === tui.keys.flatten
1223 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1224 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1225 } else if (event.key === tui.keys.take_thing
1226 && game.tasks.includes('PICK_UP')) {
1227 server.send(["TASK:PICK_UP"]);
1228 } else if (event.key === tui.keys.drop_thing
1229 && game.tasks.includes('DROP')) {
1230 server.send(["TASK:DROP"]);
1231 } else if (event.key in tui.movement_keys
1232 && game.tasks.includes('MOVE')) {
1233 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1234 } else if (event.key === tui.keys.teleport) {
1236 } else if (event.key === tui.keys.switch_to_portal) {
1237 event.preventDefault();
1238 tui.switch_mode('portal');
1239 } else if (event.key === tui.keys.switch_to_annotate) {
1240 event.preventDefault();
1241 tui.switch_mode('annotate');
1243 } else if (tui.mode.name == 'study') {
1244 if (tui.mode.mode_switch_on_key(event)) {
1246 } else if (event.key in tui.movement_keys) {
1247 explorer.move(tui.movement_keys[event.key]);
1248 } else if (event.key == tui.keys.toggle_map_mode) {
1249 if (tui.map_mode == 'terrain') {
1250 tui.map_mode = 'annotations';
1251 } else if (tui.map_mode == 'annotations') {
1252 tui.map_mode = 'control';
1254 tui.map_mode = 'terrain';
1257 } else if (tui.mode.name == 'control_tile_draw') {
1258 if (tui.mode.mode_switch_on_key(event)) {
1260 } else if (event.key in tui.movement_keys) {
1261 explorer.move(tui.movement_keys[event.key]);
1267 rows_selector.addEventListener('input', function() {
1268 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1271 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1272 terminal.initialize();
1275 cols_selector.addEventListener('input', function() {
1276 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1279 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1280 terminal.initialize();
1281 tui.window_width = terminal.cols / 2,
1284 for (let key_selector of key_selectors) {
1285 key_selector.addEventListener('input', function() {
1286 window.localStorage.setItem(key_selector.id, key_selector.value);
1290 window.setInterval(function() {
1291 if (server.connected) {
1292 server.send(['PING']);
1294 server.reconnect_to(server.url);
1295 tui.log_msg('@ attempting reconnect …')
1298 document.getElementById("terminal").onclick = function() {
1299 tui.inputEl.focus();
1301 document.getElementById("help").onclick = function() {
1302 tui.show_help = true;
1305 document.getElementById("switch_to_play").onclick = function() {
1306 tui.switch_mode('play');
1309 document.getElementById("switch_to_study").onclick = function() {
1310 tui.switch_mode('study');
1313 document.getElementById("switch_to_chat").onclick = function() {
1314 tui.switch_mode('chat');
1317 document.getElementById("switch_to_password").onclick = function() {
1318 tui.switch_mode('password');
1321 document.getElementById("switch_to_edit").onclick = function() {
1322 tui.switch_mode('edit');
1325 document.getElementById("switch_to_annotate").onclick = function() {
1326 tui.switch_mode('annotate');
1329 document.getElementById("switch_to_portal").onclick = function() {
1330 tui.switch_mode('portal');
1333 document.getElementById("switch_to_admin").onclick = function() {
1334 tui.switch_mode('admin');
1337 document.getElementById("switch_to_control_pw_type").onclick = function() {
1338 tui.switch_mode('control_pw_type');
1341 document.getElementById("switch_to_control_tile_type").onclick = function() {
1342 tui.switch_mode('control_tile_type');
1345 document.getElementById("toggle_map_mode").onclick = function() {
1346 if (tui.map_mode == 'terrain') {
1347 tui.map_mode = 'annotations';
1348 } else if (tui.map_mode == 'annotations') {
1349 tui.map_mode = 'control';
1351 tui.map_mode = 'terrain';
1355 document.getElementById("take_thing").onclick = function() {
1356 server.send(['TASK:PICK_UP']);
1358 document.getElementById("drop_thing").onclick = function() {
1359 server.send(['TASK:DROP']);
1361 document.getElementById("flatten").onclick = function() {
1362 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1364 document.getElementById("teleport").onclick = function() {
1367 document.getElementById("move_upleft").onclick = function() {
1368 if (tui.mode.name == 'play') {
1369 server.send(['TASK:MOVE', 'UPLEFT']);
1371 explorer.move('UPLEFT');
1374 document.getElementById("move_left").onclick = function() {
1375 if (tui.mode.name == 'play') {
1376 server.send(['TASK:MOVE', 'LEFT']);
1378 explorer.move('LEFT');
1381 document.getElementById("move_downleft").onclick = function() {
1382 if (tui.mode.name == 'play') {
1383 server.send(['TASK:MOVE', 'DOWNLEFT']);
1385 explorer.move('DOWNLEFT');
1388 document.getElementById("move_down").onclick = function() {
1389 if (tui.mode.name == 'play') {
1390 server.send(['TASK:MOVE', 'DOWN']);
1392 explorer.move('DOWN');
1395 document.getElementById("move_up").onclick = function() {
1396 if (tui.mode.name == 'play') {
1397 server.send(['TASK:MOVE', 'UP']);
1399 explorer.move('UP');
1402 document.getElementById("move_upright").onclick = function() {
1403 if (tui.mode.name == 'play') {
1404 server.send(['TASK:MOVE', 'UPRIGHT']);
1406 explorer.move('UPRIGHT');
1409 document.getElementById("move_right").onclick = function() {
1410 if (tui.mode.name == 'play') {
1411 server.send(['TASK:MOVE', 'RIGHT']);
1413 explorer.move('RIGHT');
1416 document.getElementById("move_downright").onclick = function() {
1417 if (tui.mode.name == 'play') {
1418 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1420 explorer.move('DOWNRIGHT');