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 style="text-align: right"><button id="move_upleft">up-left</button></td>
17 <td style="text-align: center"><button id="move_up">up</button></td>
18 <td><button id="move_upright">up-right</button></td>
21 <td style="text-align: right;"><button id="move_left">left</button></td>
22 <td stlye="text-align: center;">move</td>
23 <td><button id="move_right">right</button></td>
26 <td><button id="move_downleft">down-left</button></td>
27 <td style="text-align: center"><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 if (game.map_geometry == 'Square') {
553 this.movement_keys = {
554 [this.keys.square_move_up]: 'UP',
555 [this.keys.square_move_left]: 'LEFT',
556 [this.keys.square_move_down]: 'DOWN',
557 [this.keys.square_move_right]: 'RIGHT'
559 document.getElementById("move_upright").hidden = true;
560 document.getElementById("move_upleft").hidden = true;
561 document.getElementById("move_downright").hidden = true;
562 document.getElementById("move_downleft").hidden = true;
563 document.getElementById("move_up").hidden = false;
564 document.getElementById("move_down").hidden = false;
565 } else if (game.map_geometry == 'Hex') {
566 document.getElementById("move_upright").hidden = false;
567 document.getElementById("move_upleft").hidden = false;
568 document.getElementById("move_downright").hidden = false;
569 document.getElementById("move_downleft").hidden = false;
570 document.getElementById("move_up").hidden = true;
571 document.getElementById("move_down").hidden = true;
572 this.movement_keys = {
573 [this.keys.hex_move_upleft]: 'UPLEFT',
574 [this.keys.hex_move_upright]: 'UPRIGHT',
575 [this.keys.hex_move_right]: 'RIGHT',
576 [this.keys.hex_move_downright]: 'DOWNRIGHT',
577 [this.keys.hex_move_downleft]: 'DOWNLEFT',
578 [this.keys.hex_move_left]: 'LEFT'
582 switch_mode: function(mode_name) {
583 this.inputEl.focus();
584 this.map_mode = 'terrain';
585 this.mode = this['mode_' + mode_name];
586 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
587 explorer.position = game.things[game.player_id].position;
588 if (this.mode.shows_info) {
589 explorer.query_info();
590 } else if (this.mode.name == 'control_tile_draw') {
591 explorer.send_tile_control_command();
592 this.map_mode = 'control';
596 this.restore_input_values();
597 for (let el of document.getElementsByTagName("button")) {
600 document.getElementById("help").disabled = false;
601 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
602 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
603 move_key.disabled = false;
606 if (!this.mode.is_intro && this.mode.name != 'play') {
607 document.getElementById("switch_to_play").disabled = false;
609 if (!this.mode.is_intro && this.mode.name != 'study') {
610 document.getElementById("switch_to_study").disabled = false;
612 if (!this.mode.is_intro && this.mode.name != 'chat') {
613 document.getElementById("switch_to_chat").disabled = false;
615 if (this.mode.name == 'login') {
616 if (this.login_name) {
617 server.send(['LOGIN', this.login_name]);
619 this.log_msg("? need login name");
621 } else if (this.mode.name == 'play') {
622 if (game.tasks.includes('PICK_UP')) {
623 document.getElementById("take_thing").disabled = false;
625 if (game.tasks.includes('DROP')) {
626 document.getElementById("drop_thing").disabled = false;
628 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
629 document.getElementById("flatten").disabled = false;
631 if (game.tasks.includes('MOVE')) {
633 document.getElementById("teleport").disabled = false;
634 document.getElementById("switch_to_annotate").disabled = false;
635 document.getElementById("switch_to_edit").disabled = false;
636 document.getElementById("switch_to_portal").disabled = false;
637 document.getElementById("switch_to_password").disabled = false;
638 document.getElementById("switch_to_admin").disabled = false;
639 document.getElementById("switch_to_control_pw_type").disabled = false;
640 document.getElementById("switch_to_control_tile_type").disabled = false;
641 } else if (this.mode.name == 'study') {
642 document.getElementById("toggle_map_mode").disabled = false;
643 } else if (this.mode.is_single_char_entry) {
644 this.show_help = true;
645 } else if (this.mode.name == 'admin') {
646 this.log_msg('@ enter admin password:')
647 } else if (this.mode.name == 'control_pw_pw') {
648 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
649 } else if (this.mode.name == 'control_pw_pw') {
650 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
654 offset_links: function(offset, links) {
655 for (let y in links) {
656 let real_y = offset[0] + parseInt(y);
657 if (!this.links[real_y]) {
658 this.links[real_y] = [];
660 for (let link of links[y]) {
661 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
662 this.links[real_y].push(offset_link);
666 restore_input_values: function() {
667 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
668 let info = explorer.info_db[explorer.position];
669 if (info != "(none)") {
670 this.inputEl.value = info;
671 this.recalc_input_lines();
673 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
674 let portal = game.portals[explorer.position]
675 this.inputEl.value = portal;
676 this.recalc_input_lines();
677 } else if (this.mode.name == 'password') {
678 this.inputEl.value = this.password;
679 this.recalc_input_lines();
682 empty_input: function(str) {
683 this.inputEl.value = "";
684 if (this.mode.has_input_prompt) {
685 this.recalc_input_lines();
687 this.height_input = 0;
690 recalc_input_lines: function() {
692 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
693 this.height_input = this.input_lines.length;
695 msg_into_lines_of_width: function(msg, width) {
696 function push_inner_link(y, end_x) {
697 if (!inner_links[y]) {
700 inner_links[y].push([url_start_x, end_x, url]);
702 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
705 for (const match of matches) {
706 const url = match[0];
707 const url_start = match.index;
708 const url_end = match.index + match[0].length;
709 link_data[url_start] = url;
710 url_ends.push(url_end);
714 let inner_links = {};
718 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
719 if (x >= width || msg[i] == "\n") {
721 push_inner_link(y, chunk.length);
727 if (msg[i] == "\n") {
732 if (msg[i] != "\n") {
735 if (i in link_data) {
739 } else if (url_ends.includes(i)) {
740 push_inner_link(y, x);
746 push_inner_link(lines.length - 1, chunk.length);
748 return [lines, inner_links];
750 log_msg: function(msg) {
752 while (this.log.length > 100) {
757 draw_map: function() {
758 let map_lines_split = [];
760 let map_content = game.map;
761 if (this.map_mode == 'control') {
762 map_content = game.map_control;
764 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
765 if (j == game.map_size[1]) {
766 map_lines_split.push(line);
770 line.push(map_content[i] + ' ');
772 map_lines_split.push(line);
773 if (this.map_mode == 'annotations') {
774 for (const coordinate of explorer.info_hints) {
775 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
777 } else if (this.map_mode == 'terrain') {
778 for (const p in game.portals) {
779 let coordinate = p.split(',')
780 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
782 let used_positions = [];
783 for (const thing_id in game.things) {
784 let t = game.things[thing_id];
785 let symbol = game.thing_types[t.type_];
788 meta_char = t.player_char;
790 if (used_positions.includes(t.position.toString())) {
793 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
794 used_positions.push(t.position.toString());
797 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
798 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
801 if (game.map_geometry == 'Square') {
802 for (let line_split of map_lines_split) {
803 map_lines.push(line_split.join(''));
805 } else if (game.map_geometry == 'Hex') {
807 for (let line_split of map_lines_split) {
808 map_lines.push(' '.repeat(indent) + line_split.join(''));
816 let window_center = [terminal.rows / 2, this.window_width / 2];
817 let player = game.things[game.player_id];
818 let center_position = [player.position[0], player.position[1]];
819 if (tui.mode.shows_info) {
820 center_position = [explorer.position[0], explorer.position[1]];
822 center_position[1] = center_position[1] * 2;
823 let offset = [center_position[0] - window_center[0],
824 center_position[1] - window_center[1]]
825 if (game.map_geometry == 'Hex' && offset[0] % 2) {
828 let term_y = Math.max(0, -offset[0]);
829 let term_x = Math.max(0, -offset[1]);
830 let map_y = Math.max(0, offset[0]);
831 let map_x = Math.max(0, offset[1]);
832 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
833 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
834 terminal.write(term_y, term_x, to_draw);
837 draw_mode_line: function() {
838 let help = 'hit [' + this.keys.help + '] for help';
839 if (this.mode.has_input_prompt) {
840 help = 'enter /help for help';
842 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
844 draw_turn_line: function(n) {
845 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
847 draw_history: function() {
848 let log_display_lines = [];
850 let y_offset_in_log = 0;
851 for (let line of this.log) {
852 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
854 log_display_lines = log_display_lines.concat(new_lines);
855 for (const y in link_data) {
856 const rel_y = y_offset_in_log + parseInt(y);
857 log_links[rel_y] = [];
858 for (let link of link_data[y]) {
859 log_links[rel_y].push(link);
862 y_offset_in_log += new_lines.length;
864 let i = log_display_lines.length - 1;
865 for (let y = terminal.rows - 1 - this.height_input;
866 y >= this.height_header && i >= 0;
868 terminal.write(y, this.window_width, log_display_lines[i]);
870 for (const key of Object.keys(log_links)) {
871 if (parseInt(key) <= i) {
872 delete log_links[key];
875 let offset = [terminal.rows - this.height_input - log_display_lines.length,
877 this.offset_links(offset, log_links);
879 draw_info: function() {
880 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
882 let offset = [this.height_header, this.window_width];
883 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
884 terminal.write(y, offset[1], lines[i]);
886 this.offset_links(offset, link_data);
888 draw_input: function() {
889 if (this.mode.has_input_prompt) {
890 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
891 terminal.write(y, this.window_width, this.input_lines[i]);
895 draw_help: function() {
896 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
897 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
898 if (this.mode.name == 'play') {
899 content += "Available actions:\n";
900 if (game.tasks.includes('MOVE')) {
901 content += "[" + movement_keys_desc + "] – move player\n";
903 if (game.tasks.includes('PICK_UP')) {
904 content += "[" + this.keys.take_thing + "] – take thing under player\n";
906 if (game.tasks.includes('DROP')) {
907 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
909 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
910 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
912 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
914 } else if (this.mode.name == 'study') {
915 content += "Available actions:\n";
916 content += '[' + movement_keys_desc + '] – move question mark\n';
917 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
919 } else if (this.mode.name == 'chat') {
920 content += '/nick NAME – re-name yourself to NAME\n';
921 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
922 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
924 content += this.mode.list_available_modes();
926 if (!this.mode.has_input_prompt) {
927 start_x = this.window_width
929 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
930 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
931 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
932 terminal.write(y, start_x, lines[i]);
935 full_refresh: function() {
937 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
938 if (this.mode.is_intro) {
942 if (game.turn_complete) {
944 this.draw_turn_line();
946 this.draw_mode_line();
947 if (this.mode.shows_info) {
954 if (this.show_help) {
966 this.map_control = "";
967 this.map_size = [0,0];
972 get_thing: function(id_, create_if_not_found=false) {
973 if (id_ in game.things) {
974 return game.things[id_];
975 } else if (create_if_not_found) {
976 let t = new Thing([0,0]);
977 game.things[id_] = t;
981 move: function(start_position, direction) {
982 let target = [start_position[0], start_position[1]];
983 if (direction == 'LEFT') {
985 } else if (direction == 'RIGHT') {
987 } else if (game.map_geometry == 'Square') {
988 if (direction == 'UP') {
990 } else if (direction == 'DOWN') {
993 } else if (game.map_geometry == 'Hex') {
994 let start_indented = start_position[0] % 2;
995 if (direction == 'UPLEFT') {
997 if (!start_indented) {
1000 } else if (direction == 'UPRIGHT') {
1002 if (start_indented) {
1005 } else if (direction == 'DOWNLEFT') {
1007 if (!start_indented) {
1010 } else if (direction == 'DOWNRIGHT') {
1012 if (start_indented) {
1017 if (target[0] < 0 || target[1] < 0 ||
1018 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1023 teleport: function() {
1024 let player = this.get_thing(game.player_id);
1025 if (player.position in this.portals) {
1026 server.reconnect_to(this.portals[player.position]);
1028 terminal.blink_screen();
1029 tui.log_msg('? not standing on portal')
1037 server.init(websocket_location);
1043 move: function(direction) {
1044 let target = game.move(this.position, direction);
1046 this.position = target
1047 if (tui.mode.shows_info) {
1049 } else if (tui.mode.name == 'control_tile_draw') {
1050 this.send_tile_control_command();
1053 terminal.blink_screen();
1056 update_info_db: function(yx, str) {
1057 this.info_db[yx] = str;
1058 if (tui.mode.name == 'study') {
1062 empty_info_db: function() {
1064 this.info_hints = [];
1065 if (tui.mode.name == 'study') {
1069 query_info: function() {
1070 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1072 get_info: function() {
1073 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1074 if (game.fov[position_i] != '.') {
1075 return 'outside field of view';
1078 let terrain_char = game.map[position_i]
1079 let terrain_desc = '?'
1080 if (game.terrains[terrain_char]) {
1081 terrain_desc = game.terrains[terrain_char];
1083 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1084 let protection = game.map_control[position_i];
1085 if (protection == '.') {
1086 protection = 'unprotected';
1088 info += 'PROTECTION: ' + protection + '\n';
1089 for (let t_id in game.things) {
1090 let t = game.things[t_id];
1091 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1092 let symbol = game.thing_types[t.type_];
1093 info += "THING: " + t.type_ + " / " + symbol;
1094 if (t.player_char) {
1095 info += t.player_char;
1098 info += " (" + t.name_ + ")";
1103 if (this.position in game.portals) {
1104 info += "PORTAL: " + game.portals[this.position] + "\n";
1106 if (this.position in this.info_db) {
1107 info += "ANNOTATIONS: " + this.info_db[this.position];
1109 info += 'waiting …';
1113 annotate: function(msg) {
1114 if (msg.length == 0) {
1115 msg = " "; // triggers annotation deletion
1117 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1119 set_portal: function(msg) {
1120 if (msg.length == 0) {
1121 msg = " "; // triggers portal deletion
1123 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1125 send_tile_control_command: function() {
1126 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1130 tui.inputEl.addEventListener('input', (event) => {
1131 if (tui.mode.has_input_prompt) {
1132 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1133 if (tui.inputEl.value.length > max_length) {
1134 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1136 tui.recalc_input_lines();
1137 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1138 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1139 tui.switch_mode('play');
1140 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1141 tui.tile_control_char = tui.inputEl.value[0];
1142 tui.switch_mode('control_pw_pw');
1143 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1144 tui.tile_control_char = tui.inputEl.value[0];
1145 tui.switch_mode('control_tile_draw');
1149 tui.inputEl.addEventListener('keydown', (event) => {
1150 tui.show_help = false;
1151 if (event.key == 'Enter') {
1152 event.preventDefault();
1154 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1155 tui.show_help = true;
1157 tui.restore_input_values();
1158 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1159 && !tui.mode.is_single_char_entry) {
1160 tui.show_help = true;
1161 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1162 tui.login_name = tui.inputEl.value;
1163 server.send(['LOGIN', tui.inputEl.value]);
1165 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1166 if (tui.inputEl.value.length == 0) {
1167 tui.log_msg('@ aborted');
1169 server.send(['SET_MAP_CONTROL_PASSWORD',
1170 tui.tile_control_char, tui.inputEl.value]);
1172 tui.switch_mode('play');
1173 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1174 explorer.set_portal(tui.inputEl.value);
1175 tui.switch_mode('play');
1176 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1177 explorer.annotate(tui.inputEl.value);
1178 tui.switch_mode('play');
1179 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1180 if (tui.inputEl.value.length == 0) {
1181 tui.inputEl.value = " ";
1183 tui.password = tui.inputEl.value
1184 tui.switch_mode('play');
1185 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1186 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1187 tui.switch_mode('play');
1188 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1189 let tokens = parser.tokenize(tui.inputEl.value);
1190 if (tokens.length > 0 && tokens[0].length > 0) {
1191 if (tui.inputEl.value[0][0] == '/') {
1192 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1193 tui.switch_mode('play');
1194 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1195 tui.switch_mode('study');
1196 } else if (tokens[0].slice(1) == 'nick') {
1197 if (tokens.length > 1) {
1198 server.send(['NICK', tokens[1]]);
1200 tui.log_msg('? need new name');
1203 tui.log_msg('? unknown command');
1206 server.send(['ALL', tui.inputEl.value]);
1208 } else if (tui.inputEl.valuelength > 0) {
1209 server.send(['ALL', tui.inputEl.value]);
1212 } else if (tui.mode.name == 'play') {
1213 if (tui.mode.mode_switch_on_key(event)) {
1215 } else if (event.key === tui.keys.flatten
1216 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1217 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1218 } else if (event.key === tui.keys.take_thing
1219 && game.tasks.includes('PICK_UP')) {
1220 server.send(["TASK:PICK_UP"]);
1221 } else if (event.key === tui.keys.drop_thing
1222 && game.tasks.includes('DROP')) {
1223 server.send(["TASK:DROP"]);
1224 } else if (event.key in tui.movement_keys
1225 && game.tasks.includes('MOVE')) {
1226 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1227 } else if (event.key === tui.keys.teleport) {
1229 } else if (event.key === tui.keys.switch_to_portal) {
1230 event.preventDefault();
1231 tui.switch_mode('portal');
1232 } else if (event.key === tui.keys.switch_to_annotate) {
1233 event.preventDefault();
1234 tui.switch_mode('annotate');
1236 } else if (tui.mode.name == 'study') {
1237 if (tui.mode.mode_switch_on_key(event)) {
1239 } else if (event.key in tui.movement_keys) {
1240 explorer.move(tui.movement_keys[event.key]);
1241 } else if (event.key == tui.keys.toggle_map_mode) {
1242 if (tui.map_mode == 'terrain') {
1243 tui.map_mode = 'annotations';
1244 } else if (tui.map_mode == 'annotations') {
1245 tui.map_mode = 'control';
1247 tui.map_mode = 'terrain';
1250 } else if (tui.mode.name == 'control_tile_draw') {
1251 if (tui.mode.mode_switch_on_key(event)) {
1253 } else if (event.key in tui.movement_keys) {
1254 explorer.move(tui.movement_keys[event.key]);
1260 rows_selector.addEventListener('input', function() {
1261 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1264 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1265 terminal.initialize();
1268 cols_selector.addEventListener('input', function() {
1269 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1272 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1273 terminal.initialize();
1274 tui.window_width = terminal.cols / 2,
1277 for (let key_selector of key_selectors) {
1278 key_selector.addEventListener('input', function() {
1279 window.localStorage.setItem(key_selector.id, key_selector.value);
1283 window.setInterval(function() {
1284 if (server.connected) {
1285 server.send(['PING']);
1287 server.reconnect_to(server.url);
1288 tui.log_msg('@ attempting reconnect …')
1291 document.getElementById("terminal").onclick = function() {
1292 tui.inputEl.focus();
1294 document.getElementById("help").onclick = function() {
1295 tui.show_help = true;
1298 document.getElementById("switch_to_play").onclick = function() {
1299 tui.switch_mode('play');
1302 document.getElementById("switch_to_study").onclick = function() {
1303 tui.switch_mode('study');
1306 document.getElementById("switch_to_chat").onclick = function() {
1307 tui.switch_mode('chat');
1310 document.getElementById("switch_to_password").onclick = function() {
1311 tui.switch_mode('password');
1314 document.getElementById("switch_to_edit").onclick = function() {
1315 tui.switch_mode('edit');
1318 document.getElementById("switch_to_annotate").onclick = function() {
1319 tui.switch_mode('annotate');
1322 document.getElementById("switch_to_portal").onclick = function() {
1323 tui.switch_mode('portal');
1326 document.getElementById("switch_to_admin").onclick = function() {
1327 tui.switch_mode('admin');
1330 document.getElementById("switch_to_control_pw_type").onclick = function() {
1331 tui.switch_mode('control_pw_type');
1334 document.getElementById("switch_to_control_tile_type").onclick = function() {
1335 tui.switch_mode('control_tile_type');
1338 document.getElementById("toggle_map_mode").onclick = function() {
1339 if (tui.map_mode == 'terrain') {
1340 tui.map_mode = 'annotations';
1341 } else if (tui.map_mode == 'annotations') {
1342 tui.map_mode = 'control';
1344 tui.map_mode = 'terrain';
1348 document.getElementById("take_thing").onclick = function() {
1349 server.send(['TASK:PICK_UP']);
1351 document.getElementById("drop_thing").onclick = function() {
1352 server.send(['TASK:DROP']);
1354 document.getElementById("flatten").onclick = function() {
1355 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1357 document.getElementById("teleport").onclick = function() {
1360 document.getElementById("move_upleft").onclick = function() {
1361 if (tui.mode.name == 'play') {
1362 server.send(['TASK:MOVE', 'UPLEFT']);
1364 explorer.move('UPLEFT');
1367 document.getElementById("move_left").onclick = function() {
1368 if (tui.mode.name == 'play') {
1369 server.send(['TASK:MOVE', 'LEFT']);
1371 explorer.move('LEFT');
1374 document.getElementById("move_downleft").onclick = function() {
1375 if (tui.mode.name == 'play') {
1376 server.send(['TASK:MOVE', 'DOWNLEFT']);
1378 explorer.move('DOWNLEFT');
1381 document.getElementById("move_down").onclick = function() {
1382 if (tui.mode.name == 'play') {
1383 server.send(['TASK:MOVE', 'DOWN']);
1385 explorer.move('DOWN');
1388 document.getElementById("move_up").onclick = function() {
1389 if (tui.mode.name == 'play') {
1390 server.send(['TASK:MOVE', 'UP']);
1392 explorer.move('UP');
1395 document.getElementById("move_upright").onclick = function() {
1396 if (tui.mode.name == 'play') {
1397 server.send(['TASK:MOVE', 'UPRIGHT']);
1399 explorer.move('UPRIGHT');
1402 document.getElementById("move_right").onclick = function() {
1403 if (tui.mode.name == 'play') {
1404 server.send(['TASK:MOVE', 'RIGHT']);
1406 explorer.move('RIGHT');
1409 document.getElementById("move_downright").onclick = function() {
1410 if (tui.mode.name == 'play') {
1411 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1413 explorer.move('DOWNRIGHT');