7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
16 <td><button id="move_upleft">up-left</button></td>
17 <td><button id="move_up">up</button></td>
18 <td><button id="move_upright">up-right</button></td>
21 <td><button id="move_left">left</button></td>
23 <td><button id="move_right">right</button></td>
26 <td><button id="move_downleft">down-left</button></td>
27 <td><button id="move_down">down</button></td>
28 <td><button id="move_downright">down-right</button></td>
33 <td><button id="help">help</button></td>
36 <td><button id="switch_to_chat">chat mode</button><br /></td>
38 <td><button id="switch_to_study">study mode</button></td>
39 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
41 <td><button id="switch_to_play">play mode</button></td>
45 <td><button id="take_thing">take thing</button></td>
46 <td><button id="switch_to_edit">change tile</button></td>
47 <td><button id="switch_to_admin">become admin</button></td>
50 <td><button id="drop_thing">drop thing</button></td>
51 <td><button id="switch_to_password">change tile editing password</button></td>
52 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
55 <td><button id="flatten">flatten surroundings</button></td>
56 <td><button id="switch_to_annotate">annotate tile</button></td>
57 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
60 <td><button id="teleport">teleport</button></td>
61 <td><button id="switch_to_portal">edit portal link</button></td>
67 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
69 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
70 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
71 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
72 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
73 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
74 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
75 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
76 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
77 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
78 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
79 <li>help: <input id="key_help" type="text" value="h" />
80 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
81 <li>teleport: <input id="key_teleport" type="text" value="p" />
82 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
83 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
84 <li><input id="key_switch_to_chat" type="text" value="t" />
85 <li><input id="key_switch_to_play" type="text" value="p" />
86 <li><input id="key_switch_to_study" type="text" value="?" />
87 <li><input id="key_switch_to_edit" type="text" value="m" />
88 <li><input id="key_switch_to_password" type="text" value="P" />
89 <li><input id="key_switch_to_admin" type="text" value="A" />
90 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
91 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
92 <li><input id="key_switch_to_annotate" type="text" value="M" />
93 <li><input id="key_switch_to_portal" type="text" value="T" />
94 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
99 let websocket_location = "wss://plomlompom.com/rogue_chat/";
100 //let websocket_location = "ws://localhost:8000/";
105 'long': 'This mode allows you to interact with the map.'
109 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it.'},
111 'short': 'terrain edit',
112 'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
115 'short': 'change tiles control password',
116 'long': 'This mode is the first of two steps to change the password for a tile control character. First enter the tile control character for which you want to change the password!'
119 'short': 'change tiles control password',
120 'long': 'This mode is the second of two steps to change the password for a tile control character. Enter the new password for the tile control character you chose.'
122 'control_tile_type': {
123 'short': 'change tiles control',
124 'long': 'This mode is the first of two steps to change tile control areas on the map. First enter the tile control character you want to write.'
126 'control_tile_draw': {
127 'short': 'change tiles control',
128 'long': 'This mode is the second of two steps to change tile control areas on the map. Move cursor around the map to draw selected tile control character'
131 'short': 'annotate tile',
132 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.'
135 'short': 'edit portal',
136 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
140 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:'
144 'long': 'Pick your player name.'
146 'waiting_for_server': {
147 'short': 'waiting for server response',
148 'long': 'Waiting for a server response.'
151 'short': 'waiting for server response',
152 'long': 'Waiting for a server response.'
155 'short': 'map edit password',
156 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.'
159 'short': 'become admin',
160 'long': 'This mode allows you to become admin if you know an admin password.'
164 let rows_selector = document.getElementById("n_rows");
165 let cols_selector = document.getElementById("n_cols");
166 let key_selectors = document.querySelectorAll('[id^="key_"]');
168 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
169 const action = key_switch_selector.id.slice("key_switch_to_".length);
170 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
173 function restore_selector_value(selector) {
174 let stored_selection = window.localStorage.getItem(selector.id);
175 if (stored_selection) {
176 selector.value = stored_selection;
179 restore_selector_value(rows_selector);
180 restore_selector_value(cols_selector);
181 for (let key_selector of key_selectors) {
182 restore_selector_value(key_selector);
188 initialize: function() {
189 this.rows = rows_selector.value;
190 this.cols = cols_selector.value;
191 this.pre_el = document.getElementById("terminal");
192 this.pre_el.style.color = this.foreground;
193 this.pre_el.style.backgroundColor = this.background;
196 for (let y = 0, x = 0; y <= this.rows; x++) {
197 if (x == this.cols) {
200 this.content.push(line);
202 if (y == this.rows) {
209 blink_screen: function() {
210 this.pre_el.style.color = this.background;
211 this.pre_el.style.backgroundColor = this.foreground;
213 this.pre_el.style.color = this.foreground;
214 this.pre_el.style.backgroundColor = this.background;
217 refresh: function() {
218 function escapeHTML(str) {
220 replace(/&/g, '&').
221 replace(/</g, '<').
222 replace(/>/g, '>').
223 replace(/'/g, ''').
224 replace(/"/g, '"');
226 let pre_content = '';
227 for (let y = 0; y < this.rows; y++) {
228 let line = this.content[y].join('');
230 if (y in tui.links) {
232 for (let span of tui.links[y]) {
233 chunks.push(escapeHTML(line.slice(start_x, span[0])));
234 chunks.push('<a href="');
235 chunks.push(escapeHTML(span[2]));
237 chunks.push(escapeHTML(line.slice(span[0], span[1])));
241 chunks.push(escapeHTML(line.slice(start_x)));
243 chunks = [escapeHTML(line)];
245 for (const chunk of chunks) {
246 pre_content += chunk;
250 this.pre_el.innerHTML = pre_content;
252 write: function(start_y, start_x, msg) {
253 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
254 this.content[start_y][x] = msg[i];
257 drawBox: function(start_y, start_x, height, width) {
258 let end_y = start_y + height;
259 let end_x = start_x + width;
260 for (let y = start_y, x = start_x; y < this.rows; x++) {
268 this.content[y][x] = ' ';
272 terminal.initialize();
275 tokenize: function(str) {
280 for (let i = 0; i < str.length; i++) {
286 } else if (c == '\\') {
288 } else if (c == '"') {
293 } else if (c == '"') {
295 } else if (c === ' ') {
296 if (token.length > 0) {
304 if (token.length > 0) {
309 parse_yx: function(position_string) {
310 let coordinate_strings = position_string.split(',')
311 let position = [0, 0];
312 position[0] = parseInt(coordinate_strings[0].slice(2));
313 position[1] = parseInt(coordinate_strings[1].slice(2));
325 init: function(url) {
327 this.websocket = new WebSocket(this.url);
328 this.websocket.onopen = function(event) {
329 server.connected = true;
330 game.thing_types = {};
332 server.send(['TASKS']);
333 server.send(['TERRAINS']);
334 server.send(['THING_TYPES']);
335 tui.log_msg("@ server connected! :)");
336 tui.switch_mode('login');
338 this.websocket.onclose = function(event) {
339 server.connected = false;
340 tui.switch_mode('waiting_for_server');
341 tui.log_msg("@ server disconnected :(");
343 this.websocket.onmessage = this.handle_event;
345 reconnect_to: function(url) {
346 this.websocket.close();
349 send: function(tokens) {
350 this.websocket.send(unparser.untokenize(tokens));
352 handle_event: function(event) {
353 let tokens = parser.tokenize(event.data);
354 if (tokens[0] === 'TURN') {
355 game.turn_complete = false;
356 explorer.empty_info_db();
359 game.turn = parseInt(tokens[1]);
360 } else if (tokens[0] === 'THING') {
361 let t = game.get_thing(tokens[3], true);
362 t.position = parser.parse_yx(tokens[1]);
364 } else if (tokens[0] === 'THING_NAME') {
365 let t = game.get_thing(tokens[1], false);
369 } else if (tokens[0] === 'THING_CHAR') {
370 let t = game.get_thing(tokens[1], false);
372 t.player_char = tokens[2];
374 } else if (tokens[0] === 'TASKS') {
375 game.tasks = tokens[1].split(',');
376 tui.mode_edit.legal = game.tasks.includes('WRITE');
377 } else if (tokens[0] === 'THING_TYPE') {
378 game.thing_types[tokens[1]] = tokens[2]
379 } else if (tokens[0] === 'TERRAIN') {
380 game.terrains[tokens[1]] = tokens[2]
381 } else if (tokens[0] === 'MAP') {
382 game.map_geometry = tokens[1];
384 game.map_size = parser.parse_yx(tokens[2]);
386 } else if (tokens[0] === 'FOV') {
388 } else if (tokens[0] === 'MAP_CONTROL') {
389 game.map_control = tokens[1]
390 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
391 game.turn_complete = true;
392 if (tui.mode.name == 'post_login_wait') {
393 tui.switch_mode('play');
394 } else if (tui.mode.name == 'study') {
395 explorer.query_info();
398 } else if (tokens[0] === 'CHAT') {
399 tui.log_msg('# ' + tokens[1], 1);
400 } else if (tokens[0] === 'PLAYER_ID') {
401 game.player_id = parseInt(tokens[1]);
402 } else if (tokens[0] === 'LOGIN_OK') {
403 this.send(['GET_GAMESTATE']);
404 tui.switch_mode('post_login_wait');
405 } else if (tokens[0] === 'PORTAL') {
406 let position = parser.parse_yx(tokens[1]);
407 game.portals[position] = tokens[2];
408 } else if (tokens[0] === 'ANNOTATION_HINT') {
409 let position = parser.parse_yx(tokens[1]);
410 explorer.info_hints = explorer.info_hints.concat([position]);
411 } else if (tokens[0] === 'ANNOTATION') {
412 let position = parser.parse_yx(tokens[1]);
413 explorer.update_info_db(position, tokens[2]);
414 tui.restore_input_values();
416 } else if (tokens[0] === 'UNHANDLED_INPUT') {
417 tui.log_msg('? unknown command');
418 } else if (tokens[0] === 'PLAY_ERROR') {
419 tui.log_msg('? ' + tokens[1]);
420 terminal.blink_screen();
421 } else if (tokens[0] === 'ARGUMENT_ERROR') {
422 tui.log_msg('? syntax error: ' + tokens[1]);
423 } else if (tokens[0] === 'GAME_ERROR') {
424 tui.log_msg('? game error: ' + tokens[1]);
425 } else if (tokens[0] === 'PONG') {
428 tui.log_msg('? unhandled input: ' + event.data);
434 quote: function(str) {
436 for (let i = 0; i < str.length; i++) {
438 if (['"', '\\'].includes(c)) {
444 return quoted.join('');
446 to_yx: function(yx_coordinate) {
447 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
449 untokenize: function(tokens) {
450 let quoted_tokens = [];
451 for (let token of tokens) {
452 quoted_tokens.push(this.quote(token));
454 return quoted_tokens.join(" ");
459 constructor(name, has_input_prompt=false, shows_info=false,
460 is_intro=false, is_single_char_entry=false) {
462 this.short_desc = mode_helps[name].short;
463 this.available_modes = [];
464 this.has_input_prompt = has_input_prompt;
465 this.shows_info= shows_info;
466 this.is_intro = is_intro;
467 this.help_intro = mode_helps[name].long;
468 this.is_single_char_entry = is_single_char_entry;
471 *iter_available_modes() {
472 for (let mode_name of this.available_modes) {
473 let mode = tui['mode_' + mode_name];
477 let key = tui.keys['switch_to_' + mode.name];
481 list_available_modes() {
483 if (this.available_modes.length > 0) {
484 msg += 'Other modes available from here:\n';
485 for (let [mode, key] of this.iter_available_modes()) {
486 msg += '[' + key + '] – ' + mode.short_desc + '\n';
491 mode_switch_on_key(key_event) {
492 for (let [mode, key] of this.iter_available_modes()) {
493 if (key_event.key == key) {
494 event.preventDefault();
495 tui.switch_mode(mode.name);
507 window_width: terminal.cols / 2,
513 mode_waiting_for_server: new Mode('waiting_for_server',
515 mode_login: new Mode('login', true, false, true),
516 mode_post_login_wait: new Mode('post_login_wait'),
517 mode_chat: new Mode('chat', true),
518 mode_annotate: new Mode('annotate', true, true),
519 mode_play: new Mode('play'),
520 mode_study: new Mode('study', false, true),
521 mode_edit: new Mode('edit', false, false, false, true),
522 mode_control_pw_type: new Mode('control_pw_type',
523 false, false, false, true),
524 mode_portal: new Mode('portal', true, true),
525 mode_password: new Mode('password', true),
526 mode_admin: new Mode('admin', true),
527 mode_control_pw_pw: new Mode('control_pw_pw', true),
528 mode_control_tile_type: new Mode('control_tile_type',
529 false, false, false, true),
530 mode_control_tile_draw: new Mode('control_tile_draw'),
532 this.mode_play.available_modes = ["chat", "study", "edit",
533 "annotate", "portal",
537 this.mode_study.available_modes = ["chat", "play"]
538 this.mode_control_tile_draw.available_modes = ["play"]
539 this.mode = this.mode_waiting_for_server;
540 this.inputEl = document.getElementById("input");
541 this.inputEl.focus();
542 this.recalc_input_lines();
543 this.height_header = this.height_turn_line + this.height_mode_line;
544 this.log_msg("@ waiting for server connection ...");
547 init_keys: function() {
549 for (let key_selector of key_selectors) {
550 this.keys[key_selector.id.slice(4)] = key_selector.value;
552 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 document.getElementById("move_left").disabled = false;
603 document.getElementById("move_right").disabled = false;
604 if (game.map_geometry == 'Hex') {
605 document.getElementById("move_upleft").disabled = false;
606 document.getElementById("move_upright").disabled = false;
607 document.getElementById("move_downleft").disabled = false;
608 document.getElementById("move_downright").disabled = false;
610 document.getElementById("move_up").disabled = false;
611 document.getElementById("move_down").disabled = false;
614 if (!this.mode.is_intro && this.mode.name != 'play') {
615 document.getElementById("switch_to_play").disabled = false;
617 if (!this.mode.is_intro && this.mode.name != 'study') {
618 document.getElementById("switch_to_study").disabled = false;
620 if (!this.mode.is_intro && this.mode.name != 'chat') {
621 document.getElementById("switch_to_chat").disabled = false;
623 if (this.mode.name == 'login') {
624 if (this.login_name) {
625 server.send(['LOGIN', this.login_name]);
627 this.log_msg("? need login name");
629 } else if (this.mode.name == 'play') {
630 if (game.tasks.includes('PICK_UP')) {
631 document.getElementById("take_thing").disabled = false;
633 if (game.tasks.includes('DROP')) {
634 document.getElementById("drop_thing").disabled = false;
636 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
637 document.getElementById("flatten").disabled = false;
639 if (game.tasks.includes('MOVE')) {
641 document.getElementById("teleport").disabled = false;
642 document.getElementById("switch_to_annotate").disabled = false;
643 document.getElementById("switch_to_edit").disabled = false;
644 document.getElementById("switch_to_portal").disabled = false;
645 document.getElementById("switch_to_password").disabled = false;
646 document.getElementById("switch_to_admin").disabled = false;
647 document.getElementById("switch_to_control_pw_type").disabled = false;
648 document.getElementById("switch_to_control_tile_type").disabled = false;
649 } else if (this.mode.name == 'study') {
650 document.getElementById("toggle_map_mode").disabled = false;
651 } else if (this.mode.is_single_char_entry) {
652 this.show_help = true;
653 } else if (this.mode.name == 'admin') {
654 this.log_msg('@ enter admin password:')
655 } else if (this.mode.name == 'control_pw_pw') {
656 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
657 } else if (this.mode.name == 'control_pw_pw') {
658 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
662 offset_links: function(offset, links) {
663 for (let y in links) {
664 let real_y = offset[0] + parseInt(y);
665 if (!this.links[real_y]) {
666 this.links[real_y] = [];
668 for (let link of links[y]) {
669 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
670 this.links[real_y].push(offset_link);
674 restore_input_values: function() {
675 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
676 let info = explorer.info_db[explorer.position];
677 if (info != "(none)") {
678 this.inputEl.value = info;
679 this.recalc_input_lines();
681 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
682 let portal = game.portals[explorer.position]
683 this.inputEl.value = portal;
684 this.recalc_input_lines();
685 } else if (this.mode.name == 'password') {
686 this.inputEl.value = this.password;
687 this.recalc_input_lines();
690 empty_input: function(str) {
691 this.inputEl.value = "";
692 if (this.mode.has_input_prompt) {
693 this.recalc_input_lines();
695 this.height_input = 0;
698 recalc_input_lines: function() {
700 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
701 this.height_input = this.input_lines.length;
703 msg_into_lines_of_width: function(msg, width) {
704 function push_inner_link(y, end_x) {
705 if (!inner_links[y]) {
708 inner_links[y].push([url_start_x, end_x, url]);
710 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
713 for (const match of matches) {
714 const url = match[0];
715 const url_start = match.index;
716 const url_end = match.index + match[0].length;
717 link_data[url_start] = url;
718 url_ends.push(url_end);
722 let inner_links = {};
726 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
727 if (x >= width || msg[i] == "\n") {
729 push_inner_link(y, chunk.length);
735 if (msg[i] == "\n") {
740 if (msg[i] != "\n") {
743 if (i in link_data) {
747 } else if (url_ends.includes(i)) {
748 push_inner_link(y, x);
754 push_inner_link(lines.length - 1, chunk.length);
756 return [lines, inner_links];
758 log_msg: function(msg) {
760 while (this.log.length > 100) {
765 draw_map: function() {
766 let map_lines_split = [];
768 let map_content = game.map;
769 if (this.map_mode == 'control') {
770 map_content = game.map_control;
772 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
773 if (j == game.map_size[1]) {
774 map_lines_split.push(line);
778 line.push(map_content[i] + ' ');
780 map_lines_split.push(line);
781 if (this.map_mode == 'annotations') {
782 for (const coordinate of explorer.info_hints) {
783 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
785 } else if (this.map_mode == 'terrain') {
786 for (const p in game.portals) {
787 let coordinate = p.split(',')
788 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
790 let used_positions = [];
791 for (const thing_id in game.things) {
792 let t = game.things[thing_id];
793 let symbol = game.thing_types[t.type_];
796 meta_char = t.player_char;
798 if (used_positions.includes(t.position.toString())) {
801 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
802 used_positions.push(t.position.toString());
805 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
806 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
809 if (game.map_geometry == 'Square') {
810 for (let line_split of map_lines_split) {
811 map_lines.push(line_split.join(''));
813 } else if (game.map_geometry == 'Hex') {
815 for (let line_split of map_lines_split) {
816 map_lines.push(' '.repeat(indent) + line_split.join(''));
824 let window_center = [terminal.rows / 2, this.window_width / 2];
825 let player = game.things[game.player_id];
826 let center_position = [player.position[0], player.position[1]];
827 if (tui.mode.shows_info) {
828 center_position = [explorer.position[0], explorer.position[1]];
830 center_position[1] = center_position[1] * 2;
831 let offset = [center_position[0] - window_center[0],
832 center_position[1] - window_center[1]]
833 if (game.map_geometry == 'Hex' && offset[0] % 2) {
836 let term_y = Math.max(0, -offset[0]);
837 let term_x = Math.max(0, -offset[1]);
838 let map_y = Math.max(0, offset[0]);
839 let map_x = Math.max(0, offset[1]);
840 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
841 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
842 terminal.write(term_y, term_x, to_draw);
845 draw_mode_line: function() {
846 let help = 'hit [' + this.keys.help + '] for help';
847 if (this.mode.has_input_prompt) {
848 help = 'enter /help for help';
850 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
852 draw_turn_line: function(n) {
853 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
855 draw_history: function() {
856 let log_display_lines = [];
858 let y_offset_in_log = 0;
859 for (let line of this.log) {
860 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
862 log_display_lines = log_display_lines.concat(new_lines);
863 for (const y in link_data) {
864 const rel_y = y_offset_in_log + parseInt(y);
865 log_links[rel_y] = [];
866 for (let link of link_data[y]) {
867 log_links[rel_y].push(link);
870 y_offset_in_log += new_lines.length;
872 let i = log_display_lines.length - 1;
873 for (let y = terminal.rows - 1 - this.height_input;
874 y >= this.height_header && i >= 0;
876 terminal.write(y, this.window_width, log_display_lines[i]);
878 for (const key of Object.keys(log_links)) {
879 if (parseInt(key) <= i) {
880 delete log_links[key];
883 let offset = [terminal.rows - this.height_input - log_display_lines.length,
885 this.offset_links(offset, log_links);
887 draw_info: function() {
888 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
890 let offset = [this.height_header, this.window_width];
891 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
892 terminal.write(y, offset[1], lines[i]);
894 this.offset_links(offset, link_data);
896 draw_input: function() {
897 if (this.mode.has_input_prompt) {
898 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
899 terminal.write(y, this.window_width, this.input_lines[i]);
903 draw_help: function() {
904 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
905 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
906 if (this.mode.name == 'play') {
907 content += "Available actions:\n";
908 if (game.tasks.includes('MOVE')) {
909 content += "[" + movement_keys_desc + "] – move player\n";
911 if (game.tasks.includes('PICK_UP')) {
912 content += "[" + this.keys.take_thing + "] – take thing under player\n";
914 if (game.tasks.includes('DROP')) {
915 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
917 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
918 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
920 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
922 } else if (this.mode.name == 'study') {
923 content += "Available actions:\n";
924 content += '[' + movement_keys_desc + '] – move question mark\n';
925 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
927 } else if (this.mode.name == 'chat') {
928 content += '/nick NAME – re-name yourself to NAME\n';
929 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
930 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
932 content += this.mode.list_available_modes();
934 if (!this.mode.has_input_prompt) {
935 start_x = this.window_width
937 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
938 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
939 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
940 terminal.write(y, start_x, lines[i]);
943 full_refresh: function() {
945 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
946 if (this.mode.is_intro) {
950 if (game.turn_complete) {
952 this.draw_turn_line();
954 this.draw_mode_line();
955 if (this.mode.shows_info) {
962 if (this.show_help) {
974 this.map_control = "";
975 this.map_size = [0,0];
980 get_thing: function(id_, create_if_not_found=false) {
981 if (id_ in game.things) {
982 return game.things[id_];
983 } else if (create_if_not_found) {
984 let t = new Thing([0,0]);
985 game.things[id_] = t;
989 move: function(start_position, direction) {
990 let target = [start_position[0], start_position[1]];
991 if (direction == 'LEFT') {
993 } else if (direction == 'RIGHT') {
995 } else if (game.map_geometry == 'Square') {
996 if (direction == 'UP') {
998 } else if (direction == 'DOWN') {
1001 } else if (game.map_geometry == 'Hex') {
1002 let start_indented = start_position[0] % 2;
1003 if (direction == 'UPLEFT') {
1005 if (!start_indented) {
1008 } else if (direction == 'UPRIGHT') {
1010 if (start_indented) {
1013 } else if (direction == 'DOWNLEFT') {
1015 if (!start_indented) {
1018 } else if (direction == 'DOWNRIGHT') {
1020 if (start_indented) {
1025 if (target[0] < 0 || target[1] < 0 ||
1026 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1031 teleport: function() {
1032 let player = this.get_thing(game.player_id);
1033 if (player.position in this.portals) {
1034 server.reconnect_to(this.portals[player.position]);
1036 terminal.blink_screen();
1037 tui.log_msg('? not standing on portal')
1045 server.init(websocket_location);
1051 move: function(direction) {
1052 let target = game.move(this.position, direction);
1054 this.position = target
1055 if (tui.mode.shows_info) {
1057 } else if (tui.mode.name == 'control_tile_draw') {
1058 this.send_tile_control_command();
1061 terminal.blink_screen();
1064 update_info_db: function(yx, str) {
1065 this.info_db[yx] = str;
1066 if (tui.mode.name == 'study') {
1070 empty_info_db: function() {
1072 this.info_hints = [];
1073 if (tui.mode.name == 'study') {
1077 query_info: function() {
1078 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1080 get_info: function() {
1081 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1082 if (game.fov[position_i] != '.') {
1083 return 'outside field of view';
1086 let terrain_char = game.map[position_i]
1087 let terrain_desc = '?'
1088 if (game.terrains[terrain_char]) {
1089 terrain_desc = game.terrains[terrain_char];
1091 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1092 let protection = game.map_control[position_i];
1093 if (protection == '.') {
1094 protection = 'unprotected';
1096 info += 'PROTECTION: ' + protection + '\n';
1097 for (let t_id in game.things) {
1098 let t = game.things[t_id];
1099 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1100 let symbol = game.thing_types[t.type_];
1101 info += "THING: " + t.type_ + " / " + symbol;
1102 if (t.player_char) {
1103 info += t.player_char;
1106 info += " (" + t.name_ + ")";
1111 if (this.position in game.portals) {
1112 info += "PORTAL: " + game.portals[this.position] + "\n";
1114 if (this.position in this.info_db) {
1115 info += "ANNOTATIONS: " + this.info_db[this.position];
1117 info += 'waiting …';
1121 annotate: function(msg) {
1122 if (msg.length == 0) {
1123 msg = " "; // triggers annotation deletion
1125 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1127 set_portal: function(msg) {
1128 if (msg.length == 0) {
1129 msg = " "; // triggers portal deletion
1131 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1133 send_tile_control_command: function() {
1134 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1138 tui.inputEl.addEventListener('input', (event) => {
1139 if (tui.mode.has_input_prompt) {
1140 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1141 if (tui.inputEl.value.length > max_length) {
1142 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1144 tui.recalc_input_lines();
1145 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1146 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1147 tui.switch_mode('play');
1148 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1149 tui.tile_control_char = tui.inputEl.value[0];
1150 tui.switch_mode('control_pw_pw');
1151 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1152 tui.tile_control_char = tui.inputEl.value[0];
1153 tui.switch_mode('control_tile_draw');
1157 tui.inputEl.addEventListener('keydown', (event) => {
1158 tui.show_help = false;
1159 if (event.key == 'Enter') {
1160 event.preventDefault();
1162 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1163 tui.show_help = true;
1165 tui.restore_input_values();
1166 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1167 && !tui.mode.is_single_char_entry) {
1168 tui.show_help = true;
1169 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1170 tui.login_name = tui.inputEl.value;
1171 server.send(['LOGIN', tui.inputEl.value]);
1173 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1174 if (tui.inputEl.value.length == 0) {
1175 tui.log_msg('@ aborted');
1177 server.send(['SET_MAP_CONTROL_PASSWORD',
1178 tui.tile_control_char, tui.inputEl.value]);
1180 tui.switch_mode('play');
1181 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1182 explorer.set_portal(tui.inputEl.value);
1183 tui.switch_mode('play');
1184 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1185 explorer.annotate(tui.inputEl.value);
1186 tui.switch_mode('play');
1187 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1188 if (tui.inputEl.value.length == 0) {
1189 tui.inputEl.value = " ";
1191 tui.password = tui.inputEl.value
1192 tui.switch_mode('play');
1193 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1194 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1195 tui.switch_mode('play');
1196 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1197 let tokens = parser.tokenize(tui.inputEl.value);
1198 if (tokens.length > 0 && tokens[0].length > 0) {
1199 if (tui.inputEl.value[0][0] == '/') {
1200 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1201 tui.switch_mode('play');
1202 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1203 tui.switch_mode('study');
1204 } else if (tokens[0].slice(1) == 'nick') {
1205 if (tokens.length > 1) {
1206 server.send(['NICK', tokens[1]]);
1208 tui.log_msg('? need new name');
1211 tui.log_msg('? unknown command');
1214 server.send(['ALL', tui.inputEl.value]);
1216 } else if (tui.inputEl.valuelength > 0) {
1217 server.send(['ALL', tui.inputEl.value]);
1220 } else if (tui.mode.name == 'play') {
1221 if (tui.mode.mode_switch_on_key(event)) {
1223 } else if (event.key === tui.keys.flatten
1224 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1225 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1226 } else if (event.key === tui.keys.take_thing
1227 && game.tasks.includes('PICK_UP')) {
1228 server.send(["TASK:PICK_UP"]);
1229 } else if (event.key === tui.keys.drop_thing
1230 && game.tasks.includes('DROP')) {
1231 server.send(["TASK:DROP"]);
1232 } else if (event.key in tui.movement_keys
1233 && game.tasks.includes('MOVE')) {
1234 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1235 } else if (event.key === tui.keys.teleport) {
1237 } else if (event.key === tui.keys.switch_to_portal) {
1238 event.preventDefault();
1239 tui.switch_mode('portal');
1240 } else if (event.key === tui.keys.switch_to_annotate) {
1241 event.preventDefault();
1242 tui.switch_mode('annotate');
1244 } else if (tui.mode.name == 'study') {
1245 if (tui.mode.mode_switch_on_key(event)) {
1247 } else if (event.key in tui.movement_keys) {
1248 explorer.move(tui.movement_keys[event.key]);
1249 } else if (event.key == tui.keys.toggle_map_mode) {
1250 if (tui.map_mode == 'terrain') {
1251 tui.map_mode = 'annotations';
1252 } else if (tui.map_mode == 'annotations') {
1253 tui.map_mode = 'control';
1255 tui.map_mode = 'terrain';
1258 } else if (tui.mode.name == 'control_tile_draw') {
1259 if (tui.mode.mode_switch_on_key(event)) {
1261 } else if (event.key in tui.movement_keys) {
1262 explorer.move(tui.movement_keys[event.key]);
1268 rows_selector.addEventListener('input', function() {
1269 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1272 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1273 terminal.initialize();
1276 cols_selector.addEventListener('input', function() {
1277 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1280 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1281 terminal.initialize();
1282 tui.window_width = terminal.cols / 2,
1285 for (let key_selector of key_selectors) {
1286 key_selector.addEventListener('input', function() {
1287 window.localStorage.setItem(key_selector.id, key_selector.value);
1291 window.setInterval(function() {
1292 if (server.connected) {
1293 server.send(['PING']);
1295 server.reconnect_to(server.url);
1296 tui.log_msg('@ attempting reconnect …')
1299 document.getElementById("terminal").onclick = function() {
1300 tui.inputEl.focus();
1302 document.getElementById("help").onclick = function() {
1303 tui.show_help = true;
1306 document.getElementById("switch_to_play").onclick = function() {
1307 tui.switch_mode('play');
1310 document.getElementById("switch_to_study").onclick = function() {
1311 tui.switch_mode('study');
1314 document.getElementById("switch_to_chat").onclick = function() {
1315 tui.switch_mode('chat');
1318 document.getElementById("switch_to_password").onclick = function() {
1319 tui.switch_mode('password');
1322 document.getElementById("switch_to_edit").onclick = function() {
1323 tui.switch_mode('edit');
1326 document.getElementById("switch_to_annotate").onclick = function() {
1327 tui.switch_mode('annotate');
1330 document.getElementById("switch_to_portal").onclick = function() {
1331 tui.switch_mode('portal');
1334 document.getElementById("switch_to_admin").onclick = function() {
1335 tui.switch_mode('admin');
1338 document.getElementById("switch_to_control_pw_type").onclick = function() {
1339 tui.switch_mode('control_pw_type');
1342 document.getElementById("switch_to_control_tile_type").onclick = function() {
1343 tui.switch_mode('control_tile_type');
1346 document.getElementById("toggle_map_mode").onclick = function() {
1347 if (tui.map_mode == 'terrain') {
1348 tui.map_mode = 'annotations';
1349 } else if (tui.map_mode == 'annotations') {
1350 tui.map_mode = 'control';
1352 tui.map_mode = 'terrain';
1356 document.getElementById("take_thing").onclick = function() {
1357 server.send(['TASK:PICK_UP']);
1359 document.getElementById("drop_thing").onclick = function() {
1360 server.send(['TASK:DROP']);
1362 document.getElementById("flatten").onclick = function() {
1363 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1365 document.getElementById("teleport").onclick = function() {
1368 document.getElementById("move_upleft").onclick = function() {
1369 if (tui.mode.name == 'play') {
1370 server.send(['TASK:MOVE', 'UPLEFT']);
1372 explorer.move('UPLEFT');
1375 document.getElementById("move_left").onclick = function() {
1376 if (tui.mode.name == 'play') {
1377 server.send(['TASK:MOVE', 'LEFT']);
1379 explorer.move('LEFT');
1382 document.getElementById("move_downleft").onclick = function() {
1383 if (tui.mode.name == 'play') {
1384 server.send(['TASK:MOVE', 'DOWNLEFT']);
1386 explorer.move('DOWNLEFT');
1389 document.getElementById("move_down").onclick = function() {
1390 if (tui.mode.name == 'play') {
1391 server.send(['TASK:MOVE', 'DOWN']);
1393 explorer.move('DOWN');
1396 document.getElementById("move_up").onclick = function() {
1397 if (tui.mode.name == 'play') {
1398 server.send(['TASK:MOVE', 'UP']);
1400 explorer.move('UP');
1403 document.getElementById("move_upright").onclick = function() {
1404 if (tui.mode.name == 'play') {
1405 server.send(['TASK:MOVE', 'UPRIGHT']);
1407 explorer.move('UPRIGHT');
1410 document.getElementById("move_right").onclick = function() {
1411 if (tui.mode.name == 'play') {
1412 server.send(['TASK:MOVE', 'RIGHT']);
1414 explorer.move('RIGHT');
1417 document.getElementById("move_downright").onclick = function() {
1418 if (tui.mode.name == 'play') {
1419 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1421 explorer.move('DOWNRIGHT');