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>
46 <td><button id="switch_to_study">study mode</button></td>
47 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
50 <td><button id="switch_to_play">play mode</button></td>
54 <td><button id="take_thing">take thing</button></td>
55 <td><button id="switch_to_annotate">annotate tile</button></td>
56 <td><button id="switch_to_edit">change tile</button></td>
57 <td><button id="teleport">teleport</button></td>
60 <td><button id="drop_thing">drop thing</button></td>
61 <td><button id="flatten">flatten surroundings</button></td>
62 <td><button id="switch_to_password">change tile editing password</button></td>
63 <td><button id="switch_to_portal">edit portal link</button></td>
68 <td><button id="switch_to_admin_enter">admin mode</button></td>
72 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
75 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
82 <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 />
84 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
85 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
86 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
87 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
88 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
89 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
90 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
91 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
92 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
93 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
94 <li>help: <input id="key_help" type="text" value="h" />
95 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
96 <li>teleport: <input id="key_teleport" type="text" value="p" />
97 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
98 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
99 <li><input id="key_switch_to_chat" type="text" value="t" />
100 <li><input id="key_switch_to_play" type="text" value="p" />
101 <li><input id="key_switch_to_study" type="text" value="?" />
102 <li><input id="key_switch_to_edit" type="text" value="m" />
103 <li><input id="key_switch_to_password" type="text" value="P" />
104 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
105 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
106 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
107 <li><input id="key_switch_to_annotate" type="text" value="M" />
108 <li><input id="key_switch_to_portal" type="text" value="T" />
109 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
114 //let websocket_location = "wss://plomlompom.com/rogue_chat/";
115 let websocket_location = "ws://localhost:8000/";
120 'long': 'This mode allows you to interact with the map.'
124 '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.'},
126 'short': 'terrain edit',
127 '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.'
130 'short': 'change tiles control password',
131 '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!'
134 'short': 'change tiles control password',
135 '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.'
137 'control_tile_type': {
138 'short': 'change tiles control',
139 '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.'
141 'control_tile_draw': {
142 'short': 'change tiles control',
143 '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'
146 'short': 'annotate tile',
147 '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.'
150 'short': 'edit portal',
151 '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.'
155 '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:'
159 'long': 'Pick your player name.'
161 'waiting_for_server': {
162 'short': 'waiting for server response',
163 'long': 'Waiting for a server response.'
166 'short': 'waiting for server response',
167 'long': 'Waiting for a server response.'
170 'short': 'map edit password',
171 '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.'
174 'short': 'become admin',
175 'long': 'This mode allows you to become admin if you know an admin password.'
179 'long': 'This mode allows you access to actions limited to administrators.'
183 let rows_selector = document.getElementById("n_rows");
184 let cols_selector = document.getElementById("n_cols");
185 let key_selectors = document.querySelectorAll('[id^="key_"]');
187 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
188 const action = key_switch_selector.id.slice("key_switch_to_".length);
189 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
192 function restore_selector_value(selector) {
193 let stored_selection = window.localStorage.getItem(selector.id);
194 if (stored_selection) {
195 selector.value = stored_selection;
198 restore_selector_value(rows_selector);
199 restore_selector_value(cols_selector);
200 for (let key_selector of key_selectors) {
201 restore_selector_value(key_selector);
207 initialize: function() {
208 this.rows = rows_selector.value;
209 this.cols = cols_selector.value;
210 this.pre_el = document.getElementById("terminal");
211 this.pre_el.style.color = this.foreground;
212 this.pre_el.style.backgroundColor = this.background;
215 for (let y = 0, x = 0; y <= this.rows; x++) {
216 if (x == this.cols) {
219 this.content.push(line);
221 if (y == this.rows) {
228 blink_screen: function() {
229 this.pre_el.style.color = this.background;
230 this.pre_el.style.backgroundColor = this.foreground;
232 this.pre_el.style.color = this.foreground;
233 this.pre_el.style.backgroundColor = this.background;
236 refresh: function() {
237 function escapeHTML(str) {
239 replace(/&/g, '&').
240 replace(/</g, '<').
241 replace(/>/g, '>').
242 replace(/'/g, ''').
243 replace(/"/g, '"');
245 let pre_content = '';
246 for (let y = 0; y < this.rows; y++) {
247 let line = this.content[y].join('');
249 if (y in tui.links) {
251 for (let span of tui.links[y]) {
252 chunks.push(escapeHTML(line.slice(start_x, span[0])));
253 chunks.push('<a href="');
254 chunks.push(escapeHTML(span[2]));
256 chunks.push(escapeHTML(line.slice(span[0], span[1])));
260 chunks.push(escapeHTML(line.slice(start_x)));
262 chunks = [escapeHTML(line)];
264 for (const chunk of chunks) {
265 pre_content += chunk;
269 this.pre_el.innerHTML = pre_content;
271 write: function(start_y, start_x, msg) {
272 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
273 this.content[start_y][x] = msg[i];
276 drawBox: function(start_y, start_x, height, width) {
277 let end_y = start_y + height;
278 let end_x = start_x + width;
279 for (let y = start_y, x = start_x; y < this.rows; x++) {
287 this.content[y][x] = ' ';
291 terminal.initialize();
294 tokenize: function(str) {
299 for (let i = 0; i < str.length; i++) {
305 } else if (c == '\\') {
307 } else if (c == '"') {
312 } else if (c == '"') {
314 } else if (c === ' ') {
315 if (token.length > 0) {
323 if (token.length > 0) {
328 parse_yx: function(position_string) {
329 let coordinate_strings = position_string.split(',')
330 let position = [0, 0];
331 position[0] = parseInt(coordinate_strings[0].slice(2));
332 position[1] = parseInt(coordinate_strings[1].slice(2));
344 init: function(url) {
346 this.websocket = new WebSocket(this.url);
347 this.websocket.onopen = function(event) {
348 server.connected = true;
349 game.thing_types = {};
351 server.send(['TASKS']);
352 server.send(['TERRAINS']);
353 server.send(['THING_TYPES']);
354 tui.log_msg("@ server connected! :)");
355 tui.switch_mode('login');
357 this.websocket.onclose = function(event) {
358 server.connected = false;
359 tui.switch_mode('waiting_for_server');
360 tui.log_msg("@ server disconnected :(");
362 this.websocket.onmessage = this.handle_event;
364 reconnect_to: function(url) {
365 this.websocket.close();
368 send: function(tokens) {
369 this.websocket.send(unparser.untokenize(tokens));
371 handle_event: function(event) {
372 let tokens = parser.tokenize(event.data);
373 if (tokens[0] === 'TURN') {
374 game.turn_complete = false;
375 explorer.empty_info_db();
378 game.turn = parseInt(tokens[1]);
379 } else if (tokens[0] === 'THING') {
380 let t = game.get_thing(tokens[3], true);
381 t.position = parser.parse_yx(tokens[1]);
383 } else if (tokens[0] === 'THING_NAME') {
384 let t = game.get_thing(tokens[1], false);
388 } else if (tokens[0] === 'THING_CHAR') {
389 let t = game.get_thing(tokens[1], false);
391 t.player_char = tokens[2];
393 } else if (tokens[0] === 'TASKS') {
394 game.tasks = tokens[1].split(',');
395 tui.mode_edit.legal = game.tasks.includes('WRITE');
396 } else if (tokens[0] === 'THING_TYPE') {
397 game.thing_types[tokens[1]] = tokens[2]
398 } else if (tokens[0] === 'TERRAIN') {
399 game.terrains[tokens[1]] = tokens[2]
400 } else if (tokens[0] === 'MAP') {
401 game.map_geometry = tokens[1];
403 game.map_size = parser.parse_yx(tokens[2]);
405 } else if (tokens[0] === 'FOV') {
407 } else if (tokens[0] === 'MAP_CONTROL') {
408 game.map_control = tokens[1]
409 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
410 game.turn_complete = true;
411 if (tui.mode.name == 'post_login_wait') {
412 tui.switch_mode('play');
413 } else if (tui.mode.name == 'study') {
414 explorer.query_info();
417 } else if (tokens[0] === 'CHAT') {
418 tui.log_msg('# ' + tokens[1], 1);
419 } else if (tokens[0] === 'PLAYER_ID') {
420 game.player_id = parseInt(tokens[1]);
421 } else if (tokens[0] === 'LOGIN_OK') {
422 this.send(['GET_GAMESTATE']);
423 tui.switch_mode('post_login_wait');
424 } else if (tokens[0] === 'ADMIN_OK') {
426 tui.log_msg('@ you now have admin rights');
427 tui.switch_mode('admin');
428 } else if (tokens[0] === 'PORTAL') {
429 let position = parser.parse_yx(tokens[1]);
430 game.portals[position] = tokens[2];
431 } else if (tokens[0] === 'ANNOTATION_HINT') {
432 let position = parser.parse_yx(tokens[1]);
433 explorer.info_hints = explorer.info_hints.concat([position]);
434 } else if (tokens[0] === 'ANNOTATION') {
435 let position = parser.parse_yx(tokens[1]);
436 explorer.update_info_db(position, tokens[2]);
437 tui.restore_input_values();
439 } else if (tokens[0] === 'UNHANDLED_INPUT') {
440 tui.log_msg('? unknown command');
441 } else if (tokens[0] === 'PLAY_ERROR') {
442 tui.log_msg('? ' + tokens[1]);
443 terminal.blink_screen();
444 } else if (tokens[0] === 'ARGUMENT_ERROR') {
445 tui.log_msg('? syntax error: ' + tokens[1]);
446 } else if (tokens[0] === 'GAME_ERROR') {
447 tui.log_msg('? game error: ' + tokens[1]);
448 } else if (tokens[0] === 'PONG') {
451 tui.log_msg('? unhandled input: ' + event.data);
457 quote: function(str) {
459 for (let i = 0; i < str.length; i++) {
461 if (['"', '\\'].includes(c)) {
467 return quoted.join('');
469 to_yx: function(yx_coordinate) {
470 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
472 untokenize: function(tokens) {
473 let quoted_tokens = [];
474 for (let token of tokens) {
475 quoted_tokens.push(this.quote(token));
477 return quoted_tokens.join(" ");
482 constructor(name, has_input_prompt=false, shows_info=false,
483 is_intro=false, is_single_char_entry=false) {
485 this.short_desc = mode_helps[name].short;
486 this.available_modes = [];
487 this.has_input_prompt = has_input_prompt;
488 this.shows_info= shows_info;
489 this.is_intro = is_intro;
490 this.help_intro = mode_helps[name].long;
491 this.is_single_char_entry = is_single_char_entry;
494 *iter_available_modes() {
495 for (let mode_name of this.available_modes) {
496 let mode = tui['mode_' + mode_name];
500 let key = tui.keys['switch_to_' + mode.name];
504 list_available_modes() {
506 if (this.available_modes.length > 0) {
507 msg += 'Other modes available from here:\n';
508 for (let [mode, key] of this.iter_available_modes()) {
509 msg += '[' + key + '] – ' + mode.short_desc + '\n';
514 mode_switch_on_key(key_event) {
515 for (let [mode, key] of this.iter_available_modes()) {
516 if (key_event.key == key) {
517 event.preventDefault();
518 tui.switch_mode(mode.name);
530 window_width: terminal.cols / 2,
537 mode_waiting_for_server: new Mode('waiting_for_server',
539 mode_login: new Mode('login', true, false, true),
540 mode_post_login_wait: new Mode('post_login_wait'),
541 mode_chat: new Mode('chat', true),
542 mode_annotate: new Mode('annotate', true, true),
543 mode_play: new Mode('play'),
544 mode_study: new Mode('study', false, true),
545 mode_edit: new Mode('edit', false, false, false, true),
546 mode_control_pw_type: new Mode('control_pw_type',
547 false, false, false, true),
548 mode_portal: new Mode('portal', true, true),
549 mode_password: new Mode('password', true),
550 mode_admin_enter: new Mode('admin_enter', true),
551 mode_admin: new Mode('admin'),
552 mode_control_pw_pw: new Mode('control_pw_pw', true),
553 mode_control_tile_type: new Mode('control_tile_type',
554 false, false, false, true),
555 mode_control_tile_draw: new Mode('control_tile_draw'),
557 this.mode_play.available_modes = ["chat", "study", "edit",
558 "annotate", "portal",
559 "password", "admin_enter"];
560 this.mode_study.available_modes = ["chat", "play", "admin_enter"];
561 this.mode_admin.available_modes = ["chat", "play", "study",
563 "control_tile_type"];
564 this.mode_control_tile_draw.available_modes = ["admin_enter"];
565 this.mode = this.mode_waiting_for_server;
566 this.inputEl = document.getElementById("input");
567 this.inputEl.focus();
568 this.recalc_input_lines();
569 this.height_header = this.height_turn_line + this.height_mode_line;
570 this.log_msg("@ waiting for server connection ...");
573 init_keys: function() {
575 for (let key_selector of key_selectors) {
576 this.keys[key_selector.id.slice(4)] = key_selector.value;
578 if (game.map_geometry == 'Square') {
579 this.movement_keys = {
580 [this.keys.square_move_up]: 'UP',
581 [this.keys.square_move_left]: 'LEFT',
582 [this.keys.square_move_down]: 'DOWN',
583 [this.keys.square_move_right]: 'RIGHT'
585 document.getElementById("move_upright").hidden = true;
586 document.getElementById("move_upleft").hidden = true;
587 document.getElementById("move_downright").hidden = true;
588 document.getElementById("move_downleft").hidden = true;
589 document.getElementById("move_up").hidden = false;
590 document.getElementById("move_down").hidden = false;
591 } else if (game.map_geometry == 'Hex') {
592 document.getElementById("move_upright").hidden = false;
593 document.getElementById("move_upleft").hidden = false;
594 document.getElementById("move_downright").hidden = false;
595 document.getElementById("move_downleft").hidden = false;
596 document.getElementById("move_up").hidden = true;
597 document.getElementById("move_down").hidden = true;
598 this.movement_keys = {
599 [this.keys.hex_move_upleft]: 'UPLEFT',
600 [this.keys.hex_move_upright]: 'UPRIGHT',
601 [this.keys.hex_move_right]: 'RIGHT',
602 [this.keys.hex_move_downright]: 'DOWNRIGHT',
603 [this.keys.hex_move_downleft]: 'DOWNLEFT',
604 [this.keys.hex_move_left]: 'LEFT'
608 switch_mode: function(mode_name) {
609 this.inputEl.focus();
610 this.map_mode = 'terrain';
611 if (mode_name == 'admin_enter' && this.is_admin) {
614 this.mode = this['mode_' + mode_name];
615 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
616 explorer.position = game.things[game.player_id].position;
617 if (this.mode.shows_info) {
618 explorer.query_info();
619 } else if (this.mode.name == 'control_tile_draw') {
620 explorer.send_tile_control_command();
621 this.map_mode = 'control';
625 this.restore_input_values();
626 for (let el of document.getElementsByTagName("button")) {
629 document.getElementById("help").disabled = false;
630 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
631 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
632 move_key.disabled = false;
635 if (!this.mode.is_intro && this.mode.name != 'play') {
636 document.getElementById("switch_to_play").disabled = false;
638 if (!this.mode.is_intro && this.mode.name != 'study') {
639 document.getElementById("switch_to_study").disabled = false;
641 if (!this.mode.is_intro && this.mode.name != 'chat') {
642 document.getElementById("switch_to_chat").disabled = false;
644 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
645 document.getElementById("switch_to_admin_enter").disabled = false;
647 if (this.mode.name == 'login') {
648 if (this.login_name) {
649 server.send(['LOGIN', this.login_name]);
651 this.log_msg("? need login name");
653 } else if (this.mode.name == 'play') {
654 if (game.tasks.includes('PICK_UP')) {
655 document.getElementById("take_thing").disabled = false;
657 if (game.tasks.includes('DROP')) {
658 document.getElementById("drop_thing").disabled = false;
660 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
661 document.getElementById("flatten").disabled = false;
663 if (game.tasks.includes('MOVE')) {
665 document.getElementById("teleport").disabled = false;
666 document.getElementById("switch_to_annotate").disabled = false;
667 document.getElementById("switch_to_edit").disabled = false;
668 document.getElementById("switch_to_portal").disabled = false;
669 document.getElementById("switch_to_password").disabled = false;
670 document.getElementById("switch_to_admin_enter").disabled = false;
671 } else if (this.mode.name == 'admin') {
672 document.getElementById("switch_to_control_pw_type").disabled = false;
673 document.getElementById("switch_to_control_tile_type").disabled = false;
674 } else if (this.mode.name == 'study') {
675 document.getElementById("toggle_map_mode").disabled = false;
676 } else if (this.mode.is_single_char_entry) {
677 this.show_help = true;
678 } else if (this.mode.name == 'admin_enter') {
679 this.log_msg('@ enter admin password:')
680 } else if (this.mode.name == 'control_pw_pw') {
681 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
682 } else if (this.mode.name == 'control_pw_pw') {
683 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
687 offset_links: function(offset, links) {
688 for (let y in links) {
689 let real_y = offset[0] + parseInt(y);
690 if (!this.links[real_y]) {
691 this.links[real_y] = [];
693 for (let link of links[y]) {
694 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
695 this.links[real_y].push(offset_link);
699 restore_input_values: function() {
700 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
701 let info = explorer.info_db[explorer.position];
702 if (info != "(none)") {
703 this.inputEl.value = info;
704 this.recalc_input_lines();
706 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
707 let portal = game.portals[explorer.position]
708 this.inputEl.value = portal;
709 this.recalc_input_lines();
710 } else if (this.mode.name == 'password') {
711 this.inputEl.value = this.password;
712 this.recalc_input_lines();
715 empty_input: function(str) {
716 this.inputEl.value = "";
717 if (this.mode.has_input_prompt) {
718 this.recalc_input_lines();
720 this.height_input = 0;
723 recalc_input_lines: function() {
725 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
726 this.height_input = this.input_lines.length;
728 msg_into_lines_of_width: function(msg, width) {
729 function push_inner_link(y, end_x) {
730 if (!inner_links[y]) {
733 inner_links[y].push([url_start_x, end_x, url]);
735 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
738 for (const match of matches) {
739 const url = match[0];
740 const url_start = match.index;
741 const url_end = match.index + match[0].length;
742 link_data[url_start] = url;
743 url_ends.push(url_end);
747 let inner_links = {};
751 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
752 if (x >= width || msg[i] == "\n") {
754 push_inner_link(y, chunk.length);
760 if (msg[i] == "\n") {
765 if (msg[i] != "\n") {
768 if (i in link_data) {
772 } else if (url_ends.includes(i)) {
773 push_inner_link(y, x);
779 push_inner_link(lines.length - 1, chunk.length);
781 return [lines, inner_links];
783 log_msg: function(msg) {
785 while (this.log.length > 100) {
790 draw_map: function() {
791 let map_lines_split = [];
793 let map_content = game.map;
794 if (this.map_mode == 'control') {
795 map_content = game.map_control;
797 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
798 if (j == game.map_size[1]) {
799 map_lines_split.push(line);
803 line.push(map_content[i] + ' ');
805 map_lines_split.push(line);
806 if (this.map_mode == 'annotations') {
807 for (const coordinate of explorer.info_hints) {
808 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
810 } else if (this.map_mode == 'terrain') {
811 for (const p in game.portals) {
812 let coordinate = p.split(',')
813 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
815 let used_positions = [];
816 for (const thing_id in game.things) {
817 let t = game.things[thing_id];
818 let symbol = game.thing_types[t.type_];
821 meta_char = t.player_char;
823 if (used_positions.includes(t.position.toString())) {
826 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
827 used_positions.push(t.position.toString());
830 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
831 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
834 if (game.map_geometry == 'Square') {
835 for (let line_split of map_lines_split) {
836 map_lines.push(line_split.join(''));
838 } else if (game.map_geometry == 'Hex') {
840 for (let line_split of map_lines_split) {
841 map_lines.push(' '.repeat(indent) + line_split.join(''));
849 let window_center = [terminal.rows / 2, this.window_width / 2];
850 let player = game.things[game.player_id];
851 let center_position = [player.position[0], player.position[1]];
852 if (tui.mode.shows_info) {
853 center_position = [explorer.position[0], explorer.position[1]];
855 center_position[1] = center_position[1] * 2;
856 let offset = [center_position[0] - window_center[0],
857 center_position[1] - window_center[1]]
858 if (game.map_geometry == 'Hex' && offset[0] % 2) {
861 let term_y = Math.max(0, -offset[0]);
862 let term_x = Math.max(0, -offset[1]);
863 let map_y = Math.max(0, offset[0]);
864 let map_x = Math.max(0, offset[1]);
865 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
866 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
867 terminal.write(term_y, term_x, to_draw);
870 draw_mode_line: function() {
871 let help = 'hit [' + this.keys.help + '] for help';
872 if (this.mode.has_input_prompt) {
873 help = 'enter /help for help';
875 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
877 draw_turn_line: function(n) {
878 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
880 draw_history: function() {
881 let log_display_lines = [];
883 let y_offset_in_log = 0;
884 for (let line of this.log) {
885 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
887 log_display_lines = log_display_lines.concat(new_lines);
888 for (const y in link_data) {
889 const rel_y = y_offset_in_log + parseInt(y);
890 log_links[rel_y] = [];
891 for (let link of link_data[y]) {
892 log_links[rel_y].push(link);
895 y_offset_in_log += new_lines.length;
897 let i = log_display_lines.length - 1;
898 for (let y = terminal.rows - 1 - this.height_input;
899 y >= this.height_header && i >= 0;
901 terminal.write(y, this.window_width, log_display_lines[i]);
903 for (const key of Object.keys(log_links)) {
904 if (parseInt(key) <= i) {
905 delete log_links[key];
908 let offset = [terminal.rows - this.height_input - log_display_lines.length,
910 this.offset_links(offset, log_links);
912 draw_info: function() {
913 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
915 let offset = [this.height_header, this.window_width];
916 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
917 terminal.write(y, offset[1], lines[i]);
919 this.offset_links(offset, link_data);
921 draw_input: function() {
922 if (this.mode.has_input_prompt) {
923 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
924 terminal.write(y, this.window_width, this.input_lines[i]);
928 draw_help: function() {
929 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
930 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
931 if (this.mode.name == 'play') {
932 content += "Available actions:\n";
933 if (game.tasks.includes('MOVE')) {
934 content += "[" + movement_keys_desc + "] – move player\n";
936 if (game.tasks.includes('PICK_UP')) {
937 content += "[" + this.keys.take_thing + "] – take thing under player\n";
939 if (game.tasks.includes('DROP')) {
940 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
942 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
943 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
945 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
947 } else if (this.mode.name == 'study') {
948 content += "Available actions:\n";
949 content += '[' + movement_keys_desc + '] – move question mark\n';
950 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
952 } else if (this.mode.name == 'chat') {
953 content += '/nick NAME – re-name yourself to NAME\n';
954 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
955 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
956 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
958 content += this.mode.list_available_modes();
960 if (!this.mode.has_input_prompt) {
961 start_x = this.window_width
963 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
964 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
965 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
966 terminal.write(y, start_x, lines[i]);
969 full_refresh: function() {
971 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
972 if (this.mode.is_intro) {
976 if (game.turn_complete) {
978 this.draw_turn_line();
980 this.draw_mode_line();
981 if (this.mode.shows_info) {
988 if (this.show_help) {
1000 this.map_control = "";
1001 this.map_size = [0,0];
1002 this.player_id = -1;
1006 get_thing: function(id_, create_if_not_found=false) {
1007 if (id_ in game.things) {
1008 return game.things[id_];
1009 } else if (create_if_not_found) {
1010 let t = new Thing([0,0]);
1011 game.things[id_] = t;
1015 move: function(start_position, direction) {
1016 let target = [start_position[0], start_position[1]];
1017 if (direction == 'LEFT') {
1019 } else if (direction == 'RIGHT') {
1021 } else if (game.map_geometry == 'Square') {
1022 if (direction == 'UP') {
1024 } else if (direction == 'DOWN') {
1027 } else if (game.map_geometry == 'Hex') {
1028 let start_indented = start_position[0] % 2;
1029 if (direction == 'UPLEFT') {
1031 if (!start_indented) {
1034 } else if (direction == 'UPRIGHT') {
1036 if (start_indented) {
1039 } else if (direction == 'DOWNLEFT') {
1041 if (!start_indented) {
1044 } else if (direction == 'DOWNRIGHT') {
1046 if (start_indented) {
1051 if (target[0] < 0 || target[1] < 0 ||
1052 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1057 teleport: function() {
1058 let player = this.get_thing(game.player_id);
1059 if (player.position in this.portals) {
1060 server.reconnect_to(this.portals[player.position]);
1062 terminal.blink_screen();
1063 tui.log_msg('? not standing on portal')
1071 server.init(websocket_location);
1077 move: function(direction) {
1078 let target = game.move(this.position, direction);
1080 this.position = target
1081 if (tui.mode.shows_info) {
1083 } else if (tui.mode.name == 'control_tile_draw') {
1084 this.send_tile_control_command();
1087 terminal.blink_screen();
1090 update_info_db: function(yx, str) {
1091 this.info_db[yx] = str;
1092 if (tui.mode.name == 'study') {
1096 empty_info_db: function() {
1098 this.info_hints = [];
1099 if (tui.mode.name == 'study') {
1103 query_info: function() {
1104 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1106 get_info: function() {
1107 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1108 if (game.fov[position_i] != '.') {
1109 return 'outside field of view';
1112 let terrain_char = game.map[position_i]
1113 let terrain_desc = '?'
1114 if (game.terrains[terrain_char]) {
1115 terrain_desc = game.terrains[terrain_char];
1117 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1118 let protection = game.map_control[position_i];
1119 if (protection == '.') {
1120 protection = 'unprotected';
1122 info += 'PROTECTION: ' + protection + '\n';
1123 for (let t_id in game.things) {
1124 let t = game.things[t_id];
1125 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1126 let symbol = game.thing_types[t.type_];
1127 info += "THING: " + t.type_ + " / " + symbol;
1128 if (t.player_char) {
1129 info += t.player_char;
1132 info += " (" + t.name_ + ")";
1137 if (this.position in game.portals) {
1138 info += "PORTAL: " + game.portals[this.position] + "\n";
1140 if (this.position in this.info_db) {
1141 info += "ANNOTATIONS: " + this.info_db[this.position];
1143 info += 'waiting …';
1147 annotate: function(msg) {
1148 if (msg.length == 0) {
1149 msg = " "; // triggers annotation deletion
1151 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1153 set_portal: function(msg) {
1154 if (msg.length == 0) {
1155 msg = " "; // triggers portal deletion
1157 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1159 send_tile_control_command: function() {
1160 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1164 tui.inputEl.addEventListener('input', (event) => {
1165 if (tui.mode.has_input_prompt) {
1166 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1167 if (tui.inputEl.value.length > max_length) {
1168 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1170 tui.recalc_input_lines();
1171 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1172 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1173 tui.switch_mode('play');
1174 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1175 tui.tile_control_char = tui.inputEl.value[0];
1176 tui.switch_mode('control_pw_pw');
1177 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1178 tui.tile_control_char = tui.inputEl.value[0];
1179 tui.switch_mode('control_tile_draw');
1183 tui.inputEl.addEventListener('keydown', (event) => {
1184 tui.show_help = false;
1185 if (event.key == 'Enter') {
1186 event.preventDefault();
1188 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1189 tui.show_help = true;
1191 tui.restore_input_values();
1192 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1193 && !tui.mode.is_single_char_entry) {
1194 tui.show_help = true;
1195 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1196 tui.login_name = tui.inputEl.value;
1197 server.send(['LOGIN', tui.inputEl.value]);
1199 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1200 if (tui.inputEl.value.length == 0) {
1201 tui.log_msg('@ aborted');
1203 server.send(['SET_MAP_CONTROL_PASSWORD',
1204 tui.tile_control_char, tui.inputEl.value]);
1206 tui.switch_mode('admin');
1207 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1208 explorer.set_portal(tui.inputEl.value);
1209 tui.switch_mode('play');
1210 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1211 explorer.annotate(tui.inputEl.value);
1212 tui.switch_mode('play');
1213 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1214 if (tui.inputEl.value.length == 0) {
1215 tui.inputEl.value = " ";
1217 tui.password = tui.inputEl.value
1218 tui.switch_mode('play');
1219 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1220 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1221 tui.switch_mode('play');
1222 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1223 let tokens = parser.tokenize(tui.inputEl.value);
1224 if (tokens.length > 0 && tokens[0].length > 0) {
1225 if (tui.inputEl.value[0][0] == '/') {
1226 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1227 tui.switch_mode('play');
1228 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1229 tui.switch_mode('study');
1230 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1231 tui.switch_mode('admin_enter');
1232 } else if (tokens[0].slice(1) == 'nick') {
1233 if (tokens.length > 1) {
1234 server.send(['NICK', tokens[1]]);
1236 tui.log_msg('? need new name');
1239 tui.log_msg('? unknown command');
1242 server.send(['ALL', tui.inputEl.value]);
1244 } else if (tui.inputEl.valuelength > 0) {
1245 server.send(['ALL', tui.inputEl.value]);
1248 } else if (tui.mode.name == 'play') {
1249 if (tui.mode.mode_switch_on_key(event)) {
1251 } else if (event.key === tui.keys.flatten
1252 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1253 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1254 } else if (event.key === tui.keys.take_thing
1255 && game.tasks.includes('PICK_UP')) {
1256 server.send(["TASK:PICK_UP"]);
1257 } else if (event.key === tui.keys.drop_thing
1258 && game.tasks.includes('DROP')) {
1259 server.send(["TASK:DROP"]);
1260 } else if (event.key in tui.movement_keys
1261 && game.tasks.includes('MOVE')) {
1262 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1263 } else if (event.key === tui.keys.teleport) {
1265 } else if (event.key === tui.keys.switch_to_portal) {
1266 event.preventDefault();
1267 tui.switch_mode('portal');
1268 } else if (event.key === tui.keys.switch_to_annotate) {
1269 event.preventDefault();
1270 tui.switch_mode('annotate');
1272 } else if (tui.mode.name == 'study') {
1273 if (tui.mode.mode_switch_on_key(event)) {
1275 } else if (event.key in tui.movement_keys) {
1276 explorer.move(tui.movement_keys[event.key]);
1277 } else if (event.key == tui.keys.toggle_map_mode) {
1278 if (tui.map_mode == 'terrain') {
1279 tui.map_mode = 'annotations';
1280 } else if (tui.map_mode == 'annotations') {
1281 tui.map_mode = 'control';
1283 tui.map_mode = 'terrain';
1286 } else if (tui.mode.name == 'control_tile_draw') {
1287 if (tui.mode.mode_switch_on_key(event)) {
1289 } else if (event.key in tui.movement_keys) {
1290 explorer.move(tui.movement_keys[event.key]);
1292 } else if (tui.mode.name == 'admin') {
1293 if (tui.mode.mode_switch_on_key(event)) {
1300 rows_selector.addEventListener('input', function() {
1301 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1304 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1305 terminal.initialize();
1308 cols_selector.addEventListener('input', function() {
1309 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1312 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1313 terminal.initialize();
1314 tui.window_width = terminal.cols / 2,
1317 for (let key_selector of key_selectors) {
1318 key_selector.addEventListener('input', function() {
1319 window.localStorage.setItem(key_selector.id, key_selector.value);
1323 window.setInterval(function() {
1324 if (server.connected) {
1325 server.send(['PING']);
1327 server.reconnect_to(server.url);
1328 tui.log_msg('@ attempting reconnect …')
1331 document.getElementById("terminal").onclick = function() {
1332 tui.inputEl.focus();
1334 document.getElementById("help").onclick = function() {
1335 tui.show_help = true;
1338 document.getElementById("switch_to_play").onclick = function() {
1339 tui.switch_mode('play');
1342 document.getElementById("switch_to_study").onclick = function() {
1343 tui.switch_mode('study');
1346 document.getElementById("switch_to_chat").onclick = function() {
1347 tui.switch_mode('chat');
1350 document.getElementById("switch_to_password").onclick = function() {
1351 tui.switch_mode('password');
1354 document.getElementById("switch_to_edit").onclick = function() {
1355 tui.switch_mode('edit');
1358 document.getElementById("switch_to_annotate").onclick = function() {
1359 tui.switch_mode('annotate');
1362 document.getElementById("switch_to_portal").onclick = function() {
1363 tui.switch_mode('portal');
1366 document.getElementById("switch_to_admin_enter").onclick = function() {
1367 tui.switch_mode('admin');
1370 document.getElementById("switch_to_control_pw_type").onclick = function() {
1371 tui.switch_mode('control_pw_type');
1374 document.getElementById("switch_to_control_tile_type").onclick = function() {
1375 tui.switch_mode('control_tile_type');
1378 document.getElementById("toggle_map_mode").onclick = function() {
1379 if (tui.map_mode == 'terrain') {
1380 tui.map_mode = 'annotations';
1381 } else if (tui.map_mode == 'annotations') {
1382 tui.map_mode = 'control';
1384 tui.map_mode = 'terrain';
1388 document.getElementById("take_thing").onclick = function() {
1389 server.send(['TASK:PICK_UP']);
1391 document.getElementById("drop_thing").onclick = function() {
1392 server.send(['TASK:DROP']);
1394 document.getElementById("flatten").onclick = function() {
1395 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1397 document.getElementById("teleport").onclick = function() {
1400 document.getElementById("move_upleft").onclick = function() {
1401 if (tui.mode.name == 'play') {
1402 server.send(['TASK:MOVE', 'UPLEFT']);
1404 explorer.move('UPLEFT');
1407 document.getElementById("move_left").onclick = function() {
1408 if (tui.mode.name == 'play') {
1409 server.send(['TASK:MOVE', 'LEFT']);
1411 explorer.move('LEFT');
1414 document.getElementById("move_downleft").onclick = function() {
1415 if (tui.mode.name == 'play') {
1416 server.send(['TASK:MOVE', 'DOWNLEFT']);
1418 explorer.move('DOWNLEFT');
1421 document.getElementById("move_down").onclick = function() {
1422 if (tui.mode.name == 'play') {
1423 server.send(['TASK:MOVE', 'DOWN']);
1425 explorer.move('DOWN');
1428 document.getElementById("move_up").onclick = function() {
1429 if (tui.mode.name == 'play') {
1430 server.send(['TASK:MOVE', 'UP']);
1432 explorer.move('UP');
1435 document.getElementById("move_upright").onclick = function() {
1436 if (tui.mode.name == 'play') {
1437 server.send(['TASK:MOVE', 'UPRIGHT']);
1439 explorer.move('UPRIGHT');
1442 document.getElementById("move_right").onclick = function() {
1443 if (tui.mode.name == 'play') {
1444 server.send(['TASK:MOVE', 'RIGHT']);
1446 explorer.move('RIGHT');
1449 document.getElementById("move_downright").onclick = function() {
1450 if (tui.mode.name == 'play') {
1451 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1453 explorer.move('DOWNRIGHT');