7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
16 <td><button id="move_upleft">up-left</button></td>
17 <td><button id="move_up">up</button></td>
18 <td><button id="move_upright">up-right</button></td>
21 <td><button id="move_left">left</button></td>
23 <td><button id="move_right">right</button></td>
26 <td><button id="move_downleft">down-left</button></td>
27 <td><button id="move_down">down</button></td>
28 <td><button id="move_downright">down-right</button></td>
33 <td><button id="help">help</button></td>
36 <td><button id="switch_to_chat">chat mode</button><br /></td>
38 <td><button id="switch_to_study">study mode</button></td>
39 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
41 <td><button id="switch_to_play">play mode</button></td>
45 <td><button id="take_thing">take thing</button></td>
46 <td><button id="switch_to_edit">change tile</button></td>
47 <td><button id="switch_to_admin">become admin</button></td>
50 <td><button id="drop_thing">drop thing</button></td>
51 <td><button id="switch_to_password">change tile editing password</button></td>
52 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
55 <td><button id="flatten">flatten surroundings</button></td>
56 <td><button id="switch_to_annotate">annotate tile</button></td>
57 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
60 <td><button id="teleport">teleport</button></td>
61 <td><button id="switch_to_portal">edit portal link</button></td>
67 <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 />
69 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
70 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
71 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
72 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
73 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
74 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
75 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
76 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
77 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
78 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
79 <li>help: <input id="key_help" type="text" value="h" />
80 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
81 <li>teleport: <input id="key_teleport" type="text" value="p" />
82 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
83 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
84 <li><input id="key_switch_to_chat" type="text" value="t" />
85 <li><input id="key_switch_to_play" type="text" value="p" />
86 <li><input id="key_switch_to_study" type="text" value="?" />
87 <li><input id="key_switch_to_edit" type="text" value="m" />
88 <li><input id="key_switch_to_password" type="text" value="P" />
89 <li><input id="key_switch_to_admin" type="text" value="A" />
90 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
91 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
92 <li><input id="key_switch_to_annotate" type="text" value="M" />
93 <li><input id="key_switch_to_portal" type="text" value="T" />
94 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
99 let websocket_location = "wss://plomlompom.com/rogue_chat/";
100 //let websocket_location = "ws://localhost:8000/";
105 'long': 'This mode allows you to interact with the map.'
109 '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.'},
111 'short': 'terrain edit',
112 '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.'
115 'short': 'change tiles control password',
116 '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!'
119 'short': 'change tiles control password',
120 '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.'
122 'control_tile_type': {
123 'short': 'change tiles control',
124 '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.'
126 'control_tile_draw': {
127 'short': 'change tiles control',
128 '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'
131 'short': 'annotate tile',
132 '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.'
135 'short': 'edit portal',
136 '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.'
140 '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:'
144 'long': 'Pick your player name.'
146 'waiting_for_server': {
147 'short': 'waiting for server response',
148 'long': 'Waiting for a server response.'
151 'short': 'waiting for server response',
152 'long': 'Waiting for a server response.'
155 'short': 'map edit password',
156 '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.'
159 'short': 'become admin',
160 'long': 'This mode allows you to become admin if you know an admin password.'
164 let rows_selector = document.getElementById("n_rows");
165 let cols_selector = document.getElementById("n_cols");
166 let key_selectors = document.querySelectorAll('[id^="key_"]');
168 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
169 const action = key_switch_selector.id.slice("key_switch_to_".length);
170 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
173 function restore_selector_value(selector) {
174 let stored_selection = window.localStorage.getItem(selector.id);
175 if (stored_selection) {
176 selector.value = stored_selection;
179 restore_selector_value(rows_selector);
180 restore_selector_value(cols_selector);
181 for (let key_selector of key_selectors) {
182 restore_selector_value(key_selector);
188 initialize: function() {
189 this.rows = rows_selector.value;
190 this.cols = cols_selector.value;
191 this.pre_el = document.getElementById("terminal");
192 this.pre_el.style.color = this.foreground;
193 this.pre_el.style.backgroundColor = this.background;
196 for (let y = 0, x = 0; y <= this.rows; x++) {
197 if (x == this.cols) {
200 this.content.push(line);
202 if (y == this.rows) {
209 blink_screen: function() {
210 this.pre_el.style.color = this.background;
211 this.pre_el.style.backgroundColor = this.foreground;
213 this.pre_el.style.color = this.foreground;
214 this.pre_el.style.backgroundColor = this.background;
217 refresh: function() {
218 function escapeHTML(str) {
220 replace(/&/g, '&').
221 replace(/</g, '<').
222 replace(/>/g, '>').
223 replace(/'/g, ''').
224 replace(/"/g, '"');
226 let pre_content = '';
227 for (let y = 0; y < this.rows; y++) {
228 let line = this.content[y].join('');
230 if (y in tui.links) {
232 for (let span of tui.links[y]) {
233 chunks.push(escapeHTML(line.slice(start_x, span[0])));
234 chunks.push('<a href="');
235 chunks.push(escapeHTML(span[2]));
237 chunks.push(escapeHTML(line.slice(span[0], span[1])));
241 chunks.push(escapeHTML(line.slice(start_x)));
243 chunks = [escapeHTML(line)];
245 for (const chunk of chunks) {
246 pre_content += chunk;
250 this.pre_el.innerHTML = pre_content;
252 write: function(start_y, start_x, msg) {
253 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
254 this.content[start_y][x] = msg[i];
257 drawBox: function(start_y, start_x, height, width) {
258 let end_y = start_y + height;
259 let end_x = start_x + width;
260 for (let y = start_y, x = start_x; y < this.rows; x++) {
268 this.content[y][x] = ' ';
272 terminal.initialize();
275 tokenize: function(str) {
280 for (let i = 0; i < str.length; i++) {
286 } else if (c == '\\') {
288 } else if (c == '"') {
293 } else if (c == '"') {
295 } else if (c === ' ') {
296 if (token.length > 0) {
304 if (token.length > 0) {
309 parse_yx: function(position_string) {
310 let coordinate_strings = position_string.split(',')
311 let position = [0, 0];
312 position[0] = parseInt(coordinate_strings[0].slice(2));
313 position[1] = parseInt(coordinate_strings[1].slice(2));
325 init: function(url) {
327 this.websocket = new WebSocket(this.url);
328 this.websocket.onopen = function(event) {
329 server.connected = true;
330 game.thing_types = {};
332 server.send(['TASKS']);
333 server.send(['TERRAINS']);
334 server.send(['THING_TYPES']);
335 tui.log_msg("@ server connected! :)");
336 tui.switch_mode('login');
338 this.websocket.onclose = function(event) {
339 server.connected = false;
340 tui.switch_mode('waiting_for_server');
341 tui.log_msg("@ server disconnected :(");
343 this.websocket.onmessage = this.handle_event;
345 reconnect_to: function(url) {
346 this.websocket.close();
349 send: function(tokens) {
350 this.websocket.send(unparser.untokenize(tokens));
352 handle_event: function(event) {
353 let tokens = parser.tokenize(event.data);
354 if (tokens[0] === 'TURN') {
355 game.turn_complete = false;
356 explorer.empty_info_db();
359 game.turn = parseInt(tokens[1]);
360 } else if (tokens[0] === 'THING') {
361 let t = game.get_thing(tokens[3], true);
362 t.position = parser.parse_yx(tokens[1]);
364 } else if (tokens[0] === 'THING_NAME') {
365 let t = game.get_thing(tokens[1], false);
369 } else if (tokens[0] === 'THING_CHAR') {
370 let t = game.get_thing(tokens[1], false);
372 t.player_char = tokens[2];
374 } else if (tokens[0] === 'TASKS') {
375 game.tasks = tokens[1].split(',');
376 tui.mode_edit.legal = game.tasks.includes('WRITE');
377 } else if (tokens[0] === 'THING_TYPE') {
378 game.thing_types[tokens[1]] = tokens[2]
379 } else if (tokens[0] === 'TERRAIN') {
380 game.terrains[tokens[1]] = tokens[2]
381 } else if (tokens[0] === 'MAP') {
382 game.map_geometry = tokens[1];
384 game.map_size = parser.parse_yx(tokens[2]);
386 } else if (tokens[0] === 'FOV') {
388 } else if (tokens[0] === 'MAP_CONTROL') {
389 game.map_control = tokens[1]
390 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
391 game.turn_complete = true;
392 if (tui.mode.name == 'post_login_wait') {
393 tui.switch_mode('play');
394 } else if (tui.mode.name == 'study') {
395 explorer.query_info();
398 } else if (tokens[0] === 'CHAT') {
399 tui.log_msg('# ' + tokens[1], 1);
400 } else if (tokens[0] === 'PLAYER_ID') {
401 game.player_id = parseInt(tokens[1]);
402 } else if (tokens[0] === 'LOGIN_OK') {
403 this.send(['GET_GAMESTATE']);
404 tui.switch_mode('post_login_wait');
405 } else if (tokens[0] === 'PORTAL') {
406 let position = parser.parse_yx(tokens[1]);
407 game.portals[position] = tokens[2];
408 } else if (tokens[0] === 'ANNOTATION_HINT') {
409 let position = parser.parse_yx(tokens[1]);
410 explorer.info_hints = explorer.info_hints.concat([position]);
411 } else if (tokens[0] === 'ANNOTATION') {
412 let position = parser.parse_yx(tokens[1]);
413 explorer.update_info_db(position, tokens[2]);
414 tui.restore_input_values();
416 } else if (tokens[0] === 'UNHANDLED_INPUT') {
417 tui.log_msg('? unknown command');
418 } else if (tokens[0] === 'PLAY_ERROR') {
419 tui.log_msg('? ' + tokens[1]);
420 terminal.blink_screen();
421 } else if (tokens[0] === 'ARGUMENT_ERROR') {
422 tui.log_msg('? syntax error: ' + tokens[1]);
423 } else if (tokens[0] === 'GAME_ERROR') {
424 tui.log_msg('? game error: ' + tokens[1]);
425 } else if (tokens[0] === 'PONG') {
428 tui.log_msg('? unhandled input: ' + event.data);
434 quote: function(str) {
436 for (let i = 0; i < str.length; i++) {
438 if (['"', '\\'].includes(c)) {
444 return quoted.join('');
446 to_yx: function(yx_coordinate) {
447 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
449 untokenize: function(tokens) {
450 let quoted_tokens = [];
451 for (let token of tokens) {
452 quoted_tokens.push(this.quote(token));
454 return quoted_tokens.join(" ");
459 constructor(name, has_input_prompt=false, shows_info=false,
460 is_intro=false, is_single_char_entry=false) {
462 this.short_desc = mode_helps[name].short;
463 this.available_modes = [];
464 this.has_input_prompt = has_input_prompt;
465 this.shows_info= shows_info;
466 this.is_intro = is_intro;
467 this.help_intro = mode_helps[name].long;
468 this.is_single_char_entry = is_single_char_entry;
471 *iter_available_modes() {
472 for (let mode_name of this.available_modes) {
473 let mode = tui['mode_' + mode_name];
477 let key = tui.keys['switch_to_' + mode.name];
481 list_available_modes() {
483 if (this.available_modes.length > 0) {
484 msg += 'Other modes available from here:\n';
485 for (let [mode, key] of this.iter_available_modes()) {
486 msg += '[' + key + '] – ' + mode.short_desc + '\n';
491 mode_switch_on_key(key_event) {
492 for (let [mode, key] of this.iter_available_modes()) {
493 if (key_event.key == key) {
494 event.preventDefault();
495 tui.switch_mode(mode.name);
507 window_width: terminal.cols / 2,
513 mode_waiting_for_server: new Mode('waiting_for_server',
515 mode_login: new Mode('login', true, false, true),
516 mode_post_login_wait: new Mode('post_login_wait'),
517 mode_chat: new Mode('chat', true),
518 mode_annotate: new Mode('annotate', true, true),
519 mode_play: new Mode('play'),
520 mode_study: new Mode('study', false, true),
521 mode_edit: new Mode('edit', false, false, false, true),
522 mode_control_pw_type: new Mode('control_pw_type',
523 false, false, false, true),
524 mode_portal: new Mode('portal', true, true),
525 mode_password: new Mode('password', true),
526 mode_admin: new Mode('admin', true),
527 mode_control_pw_pw: new Mode('control_pw_pw', true),
528 mode_control_tile_type: new Mode('control_tile_type',
529 false, false, false, true),
530 mode_control_tile_draw: new Mode('control_tile_draw'),
532 this.mode_play.available_modes = ["chat", "study", "edit",
533 "annotate", "portal",
537 this.mode_study.available_modes = ["chat", "play"]
538 this.mode_control_tile_draw.available_modes = ["play"]
539 this.mode = this.mode_waiting_for_server;
540 this.inputEl = document.getElementById("input");
541 this.inputEl.focus();
542 this.recalc_input_lines();
543 this.height_header = this.height_turn_line + this.height_mode_line;
544 this.log_msg("@ waiting for server connection ...");
547 init_keys: function() {
549 for (let key_selector of key_selectors) {
550 this.keys[key_selector.id.slice(4)] = key_selector.value;
552 this.movement_keys = {
553 [this.keys.square_move_up]: 'UP',
554 [this.keys.square_move_left]: 'LEFT',
555 [this.keys.square_move_down]: 'DOWN',
556 [this.keys.square_move_right]: 'RIGHT'
558 if (game.map_geometry == 'Hex') {
559 this.movement_keys = {
560 [this.keys.hex_move_upleft]: 'UPLEFT',
561 [this.keys.hex_move_upright]: 'UPRIGHT',
562 [this.keys.hex_move_right]: 'RIGHT',
563 [this.keys.hex_move_downright]: 'DOWNRIGHT',
564 [this.keys.hex_move_downleft]: 'DOWNLEFT',
565 [this.keys.hex_move_left]: 'LEFT'
569 switch_mode: function(mode_name) {
570 this.inputEl.focus();
571 this.map_mode = 'terrain';
572 this.mode = this['mode_' + mode_name];
573 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
574 explorer.position = game.things[game.player_id].position;
575 if (this.mode.shows_info) {
576 explorer.query_info();
577 } else if (this.mode.name == 'control_tile_draw') {
578 explorer.send_tile_control_command();
579 this.map_mode = 'control';
583 this.restore_input_values();
584 for (let el of document.getElementsByTagName("button")) {
587 document.getElementById("help").disabled = false;
588 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
589 document.getElementById("move_left").disabled = false;
590 document.getElementById("move_right").disabled = false;
591 if (game.map_geometry == 'Hex') {
592 document.getElementById("move_upleft").disabled = false;
593 document.getElementById("move_upright").disabled = false;
594 document.getElementById("move_downleft").disabled = false;
595 document.getElementById("move_downright").disabled = false;
597 document.getElementById("move_up").disabled = false;
598 document.getElementById("move_down").disabled = false;
601 if (!this.mode.is_intro && this.mode.name != 'play') {
602 document.getElementById("switch_to_play").disabled = false;
604 if (!this.mode.is_intro && this.mode.name != 'study') {
605 document.getElementById("switch_to_study").disabled = false;
607 if (!this.mode.is_intro && this.mode.name != 'chat') {
608 document.getElementById("switch_to_chat").disabled = false;
610 if (this.mode.name == 'login') {
611 if (this.login_name) {
612 server.send(['LOGIN', this.login_name]);
614 this.log_msg("? need login name");
616 } else if (this.mode.name == 'play') {
617 if (game.tasks.includes('PICK_UP')) {
618 document.getElementById("take_thing").disabled = false;
620 if (game.tasks.includes('DROP')) {
621 document.getElementById("drop_thing").disabled = false;
623 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
624 document.getElementById("flatten").disabled = false;
626 if (game.tasks.includes('MOVE')) {
628 document.getElementById("teleport").disabled = false;
629 document.getElementById("switch_to_annotate").disabled = false;
630 document.getElementById("switch_to_edit").disabled = false;
631 document.getElementById("switch_to_portal").disabled = false;
632 document.getElementById("switch_to_password").disabled = false;
633 document.getElementById("switch_to_admin").disabled = false;
634 document.getElementById("switch_to_control_pw_type").disabled = false;
635 document.getElementById("switch_to_control_tile_type").disabled = false;
636 } else if (this.mode.name == 'study') {
637 document.getElementById("toggle_map_mode").disabled = false;
638 } else if (this.mode.is_single_char_entry) {
639 this.show_help = true;
640 } else if (this.mode.name == 'admin') {
641 this.log_msg('@ enter admin password:')
642 } else if (this.mode.name == 'control_pw_pw') {
643 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
644 } else if (this.mode.name == 'control_pw_pw') {
645 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
649 offset_links: function(offset, links) {
650 for (let y in links) {
651 let real_y = offset[0] + parseInt(y);
652 if (!this.links[real_y]) {
653 this.links[real_y] = [];
655 for (let link of links[y]) {
656 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
657 this.links[real_y].push(offset_link);
661 restore_input_values: function() {
662 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
663 let info = explorer.info_db[explorer.position];
664 if (info != "(none)") {
665 this.inputEl.value = info;
666 this.recalc_input_lines();
668 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
669 let portal = game.portals[explorer.position]
670 this.inputEl.value = portal;
671 this.recalc_input_lines();
672 } else if (this.mode.name == 'password') {
673 this.inputEl.value = this.password;
674 this.recalc_input_lines();
677 empty_input: function(str) {
678 this.inputEl.value = "";
679 if (this.mode.has_input_prompt) {
680 this.recalc_input_lines();
682 this.height_input = 0;
685 recalc_input_lines: function() {
687 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
688 this.height_input = this.input_lines.length;
690 msg_into_lines_of_width: function(msg, width) {
691 function push_inner_link(y, end_x) {
692 if (!inner_links[y]) {
695 inner_links[y].push([url_start_x, end_x, url]);
697 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
700 for (const match of matches) {
701 const url = match[0];
702 const url_start = match.index;
703 const url_end = match.index + match[0].length;
704 link_data[url_start] = url;
705 url_ends.push(url_end);
709 let inner_links = {};
713 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
714 if (x >= width || msg[i] == "\n") {
716 push_inner_link(y, chunk.length);
722 if (msg[i] == "\n") {
727 if (msg[i] != "\n") {
730 if (i in link_data) {
734 } else if (url_ends.includes(i)) {
735 push_inner_link(y, x);
741 push_inner_link(lines.length - 1, chunk.length);
743 return [lines, inner_links];
745 log_msg: function(msg) {
747 while (this.log.length > 100) {
752 draw_map: function() {
753 let map_lines_split = [];
755 let map_content = game.map;
756 if (this.map_mode == 'control') {
757 map_content = game.map_control;
759 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
760 if (j == game.map_size[1]) {
761 map_lines_split.push(line);
765 line.push(map_content[i] + ' ');
767 map_lines_split.push(line);
768 if (this.map_mode == 'annotations') {
769 for (const coordinate of explorer.info_hints) {
770 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
772 } else if (this.map_mode == 'terrain') {
773 for (const p in game.portals) {
774 let coordinate = p.split(',')
775 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
777 let used_positions = [];
778 for (const thing_id in game.things) {
779 let t = game.things[thing_id];
780 let symbol = game.thing_types[t.type_];
783 meta_char = t.player_char;
785 if (used_positions.includes(t.position.toString())) {
788 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
789 used_positions.push(t.position.toString());
792 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
793 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
796 if (game.map_geometry == 'Square') {
797 for (let line_split of map_lines_split) {
798 map_lines.push(line_split.join(''));
800 } else if (game.map_geometry == 'Hex') {
802 for (let line_split of map_lines_split) {
803 map_lines.push(' '.repeat(indent) + line_split.join(''));
811 let window_center = [terminal.rows / 2, this.window_width / 2];
812 let player = game.things[game.player_id];
813 let center_position = [player.position[0], player.position[1]];
814 if (tui.mode.shows_info) {
815 center_position = [explorer.position[0], explorer.position[1]];
817 center_position[1] = center_position[1] * 2;
818 let offset = [center_position[0] - window_center[0],
819 center_position[1] - window_center[1]]
820 if (game.map_geometry == 'Hex' && offset[0] % 2) {
823 let term_y = Math.max(0, -offset[0]);
824 let term_x = Math.max(0, -offset[1]);
825 let map_y = Math.max(0, offset[0]);
826 let map_x = Math.max(0, offset[1]);
827 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
828 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
829 terminal.write(term_y, term_x, to_draw);
832 draw_mode_line: function() {
833 let help = 'hit [' + this.keys.help + '] for help';
834 if (this.mode.has_input_prompt) {
835 help = 'enter /help for help';
837 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
839 draw_turn_line: function(n) {
840 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
842 draw_history: function() {
843 let log_display_lines = [];
845 let y_offset_in_log = 0;
846 for (let line of this.log) {
847 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
849 log_display_lines = log_display_lines.concat(new_lines);
850 for (const y in link_data) {
851 const rel_y = y_offset_in_log + parseInt(y);
852 log_links[rel_y] = [];
853 for (let link of link_data[y]) {
854 log_links[rel_y].push(link);
857 y_offset_in_log += new_lines.length;
859 let i = log_display_lines.length - 1;
860 for (let y = terminal.rows - 1 - this.height_input;
861 y >= this.height_header && i >= 0;
863 terminal.write(y, this.window_width, log_display_lines[i]);
865 for (const key of Object.keys(log_links)) {
866 if (parseInt(key) <= i) {
867 delete log_links[key];
870 let offset = [terminal.rows - this.height_input - log_display_lines.length,
872 this.offset_links(offset, log_links);
874 draw_info: function() {
875 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
877 let offset = [this.height_header, this.window_width];
878 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
879 terminal.write(y, offset[1], lines[i]);
881 this.offset_links(offset, link_data);
883 draw_input: function() {
884 if (this.mode.has_input_prompt) {
885 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
886 terminal.write(y, this.window_width, this.input_lines[i]);
890 draw_help: function() {
891 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
892 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
893 if (this.mode.name == 'play') {
894 content += "Available actions:\n";
895 if (game.tasks.includes('MOVE')) {
896 content += "[" + movement_keys_desc + "] – move player\n";
898 if (game.tasks.includes('PICK_UP')) {
899 content += "[" + this.keys.take_thing + "] – take thing under player\n";
901 if (game.tasks.includes('DROP')) {
902 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
904 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
905 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
907 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
909 } else if (this.mode.name == 'study') {
910 content += "Available actions:\n";
911 content += '[' + movement_keys_desc + '] – move question mark\n';
912 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
914 } else if (this.mode.name == 'chat') {
915 content += '/nick NAME – re-name yourself to NAME\n';
916 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
917 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
919 content += this.mode.list_available_modes();
921 if (!this.mode.has_input_prompt) {
922 start_x = this.window_width
924 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
925 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
926 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
927 terminal.write(y, start_x, lines[i]);
930 full_refresh: function() {
932 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
933 if (this.mode.is_intro) {
937 if (game.turn_complete) {
939 this.draw_turn_line();
941 this.draw_mode_line();
942 if (this.mode.shows_info) {
949 if (this.show_help) {
961 this.map_control = "";
962 this.map_size = [0,0];
967 get_thing: function(id_, create_if_not_found=false) {
968 if (id_ in game.things) {
969 return game.things[id_];
970 } else if (create_if_not_found) {
971 let t = new Thing([0,0]);
972 game.things[id_] = t;
976 move: function(start_position, direction) {
977 let target = [start_position[0], start_position[1]];
978 if (direction == 'LEFT') {
980 } else if (direction == 'RIGHT') {
982 } else if (game.map_geometry == 'Square') {
983 if (direction == 'UP') {
985 } else if (direction == 'DOWN') {
988 } else if (game.map_geometry == 'Hex') {
989 let start_indented = start_position[0] % 2;
990 if (direction == 'UPLEFT') {
992 if (!start_indented) {
995 } else if (direction == 'UPRIGHT') {
997 if (start_indented) {
1000 } else if (direction == 'DOWNLEFT') {
1002 if (!start_indented) {
1005 } else if (direction == 'DOWNRIGHT') {
1007 if (start_indented) {
1012 if (target[0] < 0 || target[1] < 0 ||
1013 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1018 teleport: function() {
1019 let player = this.get_thing(game.player_id);
1020 if (player.position in this.portals) {
1021 server.reconnect_to(this.portals[player.position]);
1023 terminal.blink_screen();
1024 tui.log_msg('? not standing on portal')
1032 server.init(websocket_location);
1038 move: function(direction) {
1039 let target = game.move(this.position, direction);
1041 this.position = target
1042 if (tui.mode.shows_info) {
1044 } else if (tui.mode.name == 'control_tile_draw') {
1045 this.send_tile_control_command();
1048 terminal.blink_screen();
1051 update_info_db: function(yx, str) {
1052 this.info_db[yx] = str;
1053 if (tui.mode.name == 'study') {
1057 empty_info_db: function() {
1059 this.info_hints = [];
1060 if (tui.mode.name == 'study') {
1064 query_info: function() {
1065 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1067 get_info: function() {
1068 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1069 if (game.fov[position_i] != '.') {
1070 return 'outside field of view';
1073 let terrain_char = game.map[position_i]
1074 let terrain_desc = '?'
1075 if (game.terrains[terrain_char]) {
1076 terrain_desc = game.terrains[terrain_char];
1078 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1079 let protection = game.map_control[position_i];
1080 if (protection == '.') {
1081 protection = 'unprotected';
1083 info += 'PROTECTION: ' + protection + '\n';
1084 for (let t_id in game.things) {
1085 let t = game.things[t_id];
1086 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1087 let symbol = game.thing_types[t.type_];
1088 info += "THING: " + t.type_ + " / " + symbol;
1089 if (t.player_char) {
1090 info += t.player_char;
1093 info += " (" + t.name_ + ")";
1098 if (this.position in game.portals) {
1099 info += "PORTAL: " + game.portals[this.position] + "\n";
1101 if (this.position in this.info_db) {
1102 info += "ANNOTATIONS: " + this.info_db[this.position];
1104 info += 'waiting …';
1108 annotate: function(msg) {
1109 if (msg.length == 0) {
1110 msg = " "; // triggers annotation deletion
1112 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1114 set_portal: function(msg) {
1115 if (msg.length == 0) {
1116 msg = " "; // triggers portal deletion
1118 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1120 send_tile_control_command: function() {
1121 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1125 tui.inputEl.addEventListener('input', (event) => {
1126 if (tui.mode.has_input_prompt) {
1127 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1128 if (tui.inputEl.value.length > max_length) {
1129 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1131 tui.recalc_input_lines();
1132 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1133 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1134 tui.switch_mode('play');
1135 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1136 tui.tile_control_char = tui.inputEl.value[0];
1137 tui.switch_mode('control_pw_pw');
1138 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1139 tui.tile_control_char = tui.inputEl.value[0];
1140 tui.switch_mode('control_tile_draw');
1144 tui.inputEl.addEventListener('keydown', (event) => {
1145 tui.show_help = false;
1146 if (event.key == 'Enter') {
1147 event.preventDefault();
1149 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1150 tui.show_help = true;
1152 tui.restore_input_values();
1153 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1154 && !tui.mode.is_single_char_entry) {
1155 tui.show_help = true;
1156 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1157 tui.login_name = tui.inputEl.value;
1158 server.send(['LOGIN', tui.inputEl.value]);
1160 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1161 if (tui.inputEl.value.length == 0) {
1162 tui.log_msg('@ aborted');
1164 server.send(['SET_MAP_CONTROL_PASSWORD',
1165 tui.tile_control_char, tui.inputEl.value]);
1167 tui.switch_mode('play');
1168 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1169 explorer.set_portal(tui.inputEl.value);
1170 tui.switch_mode('play');
1171 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1172 explorer.annotate(tui.inputEl.value);
1173 tui.switch_mode('play');
1174 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1175 if (tui.inputEl.value.length == 0) {
1176 tui.inputEl.value = " ";
1178 tui.password = tui.inputEl.value
1179 tui.switch_mode('play');
1180 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1181 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1182 tui.switch_mode('play');
1183 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1184 let tokens = parser.tokenize(tui.inputEl.value);
1185 if (tokens.length > 0 && tokens[0].length > 0) {
1186 if (tui.inputEl.value[0][0] == '/') {
1187 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1188 tui.switch_mode('play');
1189 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1190 tui.switch_mode('study');
1191 } else if (tokens[0].slice(1) == 'nick') {
1192 if (tokens.length > 1) {
1193 server.send(['NICK', tokens[1]]);
1195 tui.log_msg('? need new name');
1198 tui.log_msg('? unknown command');
1201 server.send(['ALL', tui.inputEl.value]);
1203 } else if (tui.inputEl.valuelength > 0) {
1204 server.send(['ALL', tui.inputEl.value]);
1207 } else if (tui.mode.name == 'play') {
1208 if (tui.mode.mode_switch_on_key(event)) {
1210 } else if (event.key === tui.keys.flatten
1211 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1212 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1213 } else if (event.key === tui.keys.take_thing
1214 && game.tasks.includes('PICK_UP')) {
1215 server.send(["TASK:PICK_UP"]);
1216 } else if (event.key === tui.keys.drop_thing
1217 && game.tasks.includes('DROP')) {
1218 server.send(["TASK:DROP"]);
1219 } else if (event.key in tui.movement_keys
1220 && game.tasks.includes('MOVE')) {
1221 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1222 } else if (event.key === tui.keys.teleport) {
1224 } else if (event.key === tui.keys.switch_to_portal) {
1225 event.preventDefault();
1226 tui.switch_mode('portal');
1227 } else if (event.key === tui.keys.switch_to_annotate) {
1228 event.preventDefault();
1229 tui.switch_mode('annotate');
1231 } else if (tui.mode.name == 'study') {
1232 if (tui.mode.mode_switch_on_key(event)) {
1234 } else if (event.key in tui.movement_keys) {
1235 explorer.move(tui.movement_keys[event.key]);
1236 } else if (event.key == tui.keys.toggle_map_mode) {
1237 if (tui.map_mode == 'terrain') {
1238 tui.map_mode = 'annotations';
1239 } else if (tui.map_mode == 'annotations') {
1240 tui.map_mode = 'control';
1242 tui.map_mode = 'terrain';
1245 } else if (tui.mode.name == 'control_tile_draw') {
1246 if (tui.mode.mode_switch_on_key(event)) {
1248 } else if (event.key in tui.movement_keys) {
1249 explorer.move(tui.movement_keys[event.key]);
1255 rows_selector.addEventListener('input', function() {
1256 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1259 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1260 terminal.initialize();
1263 cols_selector.addEventListener('input', function() {
1264 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1267 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1268 terminal.initialize();
1269 tui.window_width = terminal.cols / 2,
1272 for (let key_selector of key_selectors) {
1273 key_selector.addEventListener('input', function() {
1274 window.localStorage.setItem(key_selector.id, key_selector.value);
1278 window.setInterval(function() {
1279 if (server.connected) {
1280 server.send(['PING']);
1282 server.reconnect_to(server.url);
1283 tui.log_msg('@ attempting reconnect …')
1286 document.getElementById("terminal").onclick = function() {
1287 tui.inputEl.focus();
1289 document.getElementById("help").onclick = function() {
1290 tui.show_help = true;
1293 document.getElementById("switch_to_play").onclick = function() {
1294 tui.switch_mode('play');
1297 document.getElementById("switch_to_study").onclick = function() {
1298 tui.switch_mode('study');
1301 document.getElementById("switch_to_chat").onclick = function() {
1302 tui.switch_mode('chat');
1305 document.getElementById("switch_to_password").onclick = function() {
1306 tui.switch_mode('password');
1309 document.getElementById("switch_to_edit").onclick = function() {
1310 tui.switch_mode('edit');
1313 document.getElementById("switch_to_annotate").onclick = function() {
1314 tui.switch_mode('annotate');
1317 document.getElementById("switch_to_portal").onclick = function() {
1318 tui.switch_mode('portal');
1321 document.getElementById("switch_to_admin").onclick = function() {
1322 tui.switch_mode('admin');
1325 document.getElementById("switch_to_control_pw_type").onclick = function() {
1326 tui.switch_mode('control_pw_type');
1329 document.getElementById("switch_to_control_tile_type").onclick = function() {
1330 tui.switch_mode('control_tile_type');
1333 document.getElementById("toggle_map_mode").onclick = function() {
1334 if (tui.map_mode == 'terrain') {
1335 tui.map_mode = 'annotations';
1336 } else if (tui.map_mode == 'annotations') {
1337 tui.map_mode = 'control';
1339 tui.map_mode = 'terrain';
1343 document.getElementById("take_thing").onclick = function() {
1344 server.send(['TASK:PICK_UP']);
1346 document.getElementById("drop_thing").onclick = function() {
1347 server.send(['TASK:DROP']);
1349 document.getElementById("flatten").onclick = function() {
1350 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1352 document.getElementById("teleport").onclick = function() {
1355 document.getElementById("move_upleft").onclick = function() {
1356 if (tui.mode.name == 'play') {
1357 server.send(['TASK:MOVE', 'UPLEFT']);
1359 explorer.move('UPLEFT');
1362 document.getElementById("move_left").onclick = function() {
1363 if (tui.mode.name == 'play') {
1364 server.send(['TASK:MOVE', 'LEFT']);
1366 explorer.move('LEFT');
1369 document.getElementById("move_downleft").onclick = function() {
1370 if (tui.mode.name == 'play') {
1371 server.send(['TASK:MOVE', 'DOWNLEFT']);
1373 explorer.move('DOWNLEFT');
1376 document.getElementById("move_down").onclick = function() {
1377 if (tui.mode.name == 'play') {
1378 server.send(['TASK:MOVE', 'DOWN']);
1380 explorer.move('DOWN');
1383 document.getElementById("move_up").onclick = function() {
1384 if (tui.mode.name == 'play') {
1385 server.send(['TASK:MOVE', 'UP']);
1387 explorer.move('UP');
1390 document.getElementById("move_upright").onclick = function() {
1391 if (tui.mode.name == 'play') {
1392 server.send(['TASK:MOVE', 'UPRIGHT']);
1394 explorer.move('UPRIGHT');
1397 document.getElementById("move_right").onclick = function() {
1398 if (tui.mode.name == 'play') {
1399 server.send(['TASK:MOVE', 'RIGHT']);
1401 explorer.move('RIGHT');
1404 document.getElementById("move_downright").onclick = function() {
1405 if (tui.mode.name == 'play') {
1406 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1408 explorer.move('DOWNRIGHT');