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 const msg = "[" + mode_helps[action].short + "]: ";
171 key_switch_selector.parentNode.prepend(msg);
174 function restore_selector_value(selector) {
175 let stored_selection = window.localStorage.getItem(selector.id);
176 if (stored_selection) {
177 selector.value = stored_selection;
180 restore_selector_value(rows_selector);
181 restore_selector_value(cols_selector);
182 for (let key_selector of key_selectors) {
183 restore_selector_value(key_selector);
189 initialize: function() {
190 this.rows = rows_selector.value;
191 this.cols = cols_selector.value;
192 this.pre_el = document.getElementById("terminal");
193 this.pre_el.style.color = this.foreground;
194 this.pre_el.style.backgroundColor = this.background;
197 for (let y = 0, x = 0; y <= this.rows; x++) {
198 if (x == this.cols) {
201 this.content.push(line);
203 if (y == this.rows) {
210 blink_screen: function() {
211 this.pre_el.style.color = this.background;
212 this.pre_el.style.backgroundColor = this.foreground;
214 this.pre_el.style.color = this.foreground;
215 this.pre_el.style.backgroundColor = this.background;
218 refresh: function() {
219 function escapeHTML(str) {
221 replace(/&/g, '&').
222 replace(/</g, '<').
223 replace(/>/g, '>').
224 replace(/'/g, ''').
225 replace(/"/g, '"');
227 let pre_content = '';
228 for (let y = 0; y < this.rows; y++) {
229 let line = this.content[y].join('');
231 if (y in tui.links) {
233 for (let span of tui.links[y]) {
234 chunks.push(escapeHTML(line.slice(start_x, span[0])));
235 chunks.push('<a href="');
236 chunks.push(escapeHTML(span[2]));
238 chunks.push(escapeHTML(line.slice(span[0], span[1])));
242 chunks.push(escapeHTML(line.slice(start_x)));
244 chunks = [escapeHTML(line)];
246 for (const chunk of chunks) {
247 pre_content += chunk;
251 this.pre_el.innerHTML = pre_content;
253 write: function(start_y, start_x, msg) {
254 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
255 this.content[start_y][x] = msg[i];
258 drawBox: function(start_y, start_x, height, width) {
259 let end_y = start_y + height;
260 let end_x = start_x + width;
261 for (let y = start_y, x = start_x; y < this.rows; x++) {
269 this.content[y][x] = ' ';
273 terminal.initialize();
276 tokenize: function(str) {
281 for (let i = 0; i < str.length; i++) {
287 } else if (c == '\\') {
289 } else if (c == '"') {
294 } else if (c == '"') {
296 } else if (c === ' ') {
297 if (token.length > 0) {
305 if (token.length > 0) {
310 parse_yx: function(position_string) {
311 let coordinate_strings = position_string.split(',')
312 let position = [0, 0];
313 position[0] = parseInt(coordinate_strings[0].slice(2));
314 position[1] = parseInt(coordinate_strings[1].slice(2));
326 init: function(url) {
328 this.websocket = new WebSocket(this.url);
329 this.websocket.onopen = function(event) {
330 server.connected = true;
331 game.thing_types = {};
333 server.send(['TASKS']);
334 server.send(['TERRAINS']);
335 server.send(['THING_TYPES']);
336 tui.log_msg("@ server connected! :)");
337 tui.switch_mode('login');
339 this.websocket.onclose = function(event) {
340 server.connected = false;
341 tui.switch_mode('waiting_for_server');
342 tui.log_msg("@ server disconnected :(");
344 this.websocket.onmessage = this.handle_event;
346 reconnect_to: function(url) {
347 this.websocket.close();
350 send: function(tokens) {
351 this.websocket.send(unparser.untokenize(tokens));
353 handle_event: function(event) {
354 let tokens = parser.tokenize(event.data);
355 if (tokens[0] === 'TURN') {
356 game.turn_complete = false;
357 explorer.empty_info_db();
360 game.turn = parseInt(tokens[1]);
361 } else if (tokens[0] === 'THING') {
362 let t = game.get_thing(tokens[3], true);
363 t.position = parser.parse_yx(tokens[1]);
365 } else if (tokens[0] === 'THING_NAME') {
366 let t = game.get_thing(tokens[1], false);
370 } else if (tokens[0] === 'THING_CHAR') {
371 let t = game.get_thing(tokens[1], false);
373 t.player_char = tokens[2];
375 } else if (tokens[0] === 'TASKS') {
376 game.tasks = tokens[1].split(',');
377 tui.mode_edit.legal = game.tasks.includes('WRITE');
378 } else if (tokens[0] === 'THING_TYPE') {
379 game.thing_types[tokens[1]] = tokens[2]
380 } else if (tokens[0] === 'TERRAIN') {
381 game.terrains[tokens[1]] = tokens[2]
382 } else if (tokens[0] === 'MAP') {
383 game.map_geometry = tokens[1];
385 game.map_size = parser.parse_yx(tokens[2]);
387 } else if (tokens[0] === 'FOV') {
389 } else if (tokens[0] === 'MAP_CONTROL') {
390 game.map_control = tokens[1]
391 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
392 game.turn_complete = true;
393 if (tui.mode.name == 'post_login_wait') {
394 tui.switch_mode('play');
395 } else if (tui.mode.name == 'study') {
396 explorer.query_info();
399 } else if (tokens[0] === 'CHAT') {
400 tui.log_msg('# ' + tokens[1], 1);
401 } else if (tokens[0] === 'PLAYER_ID') {
402 game.player_id = parseInt(tokens[1]);
403 } else if (tokens[0] === 'LOGIN_OK') {
404 this.send(['GET_GAMESTATE']);
405 tui.switch_mode('post_login_wait');
406 } else if (tokens[0] === 'PORTAL') {
407 let position = parser.parse_yx(tokens[1]);
408 game.portals[position] = tokens[2];
409 } else if (tokens[0] === 'ANNOTATION_HINT') {
410 let position = parser.parse_yx(tokens[1]);
411 explorer.info_hints = explorer.info_hints.concat([position]);
412 } else if (tokens[0] === 'ANNOTATION') {
413 let position = parser.parse_yx(tokens[1]);
414 explorer.update_info_db(position, tokens[2]);
415 tui.restore_input_values();
417 } else if (tokens[0] === 'UNHANDLED_INPUT') {
418 tui.log_msg('? unknown command');
419 } else if (tokens[0] === 'PLAY_ERROR') {
420 tui.log_msg('? ' + tokens[1]);
421 terminal.blink_screen();
422 } else if (tokens[0] === 'ARGUMENT_ERROR') {
423 tui.log_msg('? syntax error: ' + tokens[1]);
424 } else if (tokens[0] === 'GAME_ERROR') {
425 tui.log_msg('? game error: ' + tokens[1]);
426 } else if (tokens[0] === 'PONG') {
429 tui.log_msg('? unhandled input: ' + event.data);
435 quote: function(str) {
437 for (let i = 0; i < str.length; i++) {
439 if (['"', '\\'].includes(c)) {
445 return quoted.join('');
447 to_yx: function(yx_coordinate) {
448 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
450 untokenize: function(tokens) {
451 let quoted_tokens = [];
452 for (let token of tokens) {
453 quoted_tokens.push(this.quote(token));
455 return quoted_tokens.join(" ");
460 constructor(name, has_input_prompt=false, shows_info=false,
461 is_intro=false, is_single_char_entry=false) {
463 this.short_desc = mode_helps[name].short;
464 this.available_modes = [];
465 this.has_input_prompt = has_input_prompt;
466 this.shows_info= shows_info;
467 this.is_intro = is_intro;
468 this.help_intro = mode_helps[name].long;
469 this.is_single_char_entry = is_single_char_entry;
472 *iter_available_modes() {
473 for (let mode_name of this.available_modes) {
474 let mode = tui['mode_' + mode_name];
478 let key = tui.keys['switch_to_' + mode.name];
482 list_available_modes() {
484 if (this.available_modes.length > 0) {
485 msg += 'Other modes available from here:\n';
486 for (let [mode, key] of this.iter_available_modes()) {
487 msg += '[' + key + '] – ' + mode.short_desc + '\n';
492 mode_switch_on_key(key_event) {
493 for (let [mode, key] of this.iter_available_modes()) {
494 if (key_event.key == key) {
495 event.preventDefault();
496 tui.switch_mode(mode.name);
508 window_width: terminal.cols / 2,
514 mode_waiting_for_server: new Mode('waiting_for_server',
516 mode_login: new Mode('login', true, false, true),
517 mode_post_login_wait: new Mode('post_login_wait'),
518 mode_chat: new Mode('chat', true),
519 mode_annotate: new Mode('annotate', true, true),
520 mode_play: new Mode('play'),
521 mode_study: new Mode('study', false, true),
522 mode_edit: new Mode('edit', false, false, false, true),
523 mode_control_pw_type: new Mode('control_pw_type',
524 false, false, false, true),
525 mode_portal: new Mode('portal', true, true),
526 mode_password: new Mode('password', true),
527 mode_admin: new Mode('admin', true),
528 mode_control_pw_pw: new Mode('control_pw_pw', true),
529 mode_control_tile_type: new Mode('control_tile_type',
530 false, false, false, true),
531 mode_control_tile_draw: new Mode('control_tile_draw'),
533 this.mode_play.available_modes = ["chat", "study", "edit",
534 "annotate", "portal",
538 this.mode_study.available_modes = ["chat", "play"]
539 this.mode_control_tile_draw.available_modes = ["play"]
540 this.mode = this.mode_waiting_for_server;
541 this.inputEl = document.getElementById("input");
542 this.inputEl.focus();
543 this.recalc_input_lines();
544 this.height_header = this.height_turn_line + this.height_mode_line;
545 this.log_msg("@ waiting for server connection ...");
548 init_keys: function() {
550 for (let key_selector of key_selectors) {
551 this.keys[key_selector.id.slice(4)] = key_selector.value;
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 if (game.map_geometry == 'Hex') {
560 this.movement_keys = {
561 [this.keys.hex_move_upleft]: 'UPLEFT',
562 [this.keys.hex_move_upright]: 'UPRIGHT',
563 [this.keys.hex_move_right]: 'RIGHT',
564 [this.keys.hex_move_downright]: 'DOWNRIGHT',
565 [this.keys.hex_move_downleft]: 'DOWNLEFT',
566 [this.keys.hex_move_left]: 'LEFT'
570 switch_mode: function(mode_name) {
571 this.inputEl.focus();
572 this.map_mode = 'terrain';
573 this.mode = this['mode_' + mode_name];
574 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
575 explorer.position = game.things[game.player_id].position;
576 if (this.mode.shows_info) {
577 explorer.query_info();
578 } else if (this.mode.name == 'control_tile_draw') {
579 explorer.send_tile_control_command();
580 this.map_mode = 'control';
584 this.restore_input_values();
585 document.getElementById("take_thing").disabled = true;
586 document.getElementById("drop_thing").disabled = true;
587 document.getElementById("flatten").disabled = true;
588 document.getElementById("teleport").disabled = true;
589 document.getElementById("toggle_map_mode").disabled = true;
590 document.getElementById("switch_to_chat").disabled = true;
591 document.getElementById("switch_to_play").disabled = true;
592 document.getElementById("switch_to_study").disabled = true;
593 document.getElementById("switch_to_edit").disabled = true;
594 document.getElementById("switch_to_portal").disabled = true;
595 document.getElementById("switch_to_annotate").disabled = true;
596 document.getElementById("switch_to_password").disabled = true;
597 document.getElementById("switch_to_admin").disabled = true;
598 document.getElementById("switch_to_control_pw_type").disabled = true;
599 document.getElementById("switch_to_control_tile_type").disabled = true;
600 document.getElementById("move_left").disabled = true;
601 document.getElementById("move_upleft").disabled = true;
602 document.getElementById("move_up").disabled = true;
603 document.getElementById("move_upright").disabled = true;
604 document.getElementById("move_downleft").disabled = true;
605 document.getElementById("move_down").disabled = true;
606 document.getElementById("move_downright").disabled = true;
607 document.getElementById("move_right").disabled = true;
608 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
609 document.getElementById("move_left").disabled = false;
610 document.getElementById("move_right").disabled = false;
611 if (game.map_geometry == 'Hex') {
612 document.getElementById("move_upleft").disabled = false;
613 document.getElementById("move_upright").disabled = false;
614 document.getElementById("move_downleft").disabled = false;
615 document.getElementById("move_downright").disabled = false;
617 document.getElementById("move_up").disabled = false;
618 document.getElementById("move_down").disabled = false;
621 if (!this.mode.is_intro && this.mode.name != 'play') {
622 document.getElementById("switch_to_play").disabled = false;
624 if (!this.mode.is_intro && this.mode.name != 'study') {
625 document.getElementById("switch_to_study").disabled = false;
627 if (!this.mode.is_intro && this.mode.name != 'chat') {
628 document.getElementById("switch_to_chat").disabled = false;
630 if (this.mode.name == 'login') {
631 if (this.login_name) {
632 server.send(['LOGIN', this.login_name]);
634 this.log_msg("? need login name");
636 } else if (this.mode.name == 'play') {
637 if (game.tasks.includes('PICK_UP')) {
638 document.getElementById("take_thing").disabled = false;
640 if (game.tasks.includes('DROP')) {
641 document.getElementById("drop_thing").disabled = false;
643 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
644 document.getElementById("flatten").disabled = false;
646 if (game.tasks.includes('MOVE')) {
648 document.getElementById("teleport").disabled = false;
649 document.getElementById("switch_to_annotate").disabled = false;
650 document.getElementById("switch_to_edit").disabled = false;
651 document.getElementById("switch_to_portal").disabled = false;
652 document.getElementById("switch_to_password").disabled = false;
653 document.getElementById("switch_to_admin").disabled = false;
654 document.getElementById("switch_to_control_pw_type").disabled = false;
655 document.getElementById("switch_to_control_tile_type").disabled = false;
656 } else if (this.mode.name == 'study') {
657 document.getElementById("toggle_map_mode").disabled = false;
658 } else if (this.mode.is_single_char_entry) {
659 this.show_help = true;
660 } else if (this.mode.name == 'admin') {
661 this.log_msg('@ enter admin password:')
662 } else if (this.mode.name == 'control_pw_pw') {
663 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
664 } else if (this.mode.name == 'control_pw_pw') {
665 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
669 offset_links: function(offset, links) {
670 for (let y in links) {
671 let real_y = offset[0] + parseInt(y);
672 if (!this.links[real_y]) {
673 this.links[real_y] = [];
675 for (let link of links[y]) {
676 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
677 this.links[real_y].push(offset_link);
681 restore_input_values: function() {
682 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
683 let info = explorer.info_db[explorer.position];
684 if (info != "(none)") {
685 this.inputEl.value = info;
686 this.recalc_input_lines();
688 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
689 let portal = game.portals[explorer.position]
690 this.inputEl.value = portal;
691 this.recalc_input_lines();
692 } else if (this.mode.name == 'password') {
693 this.inputEl.value = this.password;
694 this.recalc_input_lines();
697 empty_input: function(str) {
698 this.inputEl.value = "";
699 if (this.mode.has_input_prompt) {
700 this.recalc_input_lines();
702 this.height_input = 0;
705 recalc_input_lines: function() {
707 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
708 this.height_input = this.input_lines.length;
710 msg_into_lines_of_width: function(msg, width) {
711 function push_inner_link(y, end_x) {
712 if (!inner_links[y]) {
715 inner_links[y].push([url_start_x, end_x, url]);
717 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
720 for (const match of matches) {
721 const url = match[0];
722 const url_start = match.index;
723 const url_end = match.index + match[0].length;
724 link_data[url_start] = url;
725 url_ends.push(url_end);
729 let inner_links = {};
733 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
734 if (x >= width || msg[i] == "\n") {
736 push_inner_link(y, chunk.length);
742 if (msg[i] == "\n") {
747 if (msg[i] != "\n") {
750 if (i in link_data) {
754 } else if (url_ends.includes(i)) {
755 push_inner_link(y, x);
761 push_inner_link(lines.length - 1, chunk.length);
763 return [lines, inner_links];
765 log_msg: function(msg) {
767 while (this.log.length > 100) {
772 draw_map: function() {
773 let map_lines_split = [];
775 let map_content = game.map;
776 if (this.map_mode == 'control') {
777 map_content = game.map_control;
779 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
780 if (j == game.map_size[1]) {
781 map_lines_split.push(line);
785 line.push(map_content[i] + ' ');
787 map_lines_split.push(line);
788 if (this.map_mode == 'annotations') {
789 for (const coordinate of explorer.info_hints) {
790 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
792 } else if (this.map_mode == 'terrain') {
793 for (const p in game.portals) {
794 let coordinate = p.split(',')
795 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
797 let used_positions = [];
798 for (const thing_id in game.things) {
799 let t = game.things[thing_id];
800 let symbol = game.thing_types[t.type_];
803 meta_char = t.player_char;
805 if (used_positions.includes(t.position.toString())) {
808 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
809 used_positions.push(t.position.toString());
812 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
813 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
816 if (game.map_geometry == 'Square') {
817 for (let line_split of map_lines_split) {
818 map_lines.push(line_split.join(''));
820 } else if (game.map_geometry == 'Hex') {
822 for (let line_split of map_lines_split) {
823 map_lines.push(' '.repeat(indent) + line_split.join(''));
831 let window_center = [terminal.rows / 2, this.window_width / 2];
832 let player = game.things[game.player_id];
833 let center_position = [player.position[0], player.position[1]];
834 if (tui.mode.shows_info) {
835 center_position = [explorer.position[0], explorer.position[1]];
837 center_position[1] = center_position[1] * 2;
838 let offset = [center_position[0] - window_center[0],
839 center_position[1] - window_center[1]]
840 if (game.map_geometry == 'Hex' && offset[0] % 2) {
843 let term_y = Math.max(0, -offset[0]);
844 let term_x = Math.max(0, -offset[1]);
845 let map_y = Math.max(0, offset[0]);
846 let map_x = Math.max(0, offset[1]);
847 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
848 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
849 terminal.write(term_y, term_x, to_draw);
852 draw_mode_line: function() {
853 let help = 'hit [' + this.keys.help + '] for help';
854 if (this.mode.has_input_prompt) {
855 help = 'enter /help for help';
857 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
859 draw_turn_line: function(n) {
860 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
862 draw_history: function() {
863 let log_display_lines = [];
865 let y_offset_in_log = 0;
866 for (let line of this.log) {
867 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
869 log_display_lines = log_display_lines.concat(new_lines);
870 for (const y in link_data) {
871 const rel_y = y_offset_in_log + parseInt(y);
872 log_links[rel_y] = [];
873 for (let link of link_data[y]) {
874 log_links[rel_y].push(link);
877 y_offset_in_log += new_lines.length;
879 let i = log_display_lines.length - 1;
880 for (let y = terminal.rows - 1 - this.height_input;
881 y >= this.height_header && i >= 0;
883 terminal.write(y, this.window_width, log_display_lines[i]);
885 for (const key of Object.keys(log_links)) {
886 if (parseInt(key) <= i) {
887 delete log_links[key];
890 let offset = [terminal.rows - this.height_input - log_display_lines.length,
892 this.offset_links(offset, log_links);
894 draw_info: function() {
895 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
897 let offset = [this.height_header, this.window_width];
898 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
899 terminal.write(y, offset[1], lines[i]);
901 this.offset_links(offset, link_data);
903 draw_input: function() {
904 if (this.mode.has_input_prompt) {
905 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
906 terminal.write(y, this.window_width, this.input_lines[i]);
910 draw_help: function() {
911 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
912 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
913 if (this.mode.name == 'play') {
914 content += "Available actions:\n";
915 if (game.tasks.includes('MOVE')) {
916 content += "[" + movement_keys_desc + "] – move player\n";
918 if (game.tasks.includes('PICK_UP')) {
919 content += "[" + this.keys.take_thing + "] – take thing under player\n";
921 if (game.tasks.includes('DROP')) {
922 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
924 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
925 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
927 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
929 } else if (this.mode.name == 'study') {
930 content += "Available actions:\n";
931 content += '[' + movement_keys_desc + '] – move question mark\n';
932 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
934 } else if (this.mode.name == 'chat') {
935 content += '/nick NAME – re-name yourself to NAME\n';
936 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
937 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
939 content += this.mode.list_available_modes();
941 if (!this.mode.has_input_prompt) {
942 start_x = this.window_width
944 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
945 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
946 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
947 terminal.write(y, start_x, lines[i]);
950 full_refresh: function() {
952 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
953 if (this.mode.is_intro) {
957 if (game.turn_complete) {
959 this.draw_turn_line();
961 this.draw_mode_line();
962 if (this.mode.shows_info) {
969 if (this.show_help) {
981 this.map_control = "";
982 this.map_size = [0,0];
987 get_thing: function(id_, create_if_not_found=false) {
988 if (id_ in game.things) {
989 return game.things[id_];
990 } else if (create_if_not_found) {
991 let t = new Thing([0,0]);
992 game.things[id_] = t;
996 move: function(start_position, direction) {
997 let target = [start_position[0], start_position[1]];
998 if (direction == 'LEFT') {
1000 } else if (direction == 'RIGHT') {
1002 } else if (game.map_geometry == 'Square') {
1003 if (direction == 'UP') {
1005 } else if (direction == 'DOWN') {
1008 } else if (game.map_geometry == 'Hex') {
1009 let start_indented = start_position[0] % 2;
1010 if (direction == 'UPLEFT') {
1012 if (!start_indented) {
1015 } else if (direction == 'UPRIGHT') {
1017 if (start_indented) {
1020 } else if (direction == 'DOWNLEFT') {
1022 if (!start_indented) {
1025 } else if (direction == 'DOWNRIGHT') {
1027 if (start_indented) {
1032 if (target[0] < 0 || target[1] < 0 ||
1033 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1038 teleport: function() {
1039 let player = this.get_thing(game.player_id);
1040 if (player.position in this.portals) {
1041 server.reconnect_to(this.portals[player.position]);
1043 terminal.blink_screen();
1044 tui.log_msg('? not standing on portal')
1052 server.init(websocket_location);
1058 move: function(direction) {
1059 let target = game.move(this.position, direction);
1061 this.position = target
1062 if (tui.mode.shows_info) {
1064 } else if (tui.mode.name == 'control_tile_draw') {
1065 this.send_tile_control_command();
1068 terminal.blink_screen();
1071 update_info_db: function(yx, str) {
1072 this.info_db[yx] = str;
1073 if (tui.mode.name == 'study') {
1077 empty_info_db: function() {
1079 this.info_hints = [];
1080 if (tui.mode.name == 'study') {
1084 query_info: function() {
1085 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1087 get_info: function() {
1088 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1089 if (game.fov[position_i] != '.') {
1090 return 'outside field of view';
1093 let terrain_char = game.map[position_i]
1094 let terrain_desc = '?'
1095 if (game.terrains[terrain_char]) {
1096 terrain_desc = game.terrains[terrain_char];
1098 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1099 let protection = game.map_control[position_i];
1100 if (protection == '.') {
1101 protection = 'unprotected';
1103 info += 'PROTECTION: ' + protection + '\n';
1104 for (let t_id in game.things) {
1105 let t = game.things[t_id];
1106 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1107 let symbol = game.thing_types[t.type_];
1108 info += "THING: " + t.type_ + " / " + symbol;
1109 if (t.player_char) {
1110 info += t.player_char;
1113 info += " (" + t.name_ + ")";
1118 if (this.position in game.portals) {
1119 info += "PORTAL: " + game.portals[this.position] + "\n";
1121 if (this.position in this.info_db) {
1122 info += "ANNOTATIONS: " + this.info_db[this.position];
1124 info += 'waiting …';
1128 annotate: function(msg) {
1129 if (msg.length == 0) {
1130 msg = " "; // triggers annotation deletion
1132 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1134 set_portal: function(msg) {
1135 if (msg.length == 0) {
1136 msg = " "; // triggers portal deletion
1138 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1140 send_tile_control_command: function() {
1141 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1145 tui.inputEl.addEventListener('input', (event) => {
1146 if (tui.mode.has_input_prompt) {
1147 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1148 if (tui.inputEl.value.length > max_length) {
1149 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1151 tui.recalc_input_lines();
1152 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1153 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1154 tui.switch_mode('play');
1155 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1156 tui.tile_control_char = tui.inputEl.value[0];
1157 tui.switch_mode('control_pw_pw');
1158 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1159 tui.tile_control_char = tui.inputEl.value[0];
1160 tui.switch_mode('control_tile_draw');
1164 tui.inputEl.addEventListener('keydown', (event) => {
1165 tui.show_help = false;
1166 if (event.key == 'Enter') {
1167 event.preventDefault();
1169 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1170 tui.show_help = true;
1172 tui.restore_input_values();
1173 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1174 && !tui.mode.is_single_char_entry) {
1175 tui.show_help = true;
1176 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1177 tui.login_name = tui.inputEl.value;
1178 server.send(['LOGIN', tui.inputEl.value]);
1180 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1181 if (tui.inputEl.value.length == 0) {
1182 tui.log_msg('@ aborted');
1184 server.send(['SET_MAP_CONTROL_PASSWORD',
1185 tui.tile_control_char, tui.inputEl.value]);
1187 tui.switch_mode('play');
1188 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1189 explorer.set_portal(tui.inputEl.value);
1190 tui.switch_mode('play');
1191 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1192 explorer.annotate(tui.inputEl.value);
1193 tui.switch_mode('play');
1194 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1195 if (tui.inputEl.value.length == 0) {
1196 tui.inputEl.value = " ";
1198 tui.password = tui.inputEl.value
1199 tui.switch_mode('play');
1200 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1201 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1202 tui.switch_mode('play');
1203 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1204 let tokens = parser.tokenize(tui.inputEl.value);
1205 if (tokens.length > 0 && tokens[0].length > 0) {
1206 if (tui.inputEl.value[0][0] == '/') {
1207 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1208 tui.switch_mode('play');
1209 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1210 tui.switch_mode('study');
1211 } else if (tokens[0].slice(1) == 'nick') {
1212 if (tokens.length > 1) {
1213 server.send(['NICK', tokens[1]]);
1215 tui.log_msg('? need new name');
1218 tui.log_msg('? unknown command');
1221 server.send(['ALL', tui.inputEl.value]);
1223 } else if (tui.inputEl.valuelength > 0) {
1224 server.send(['ALL', tui.inputEl.value]);
1227 } else if (tui.mode.name == 'play') {
1228 if (tui.mode.mode_switch_on_key(event)) {
1230 } else if (event.key === tui.keys.flatten
1231 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1232 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1233 } else if (event.key === tui.keys.take_thing
1234 && game.tasks.includes('PICK_UP')) {
1235 server.send(["TASK:PICK_UP"]);
1236 } else if (event.key === tui.keys.drop_thing
1237 && game.tasks.includes('DROP')) {
1238 server.send(["TASK:DROP"]);
1239 } else if (event.key in tui.movement_keys
1240 && game.tasks.includes('MOVE')) {
1241 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1242 } else if (event.key === tui.keys.teleport) {
1244 } else if (event.key === tui.keys.switch_to_portal) {
1245 event.preventDefault();
1246 tui.switch_mode('portal');
1247 } else if (event.key === tui.keys.switch_to_annotate) {
1248 event.preventDefault();
1249 tui.switch_mode('annotate');
1251 } else if (tui.mode.name == 'study') {
1252 if (tui.mode.mode_switch_on_key(event)) {
1254 } else if (event.key in tui.movement_keys) {
1255 explorer.move(tui.movement_keys[event.key]);
1256 } else if (event.key == tui.keys.toggle_map_mode) {
1257 if (tui.map_mode == 'terrain') {
1258 tui.map_mode = 'annotations';
1259 } else if (tui.map_mode == 'annotations') {
1260 tui.map_mode = 'control';
1262 tui.map_mode = 'terrain';
1265 } else if (tui.mode.name == 'control_tile_draw') {
1266 if (tui.mode.mode_switch_on_key(event)) {
1268 } else if (event.key in tui.movement_keys) {
1269 explorer.move(tui.movement_keys[event.key]);
1275 rows_selector.addEventListener('input', function() {
1276 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1279 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1280 terminal.initialize();
1283 cols_selector.addEventListener('input', function() {
1284 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1287 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1288 terminal.initialize();
1289 tui.window_width = terminal.cols / 2,
1292 for (let key_selector of key_selectors) {
1293 key_selector.addEventListener('input', function() {
1294 window.localStorage.setItem(key_selector.id, key_selector.value);
1298 window.setInterval(function() {
1299 if (server.connected) {
1300 server.send(['PING']);
1302 server.reconnect_to(server.url);
1303 tui.log_msg('@ attempting reconnect …')
1306 document.getElementById("terminal").onclick = function() {
1307 tui.inputEl.focus();
1309 document.getElementById("help").onclick = function() {
1310 tui.show_help = true;
1313 document.getElementById("switch_to_play").onclick = function() {
1314 tui.switch_mode('play');
1317 document.getElementById("switch_to_study").onclick = function() {
1318 tui.switch_mode('study');
1321 document.getElementById("switch_to_chat").onclick = function() {
1322 tui.switch_mode('chat');
1325 document.getElementById("switch_to_password").onclick = function() {
1326 tui.switch_mode('password');
1329 document.getElementById("switch_to_edit").onclick = function() {
1330 tui.switch_mode('edit');
1333 document.getElementById("switch_to_annotate").onclick = function() {
1334 tui.switch_mode('annotate');
1337 document.getElementById("switch_to_portal").onclick = function() {
1338 tui.switch_mode('portal');
1341 document.getElementById("switch_to_admin").onclick = function() {
1342 tui.switch_mode('admin');
1345 document.getElementById("switch_to_control_pw_type").onclick = function() {
1346 tui.switch_mode('control_pw_type');
1349 document.getElementById("switch_to_control_tile_type").onclick = function() {
1350 tui.switch_mode('control_tile_type');
1353 document.getElementById("toggle_map_mode").onclick = function() {
1354 if (tui.map_mode == 'terrain') {
1355 tui.map_mode = 'annotations';
1356 } else if (tui.map_mode == 'annotations') {
1357 tui.map_mode = 'control';
1359 tui.map_mode = 'terrain';
1363 document.getElementById("take_thing").onclick = function() {
1364 server.send(['TASK:PICK_UP']);
1366 document.getElementById("drop_thing").onclick = function() {
1367 server.send(['TASK:DROP']);
1369 document.getElementById("flatten").onclick = function() {
1370 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1372 document.getElementById("teleport").onclick = function() {
1375 document.getElementById("move_upleft").onclick = function() {
1376 if (tui.mode.name == 'play') {
1377 server.send(['TASK:MOVE', 'UPLEFT']);
1379 explorer.move('UPLEFT');
1382 document.getElementById("move_left").onclick = function() {
1383 if (tui.mode.name == 'play') {
1384 server.send(['TASK:MOVE', 'LEFT']);
1386 explorer.move('LEFT');
1389 document.getElementById("move_downleft").onclick = function() {
1390 if (tui.mode.name == 'play') {
1391 server.send(['TASK:MOVE', 'DOWNLEFT']);
1393 explorer.move('DOWNLEFT');
1396 document.getElementById("move_down").onclick = function() {
1397 if (tui.mode.name == 'play') {
1398 server.send(['TASK:MOVE', 'DOWN']);
1400 explorer.move('DOWN');
1403 document.getElementById("move_up").onclick = function() {
1404 if (tui.mode.name == 'play') {
1405 server.send(['TASK:MOVE', 'UP']);
1407 explorer.move('UP');
1410 document.getElementById("move_upright").onclick = function() {
1411 if (tui.mode.name == 'play') {
1412 server.send(['TASK:MOVE', 'UPRIGHT']);
1414 explorer.move('UPRIGHT');
1417 document.getElementById("move_right").onclick = function() {
1418 if (tui.mode.name == 'play') {
1419 server.send(['TASK:MOVE', 'RIGHT']);
1421 explorer.move('RIGHT');
1424 document.getElementById("move_downright").onclick = function() {
1425 if (tui.mode.name == 'play') {
1426 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1428 explorer.move('DOWNRIGHT');