13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <div style="position: relative; display: inline-block;">
18 <pre id="terminal"></pre>
19 <textarea id="input" style="position: absolute; left: 0; height: 100%; width: 100%; opacity: 0"></textarea>
21 <h3>button controls for hard-to-remember keybindings</h3>
22 <table id="move_table" style="float: left">
24 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
25 <td style="text-align: center"><button id="square_move_up"></button></td>
26 <td><button id="hex_move_upright"></button></td>
29 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
30 <td stlye="text-align: center;">move</td>
31 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
34 <td><button id="hex_move_downleft"></button></td>
35 <td style="text-align: center"><button id="square_move_down"></button></td>
36 <td><button id="hex_move_downright"></button></td>
41 <td><button id="help"></button></td>
44 <td><button id="switch_to_chat"></button><br /></td>
47 <td><button id="switch_to_study"></button></td>
48 <td><button id="toggle_map_mode"></button>
51 <td><button id="switch_to_play"></button></td>
53 <button id="switch_to_take_thing"></button>
54 <button id="switch_to_drop_thing"></button>
55 <button id="door"></button>
56 <button id="consume"></button>
57 <button id="switch_to_command_thing"></button>
58 <button id="teleport"></button>
59 <button id="wear"></button>
60 <button id="spin"></button>
64 <td><button id="switch_to_edit"></button></td>
66 <button id="switch_to_write"></button>
67 <button id="flatten"></button>
68 <button id="install"></button>
69 <button id="switch_to_annotate"></button>
70 <button id="switch_to_portal"></button>
71 <button id="switch_to_name_thing"></button>
72 <button id="switch_to_password"></button>
73 <button id="switch_to_enter_face"></button>
74 <button id="switch_to_enter_hat"></button>
78 <td><button id="switch_to_admin_enter"></button></td>
80 <button id="switch_to_control_pw_type"></button>
81 <button id="switch_to_control_tile_type"></button>
82 <button id="switch_to_admin_thing_protect"></button>
83 <button id="toggle_tile_draw"></button>
88 <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 />
90 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
91 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
92 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
93 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
94 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
95 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
96 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
97 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
98 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
99 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
100 <li>help: <input id="key_help" type="text" value="h" />
101 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
102 <li>teleport: <input id="key_teleport" type="text" value="p" />
103 <li>spin: <input id="key_spin" type="text" value="S" />
104 <li>open/close: <input id="key_door" type="text" value="D" />
105 <li>consume: <input id="key_consume" type="text" value="C" />
106 <li>install: <input id="key_install" type="text" value="I" />
107 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
108 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
109 <li><input id="key_switch_to_enter_face" type="text" value="f" />
110 <li><input id="key_switch_to_enter_hat" type="text" value="H" />
111 <li><input id="key_switch_to_take_thing" type="text" value="z" />
112 <li><input id="key_switch_to_chat" type="text" value="t" />
113 <li><input id="key_switch_to_play" type="text" value="p" />
114 <li><input id="key_switch_to_study" type="text" value="?" />
115 <li><input id="key_switch_to_edit" type="text" value="E" />
116 <li><input id="key_switch_to_write" type="text" value="m" />
117 <li><input id="key_switch_to_name_thing" type="text" value="N" />
118 <li><input id="key_switch_to_command_thing" type="text" value="O" />
119 <li><input id="key_switch_to_password" type="text" value="P" />
120 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
121 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
122 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
123 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
124 <li><input id="key_switch_to_annotate" type="text" value="M" />
125 <li><input id="key_switch_to_portal" type="text" value="T" />
126 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
127 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
132 let websocket_location = "wss://plomlompom.com/rogue_chat/";
133 //let websocket_location = "ws://localhost:8000/";
139 'long': 'This mode allows you to interact with the map in various ways.'
144 '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. Toggle the map view to show or hide different information layers.'},
146 'short': 'world edit',
148 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
151 'short': 'name thing',
153 'long': 'Give name to/change name of carried thing.'
158 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
162 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
163 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
167 'intro': 'Enter number of direction to which you want to drop thing.',
168 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
170 'admin_thing_protect': {
171 'short': 'change thing protection',
172 'intro': '@ enter thing protection character:',
173 'long': 'Change protection character for carried thing.'
176 'short': 'edit face',
177 'intro': '@ enter face line (enter nothing to abort):',
178 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom. Eat cookies to extend the ASCII characters available for drawing.'
182 'intro': '@ enter hat line (enter nothing to abort):',
183 'long': 'Draw your hat as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
186 'short': 'edit tile',
188 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
191 'short': 'change protection character password',
192 'intro': '@ enter protection character for which you want to change the password:',
193 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
196 'short': 'change protection character password',
198 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
200 'control_tile_type': {
201 'short': 'change tiles protection',
202 'intro': '@ enter protection character which you want to draw:',
203 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
205 'control_tile_draw': {
206 'short': 'change tiles protection',
208 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
211 'short': 'annotate tile',
213 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
216 'short': 'edit portal',
218 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world 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.'
223 '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:\n\n/nick NAME – re-name yourself to NAME'
228 'long': 'Enter your player name.'
230 'waiting_for_server': {
231 'short': 'waiting for server response',
232 'intro': '@ waiting for server …',
233 'long': 'Waiting for a server response.'
236 'short': 'waiting for server response',
238 'long': 'Waiting for a server response.'
241 'short': 'set world edit password',
243 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
246 'short': 'become admin',
247 'intro': '@ enter admin password:',
248 'long': 'This mode allows you to become admin if you know an admin password.'
253 'long': 'This mode allows you access to actions limited to administrators.'
256 let key_descriptions = {
258 'flatten': 'flatten surroundings',
259 'teleport': 'teleport',
260 'door': 'open/close',
261 'consume': 'consume',
262 'install': '(un-)install',
265 'toggle_map_mode': 'toggle map view',
266 'toggle_tile_draw': 'toggle protection character drawing',
267 'hex_move_upleft': 'up-left',
268 'hex_move_upright': 'up-right',
269 'hex_move_right': 'right',
270 'hex_move_left': 'left',
271 'hex_move_downleft': 'down-left',
272 'hex_move_downright': 'down-right',
273 'square_move_up': 'up',
274 'square_move_left': 'left',
275 'square_move_down': 'down',
276 'square_move_right': 'right',
278 for (const mode_name of Object.keys(mode_helps)) {
279 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
282 let rows_selector = document.getElementById("n_rows");
283 let cols_selector = document.getElementById("n_cols");
284 let key_selectors = document.querySelectorAll('[id^="key_"]');
286 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
287 const action = key_switch_selector.id.slice("key_switch_to_".length);
288 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
291 function restore_selector_value(selector) {
292 let stored_selection = window.localStorage.getItem(selector.id);
293 if (stored_selection) {
294 selector.value = stored_selection;
297 restore_selector_value(rows_selector);
298 restore_selector_value(cols_selector);
299 for (let key_selector of key_selectors) {
300 restore_selector_value(key_selector);
303 function escapeHTML(str) {
305 replace(/&/g, '&').
306 replace(/</g, '<').
307 replace(/>/g, '>').
308 replace(/'/g, ''').
309 replace(/"/g, '"');
313 initialize: function() {
314 this.rows = rows_selector.value;
315 this.cols = cols_selector.value;
316 this.pre_el = document.getElementById("terminal");
317 this.set_default_colors();
321 for (let y = 0, x = 0; y <= this.rows; x++) {
322 if (x == this.cols) {
325 this.content.push(line);
327 if (y == this.rows) {
334 apply_colors: function() {
335 this.pre_el.style.color = this.foreground;
336 this.pre_el.style.backgroundColor = this.background;
338 set_default_colors: function() {
339 this.foreground = 'white';
340 this.background = 'black';
343 set_random_colors: function() {
344 function rand(offset) {
345 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
347 this.foreground = '#' + rand(159) + rand(159) + rand(159);
348 this.background = '#' + rand(0) + rand(0) + rand(0);
351 blink_screen: function() {
352 this.pre_el.style.color = this.background;
353 this.pre_el.style.backgroundColor = this.foreground;
355 this.pre_el.style.color = this.foreground;
356 this.pre_el.style.backgroundColor = this.background;
359 refresh: function() {
360 let pre_content = '';
361 for (let y = 0; y < this.rows; y++) {
362 let line = this.content[y].join('');
364 if (y in tui.links) {
366 for (let span of tui.links[y]) {
367 chunks.push(escapeHTML(line.slice(start_x, span[0])));
368 chunks.push('<a target="_blank" href="');
369 chunks.push(escapeHTML(span[2]));
371 chunks.push(escapeHTML(line.slice(span[0], span[1])));
375 chunks.push(escapeHTML(line.slice(start_x)));
377 chunks = [escapeHTML(line)];
379 for (const chunk of chunks) {
380 pre_content += chunk;
384 this.pre_el.innerHTML = pre_content;
386 write: function(start_y, start_x, msg) {
387 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
388 this.content[start_y][x] = msg[i];
391 drawBox: function(start_y, start_x, height, width) {
392 let end_y = start_y + height;
393 let end_x = start_x + width;
394 for (let y = start_y, x = start_x; y < this.rows; x++) {
402 this.content[y][x] = ' ';
406 terminal.initialize();
409 tokenize: function(str) {
414 for (let i = 0; i < str.length; i++) {
420 } else if (c == '\\') {
422 } else if (c == '"') {
427 } else if (c == '"') {
429 } else if (c === ' ') {
430 if (token.length > 0) {
438 if (token.length > 0) {
443 parse_yx: function(position_string) {
444 let coordinate_strings = position_string.split(',')
445 let position = [0, 0];
446 position[0] = parseInt(coordinate_strings[0].slice(2));
447 position[1] = parseInt(coordinate_strings[1].slice(2));
459 init: function(url) {
461 this.websocket = new WebSocket(this.url);
462 this.websocket.onopen = function(event) {
463 game.thing_types = {};
465 server.send(['TASKS']);
466 server.send(['TERRAINS']);
467 server.send(['THING_TYPES']);
468 tui.log_msg("@ server connected! :)");
469 tui.switch_mode('login');
471 this.websocket.onclose = function(event) {
472 tui.switch_mode('waiting_for_server');
473 tui.log_msg("@ server disconnected :(");
475 this.websocket.onmessage = this.handle_event;
477 reconnect_to: function(url) {
478 this.websocket.close();
481 send: function(tokens) {
482 this.websocket.send(unparser.untokenize(tokens));
484 handle_event: function(event) {
485 let tokens = parser.tokenize(event.data);
486 if (tokens[0] === 'TURN') {
487 game.turn_complete = false;
488 game.turn = parseInt(tokens[1]);
489 } else if (tokens[0] === 'OTHER_WIPE') {
490 game.portals_new = {};
491 explorer.annotations_new = {};
492 game.things_new = [];
493 } else if (tokens[0] === 'THING') {
494 let t = game.get_thing_temp(tokens[4], true);
495 t.position = parser.parse_yx(tokens[1]);
497 t.protection = tokens[3];
498 t.portable = parseInt(tokens[5]);
499 t.commandable = parseInt(tokens[6]);
500 } else if (tokens[0] === 'THING_NAME') {
501 let t = game.get_thing_temp(tokens[1]);
503 } else if (tokens[0] === 'THING_FACE') {
504 let t = game.get_thing_temp(tokens[1]);
506 } else if (tokens[0] === 'THING_HAT') {
507 let t = game.get_thing_temp(tokens[1]);
509 } else if (tokens[0] === 'THING_CHAR') {
510 let t = game.get_thing_temp(tokens[1]);
511 t.thing_char = tokens[2];
512 } else if (tokens[0] === 'TASKS') {
513 game.tasks = tokens[1].split(',');
514 tui.mode_write.legal = game.tasks.includes('WRITE');
515 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
516 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
517 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
518 } else if (tokens[0] === 'THING_TYPE') {
519 game.thing_types[tokens[1]] = tokens[2]
520 } else if (tokens[0] === 'THING_CARRYING') {
521 let t = game.get_thing_temp(tokens[1]);
522 t.carrying = game.get_thing_temp(tokens[2], false);
523 } else if (tokens[0] === 'THING_INSTALLED') {
524 let t = game.get_thing_temp(tokens[1]);
526 } else if (tokens[0] === 'TERRAIN') {
527 game.terrains[tokens[1]] = tokens[2]
528 } else if (tokens[0] === 'MAP') {
529 game.map_geometry_new = tokens[1];
530 game.map_size_new = parser.parse_yx(tokens[2]);
531 game.map_new = tokens[3]
532 } else if (tokens[0] === 'FOV') {
533 game.fov_new = tokens[1]
534 } else if (tokens[0] === 'MAP_CONTROL') {
535 game.map_control_new = tokens[1]
536 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
537 game.portals = game.portals_new;
538 game.map_geometry = game.map_geometry_new;
539 game.map_size = game.map_size_new;
540 game.map = game.map_new;
541 game.fov = game.fov_new;
543 game.map_control = game.map_control_new;
544 explorer.annotations = explorer.annotations_new;
545 explorer.info_cached = false;
546 game.things = game.things_new;
547 game.player = game.things[game.player_id];
548 game.players_hat_chars = game.players_hat_chars_new;
549 game.turn_complete = true;
550 if (tui.mode.name == 'post_login_wait') {
551 tui.switch_mode('play');
555 } else if (tokens[0] === 'CHAT') {
556 tui.log_msg('# ' + tokens[1], 1);
557 } else if (tokens[0] === 'CHATFACE') {
558 tui.draw_face = tokens[1];
560 } else if (tokens[0] === 'REPLY') {
561 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
562 } else if (tokens[0] === 'PLAYER_ID') {
563 game.player_id = parseInt(tokens[1]);
564 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
565 game.players_hat_chars_new = tokens[1];
566 } else if (tokens[0] === 'LOGIN_OK') {
567 this.send(['GET_GAMESTATE']);
568 tui.switch_mode('post_login_wait');
569 } else if (tokens[0] === 'DEFAULT_COLORS') {
570 terminal.set_default_colors();
571 } else if (tokens[0] === 'RANDOM_COLORS') {
572 terminal.set_random_colors();
573 } else if (tokens[0] === 'ADMIN_OK') {
575 tui.log_msg('@ you now have admin rights');
576 tui.switch_mode('admin');
577 } else if (tokens[0] === 'PORTAL') {
578 let position = parser.parse_yx(tokens[1]);
579 game.portals_new[position] = tokens[2];
580 } else if (tokens[0] === 'ANNOTATION') {
581 let position = parser.parse_yx(tokens[1]);
582 explorer.annotations_new[position] = tokens[2];
583 } else if (tokens[0] === 'UNHANDLED_INPUT') {
584 tui.log_msg('? unknown command');
585 } else if (tokens[0] === 'PLAY_ERROR') {
586 tui.log_msg('? ' + tokens[1]);
587 terminal.blink_screen();
588 } else if (tokens[0] === 'ARGUMENT_ERROR') {
589 tui.log_msg('? syntax error: ' + tokens[1]);
590 } else if (tokens[0] === 'GAME_ERROR') {
591 tui.log_msg('? game error: ' + tokens[1]);
592 } else if (tokens[0] === 'PONG') {
595 tui.log_msg('? unhandled input: ' + event.data);
601 quote: function(str) {
603 for (let i = 0; i < str.length; i++) {
605 if (['"', '\\'].includes(c)) {
611 return quoted.join('');
613 to_yx: function(yx_coordinate) {
614 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
616 untokenize: function(tokens) {
617 let quoted_tokens = [];
618 for (let token of tokens) {
619 quoted_tokens.push(this.quote(token));
621 return quoted_tokens.join(" ");
626 constructor(name, has_input_prompt=false, shows_info=false,
627 is_intro=false, is_single_char_entry=false) {
629 this.short_desc = mode_helps[name].short;
630 this.available_modes = [];
631 this.available_actions = [];
632 this.has_input_prompt = has_input_prompt;
633 this.shows_info= shows_info;
634 this.is_intro = is_intro;
635 this.help_intro = mode_helps[name].long;
636 this.intro_msg = mode_helps[name].intro;
637 this.is_single_char_entry = is_single_char_entry;
640 *iter_available_modes() {
641 for (let mode_name of this.available_modes) {
642 let mode = tui['mode_' + mode_name];
646 let key = tui.keys['switch_to_' + mode.name];
650 list_available_modes() {
652 if (this.available_modes.length > 0) {
653 msg += 'Other modes available from here:\n';
654 for (let [mode, key] of this.iter_available_modes()) {
655 msg += '[' + key + '] – ' + mode.short_desc + '\n';
660 mode_switch_on_key(key_event) {
661 for (let [mode, key] of this.iter_available_modes()) {
662 if (key_event.key == key) {
663 event.preventDefault();
664 tui.switch_mode(mode.name);
676 window_width: terminal.cols / 2,
684 mode_waiting_for_server: new Mode('waiting_for_server',
686 mode_login: new Mode('login', true, false, true),
687 mode_post_login_wait: new Mode('post_login_wait'),
688 mode_chat: new Mode('chat', true),
689 mode_annotate: new Mode('annotate', true, true),
690 mode_play: new Mode('play'),
691 mode_study: new Mode('study', false, true),
692 mode_write: new Mode('write', false, false, false, true),
693 mode_edit: new Mode('edit'),
694 mode_control_pw_type: new Mode('control_pw_type', true),
695 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
696 mode_portal: new Mode('portal', true, true),
697 mode_password: new Mode('password', true),
698 mode_name_thing: new Mode('name_thing', true, true),
699 mode_command_thing: new Mode('command_thing', true),
700 mode_take_thing: new Mode('take_thing', true),
701 mode_drop_thing: new Mode('drop_thing', true),
702 mode_enter_face: new Mode('enter_face', true),
703 mode_enter_hat: new Mode('enter_hat', true),
704 mode_admin_enter: new Mode('admin_enter', true),
705 mode_admin: new Mode('admin'),
706 mode_control_pw_pw: new Mode('control_pw_pw', true),
707 mode_control_tile_type: new Mode('control_tile_type', true),
708 mode_control_tile_draw: new Mode('control_tile_draw'),
710 'flatten': 'FLATTEN_SURROUNDINGS',
711 'take_thing': 'PICK_UP',
712 'drop_thing': 'DROP',
715 'install': 'INSTALL',
717 'command': 'COMMAND',
718 'consume': 'INTOXICATE',
728 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
729 "command_thing", "take_thing", "drop_thing"]
730 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
732 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
733 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
734 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
735 "control_tile_type", "chat",
736 "study", "play", "edit"]
737 this.mode_admin.available_actions = ["move"];
738 this.mode_control_tile_draw.available_modes = ["admin_enter"]
739 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
740 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
741 "password", "chat", "study", "play",
742 "admin_enter", "enter_face", "enter_hat"]
743 this.mode_edit.available_actions = ["move", "flatten", "install",
745 this.inputEl = document.getElementById("input");
746 this.switch_mode('waiting_for_server');
747 this.recalc_input_lines();
748 this.height_header = this.height_turn_line + this.height_mode_line;
751 init_keys: function() {
752 document.getElementById("move_table").hidden = true;
754 for (let key_selector of key_selectors) {
755 this.keys[key_selector.id.slice(4)] = key_selector.value;
757 this.movement_keys = {};
758 let geometry_prefix = 'undefinedMapGeometry_';
759 if (game.map_geometry) {
760 geometry_prefix = game.map_geometry.toLowerCase() + '_';
762 for (const key_name of Object.keys(key_descriptions)) {
763 if (key_name.startsWith(geometry_prefix)) {
764 let direction = key_name.split('_')[2].toUpperCase();
765 let key = this.keys[key_name];
766 this.movement_keys[key] = direction;
769 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
770 if (move_button.id.startsWith('key_')) {
773 move_button.hidden = true;
775 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
776 document.getElementById("move_table").hidden = false;
777 move_button.hidden = false;
779 for (let el of document.getElementsByTagName("button")) {
780 let action_desc = key_descriptions[el.id];
781 let action_key = '[' + this.keys[el.id] + ']';
782 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
785 task_action_on: function(action) {
786 return game.tasks.includes(this.action_tasks[action]);
788 switch_mode: function(mode_name) {
790 function fail(msg, return_mode='play') {
791 tui.log_msg('? ' + msg);
792 terminal.blink_screen();
793 tui.switch_mode(return_mode);
796 if (this.mode && this.mode.name == 'control_tile_draw') {
797 tui.log_msg('@ finished tile protection drawing.')
799 this.draw_face = false;
800 this.tile_draw = false;
801 if (mode_name == 'command_thing' && (!game.player.carrying
802 || !game.player.carrying.commandable)) {
803 return fail('not carrying anything commandable');
804 } else if (mode_name == 'name_thing' && !game.player.carrying) {
805 return fail('not carrying anything to re-name');
806 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
807 return fail('not carrying anything to protect')
808 } else if (mode_name == 'take_thing' && game.player.carrying) {
809 return fail('already carrying something');
810 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
811 return fail('not carrying anything droppable');
812 } else if (mode_name == 'enter_hat' && !game.player.hat) {
813 return fail('not wearing hat to edit', 'edit');
815 if (mode_name == 'admin_enter' && this.is_admin) {
818 this.mode = this['mode_' + mode_name];
819 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
820 this.map_mode = 'protections';
821 } else if (this.mode.name != "edit") {
822 this.map_mode = 'terrain + things';
824 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
825 explorer.position = game.player.position;
827 this.inputEl.value = "";
828 this.restore_input_values();
829 for (let el of document.getElementsByTagName("button")) {
832 document.getElementById("help").disabled = false;
833 for (const action of this.mode.available_actions) {
834 if (["move", "move_explorer"].includes(action)) {
835 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
836 move_key.disabled = false;
838 } else if (Object.keys(this.action_tasks).includes(action)) {
839 if (this.task_action_on(action)) {
840 document.getElementById(action).disabled = false;
843 document.getElementById(action).disabled = false;
846 for (const mode_name of this.mode.available_modes) {
847 document.getElementById('switch_to_' + mode_name).disabled = false;
849 if (this.mode.intro_msg.length > 0) {
850 this.log_msg(this.mode.intro_msg);
852 if (this.mode.name == 'login') {
853 if (this.login_name) {
854 server.send(['LOGIN', this.login_name]);
856 this.log_msg("? need login name");
858 } else if (this.mode.is_single_char_entry) {
859 this.show_help = true;
860 } else if (this.mode.name == 'take_thing') {
861 this.log_msg("Portable things in reach for pick-up:");
862 const y = game.player.position[0]
863 const x = game.player.position[1]
864 let select_range = [y.toString() + ':' + x.toString(),
865 (y + 0).toString() + ':' + (x - 1).toString(),
866 (y + 0).toString() + ':' + (x + 1).toString(),
867 (y - 1).toString() + ':' + (x).toString(),
868 (y + 1).toString() + ':' + (x).toString()];
869 if (game.map_geometry == 'Hex') {
871 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
872 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
874 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
875 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
878 this.selectables = [];
879 for (const t_id in game.things) {
880 const t = game.things[t_id];
881 if (select_range.includes(t.position[0].toString()
882 + ':' + t.position[1].toString())
884 this.selectables.push(t_id);
887 if (this.selectables.length == 0) {
888 this.log_msg('none');
889 terminal.blink_screen();
890 this.switch_mode('play');
893 for (let [i, t_id] of this.selectables.entries()) {
894 const t = game.things[t_id];
895 this.log_msg(i + ': ' + explorer.get_thing_info(t));
898 } else if (this.mode.name == 'drop_thing') {
899 this.log_msg('Direction to drop thing to:');
900 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
901 for (let [i, direction] of this.selectables.entries()) {
902 this.log_msg(i + ': ' + direction);
904 } else if (this.mode.name == 'enter_hat') {
905 this.log_msg('legal characters: ' + game.players_hat_chars);
906 } else if (this.mode.name == 'command_thing') {
907 server.send(['TASK:COMMAND', 'HELP']);
908 } else if (this.mode.name == 'control_pw_pw') {
909 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
910 } else if (this.mode.name == 'control_tile_draw') {
911 this.log_msg('@ can draw protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
915 offset_links: function(offset, links) {
916 for (let y in links) {
917 let real_y = offset[0] + parseInt(y);
918 if (!this.links[real_y]) {
919 this.links[real_y] = [];
921 for (let link of links[y]) {
922 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
923 this.links[real_y].push(offset_link);
927 restore_input_values: function() {
928 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
929 let info = explorer.annotations[explorer.position];
930 if (info != "(none)") {
931 this.inputEl.value = info;
933 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
934 let portal = game.portals[explorer.position]
935 this.inputEl.value = portal;
936 } else if (this.mode.name == 'password') {
937 this.inputEl.value = this.password;
938 } else if (this.mode.name == 'name_thing') {
939 if (game.player.carrying && game.player.carrying.name_) {
940 this.inputEl.value = game.player.carrying.name_;
942 } else if (this.mode.name == 'admin_thing_protect') {
943 if (game.player.carrying && game.player.carrying.protection) {
944 this.inputEl.value = game.player.carrying.protection;
946 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
947 const start = this.ascii_draw_stage * 6;
948 const end = (this.ascii_draw_stage + 1) * 6;
949 if (this.mode.name == 'enter_face') {
950 this.inputEl.value = game.player.face.slice(start, end);
951 } else if (this.mode.name == 'enter_hat') {
952 this.inputEl.value = game.player.hat.slice(start, end);
956 recalc_input_lines: function() {
957 if (this.mode.has_input_prompt) {
959 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
961 this.input_lines = [];
963 this.height_input = this.input_lines.length;
965 msg_into_lines_of_width: function(msg, width) {
966 function push_inner_link(y, end_x) {
967 if (!inner_links[y]) {
970 inner_links[y].push([url_start_x, end_x, url]);
972 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
975 for (const match of matches) {
976 const url = match[0];
977 const url_start = match.index;
978 const url_end = match.index + match[0].length;
979 link_data[url_start] = url;
980 url_ends.push(url_end);
984 let inner_links = {};
988 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
989 if (x >= width || msg[i] == "\n") {
991 push_inner_link(y, chunk.length);
993 if (url_ends[0] == i) {
1001 if (msg[i] == "\n") {
1006 if (msg[i] != "\n") {
1009 if (i in link_data) {
1013 } else if (url_ends[0] == i) {
1015 push_inner_link(y, x);
1021 push_inner_link(lines.length - 1, chunk.length);
1023 return [lines, inner_links];
1025 log_msg: function(msg) {
1027 while (this.log.length > 100) {
1030 this.full_refresh();
1032 pick_selectable: function(task_name) {
1033 const i = parseInt(this.inputEl.value);
1034 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1035 tui.log_msg('? invalid index, aborted');
1037 server.send(['TASK:' + task_name, tui.selectables[i]]);
1039 this.inputEl.value = "";
1040 this.switch_mode('play');
1042 enter_ascii_art: function(command) {
1043 if (this.inputEl.value.length != 6) {
1044 this.log_msg('? wrong input length, must be 6; try again');
1047 this.log_msg(' ' + this.inputEl.value);
1048 this.full_ascii_draw += this.inputEl.value;
1049 this.ascii_draw_stage += 1;
1050 if (this.ascii_draw_stage < 3) {
1051 this.restore_input_values();
1053 server.send([command, this.full_ascii_draw]);
1054 this.full_ascii_draw = '';
1055 this.ascii_draw_stage = 0;
1056 this.inputEl.value = '';
1057 this.switch_mode('edit');
1060 draw_map: function() {
1061 if (!game.turn_complete && this.map_lines.length == 0) {
1064 if (game.turn_complete) {
1065 let map_lines_split = [];
1067 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1068 if (j == game.map_size[1]) {
1069 map_lines_split.push(line);
1073 if (this.map_mode == 'protections') {
1074 line.push(game.map_control[i] + ' ');
1076 line.push(game.map[i] + ' ');
1079 map_lines_split.push(line);
1080 if (this.map_mode == 'terrain + annotations') {
1081 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1082 const yx = coordinate.split(',')
1083 map_lines_split[yx[0]][yx[1]] = 'A ';
1085 } else if (this.map_mode == 'terrain + things') {
1086 for (const p in game.portals) {
1087 let coordinate = p.split(',')
1088 let original = map_lines_split[coordinate[0]][coordinate[1]];
1089 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1091 let used_positions = [];
1092 function draw_thing(t, used_positions) {
1093 let symbol = game.thing_types[t.type_];
1094 let meta_char = ' ';
1096 meta_char = t.thing_char;
1098 if (used_positions.includes(t.position.toString())) {
1104 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1105 used_positions.push(t.position.toString());
1107 for (const thing_id in game.things) {
1108 let t = game.things[thing_id];
1109 if (t.type_ != 'Player') {
1110 draw_thing(t, used_positions);
1113 for (const thing_id in game.things) {
1114 let t = game.things[thing_id];
1115 if (t.type_ == 'Player') {
1116 draw_thing(t, used_positions);
1120 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1121 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1122 } else if (tui.map_mode != 'terrain + things') {
1123 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1126 if (game.map_geometry == 'Square') {
1127 for (let line_split of map_lines_split) {
1128 this.map_lines.push(line_split.join(''));
1130 } else if (game.map_geometry == 'Hex') {
1132 for (let line_split of map_lines_split) {
1133 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1141 let window_center = [terminal.rows / 2, this.window_width / 2];
1142 let center_position = [game.player.position[0], game.player.position[1]];
1143 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1144 center_position = [explorer.position[0], explorer.position[1]];
1146 center_position[1] = center_position[1] * 2;
1147 this.offset = [center_position[0] - window_center[0],
1148 center_position[1] - window_center[1]]
1149 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1150 this.offset[1] += 1;
1153 let term_y = Math.max(0, -this.offset[0]);
1154 let term_x = Math.max(0, -this.offset[1]);
1155 let map_y = Math.max(0, this.offset[0]);
1156 let map_x = Math.max(0, this.offset[1]);
1157 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1158 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1159 terminal.write(term_y, term_x, to_draw);
1162 draw_face_popup: function() {
1163 const t = game.things[this.draw_face];
1164 if (!t || !t.face) {
1165 this.draw_face = false;
1168 const start_x = tui.window_width - 10;
1171 t_char = t.thing_char;
1173 function draw_body_part(body_part, end_y) {
1174 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1175 terminal.write(end_y - 3, start_x, '| |');
1176 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1177 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1178 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1181 draw_body_part(t.face, terminal.rows - 2);
1184 draw_body_part(t.hat, terminal.rows - 5);
1186 terminal.write(terminal.rows - 1, start_x, '| |');
1188 draw_mode_line: function() {
1189 let help = 'hit [' + this.keys.help + '] for help';
1190 if (this.mode.has_input_prompt) {
1191 help = 'enter /help for help';
1193 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1195 draw_turn_line: function(n) {
1196 if (game.turn_complete) {
1197 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1200 draw_history: function() {
1201 let log_display_lines = [];
1203 let y_offset_in_log = 0;
1204 for (let line of this.log) {
1205 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1207 log_display_lines = log_display_lines.concat(new_lines);
1208 for (const y in link_data) {
1209 const rel_y = y_offset_in_log + parseInt(y);
1210 log_links[rel_y] = [];
1211 for (let link of link_data[y]) {
1212 log_links[rel_y].push(link);
1215 y_offset_in_log += new_lines.length;
1217 let i = log_display_lines.length - 1;
1218 for (let y = terminal.rows - 1 - this.height_input;
1219 y >= this.height_header && i >= 0;
1221 terminal.write(y, this.window_width, log_display_lines[i]);
1223 for (const key of Object.keys(log_links)) {
1224 if (parseInt(key) <= i) {
1225 delete log_links[key];
1228 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1230 this.offset_links(offset, log_links);
1232 draw_info: function() {
1233 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1234 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1235 let offset = [this.height_header, this.window_width];
1236 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1237 terminal.write(y, offset[1], lines[i]);
1239 this.offset_links(offset, link_data);
1241 draw_input: function() {
1242 if (this.mode.has_input_prompt) {
1243 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1244 terminal.write(y, this.window_width, this.input_lines[i]);
1248 draw_help: function() {
1249 let movement_keys_desc = '';
1250 if (!this.mode.is_intro) {
1251 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1253 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1254 if (this.mode.available_actions.length > 0) {
1255 content += "Available actions:\n";
1256 for (let action of this.mode.available_actions) {
1257 if (Object.keys(this.action_tasks).includes(action)) {
1258 if (!this.task_action_on(action)) {
1262 if (action == 'move_explorer') {
1265 if (action == 'move') {
1266 content += "[" + movement_keys_desc + "] – move\n"
1268 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1273 content += this.mode.list_available_modes();
1275 if (!this.mode.has_input_prompt) {
1276 start_x = this.window_width;
1277 this.draw_links = false;
1279 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1280 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1281 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1282 terminal.write(y, start_x, lines[i]);
1285 toggle_tile_draw: function() {
1286 if (tui.tile_draw) {
1287 tui.tile_draw = false;
1289 tui.tile_draw = true;
1292 toggle_map_mode: function() {
1293 if (tui.map_mode == 'terrain only') {
1294 tui.map_mode = 'terrain + annotations';
1295 } else if (tui.map_mode == 'terrain + annotations') {
1296 tui.map_mode = 'terrain + things';
1297 } else if (tui.map_mode == 'terrain + things') {
1298 tui.map_mode = 'protections';
1299 } else if (tui.map_mode == 'protections') {
1300 tui.map_mode = 'terrain only';
1303 full_refresh: function() {
1304 this.draw_links = true;
1306 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1307 this.recalc_input_lines();
1308 if (this.mode.is_intro) {
1309 this.draw_history();
1313 this.draw_turn_line();
1314 this.draw_mode_line();
1315 if (this.mode.shows_info) {
1318 this.draw_history();
1322 if (this.show_help) {
1325 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1326 this.draw_face_popup();
1328 if (!this.draw_links) {
1338 this.player_id = -1;
1341 this.things_new = {};
1346 this.map_control = "";
1347 this.map_control_new = "";
1348 this.map_size = [0,0];
1349 this.map_size_new = [0,0];
1351 this.portals_new = {};
1352 this.players_hat_chars = "";
1354 get_thing_temp: function(id_, create_if_not_found=false) {
1355 if (id_ in game.things_new) {
1356 return game.things_new[id_];
1357 } else if (create_if_not_found) {
1358 let t = new Thing([0,0]);
1359 game.things_new[id_] = t;
1363 get_thing: function(id_, create_if_not_found=false) {
1364 if (id_ in game.things) {
1365 return game.things[id_];
1368 move: function(start_position, direction) {
1369 let target = [start_position[0], start_position[1]];
1370 if (direction == 'LEFT') {
1372 } else if (direction == 'RIGHT') {
1374 } else if (game.map_geometry == 'Square') {
1375 if (direction == 'UP') {
1377 } else if (direction == 'DOWN') {
1380 } else if (game.map_geometry == 'Hex') {
1381 let start_indented = start_position[0] % 2;
1382 if (direction == 'UPLEFT') {
1384 if (!start_indented) {
1387 } else if (direction == 'UPRIGHT') {
1389 if (start_indented) {
1392 } else if (direction == 'DOWNLEFT') {
1394 if (!start_indented) {
1397 } else if (direction == 'DOWNRIGHT') {
1399 if (start_indented) {
1404 if (target[0] < 0 || target[1] < 0 ||
1405 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1410 teleport: function() {
1411 if (game.player.position in this.portals) {
1412 server.reconnect_to(this.portals[game.player.position]);
1414 terminal.blink_screen();
1415 tui.log_msg('? not standing on portal')
1423 server.init(websocket_location);
1428 annotations_new: {},
1430 move: function(direction) {
1431 let target = game.move(this.position, direction);
1433 this.position = target
1434 this.info_cached = false;
1435 if (tui.tile_draw) {
1436 this.send_tile_control_command();
1439 terminal.blink_screen();
1442 get_info: function() {
1443 if (this.info_cached) {
1444 return this.info_cached;
1446 let info_to_cache = '';
1447 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1448 if (game.fov[position_i] != '.') {
1449 info_to_cache += 'outside field of view';
1451 for (let t_id in game.things) {
1452 let t = game.things[t_id];
1453 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1454 info_to_cache += "THING: " + this.get_thing_info(t);
1455 let protection = t.protection;
1456 if (protection == '.') {
1457 protection = 'none';
1459 info_to_cache += " / protection: " + protection + "\n";
1461 info_to_cache += t.hat.slice(0, 6) + '\n';
1462 info_to_cache += t.hat.slice(6, 12) + '\n';
1463 info_to_cache += t.hat.slice(12, 18) + '\n';
1466 info_to_cache += t.face.slice(0, 6) + '\n';
1467 info_to_cache += t.face.slice(6, 12) + '\n';
1468 info_to_cache += t.face.slice(12, 18) + '\n';
1472 let terrain_char = game.map[position_i]
1473 let terrain_desc = '?'
1474 if (game.terrains[terrain_char]) {
1475 terrain_desc = game.terrains[terrain_char];
1477 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1478 let protection = game.map_control[position_i];
1479 if (protection == '.') {
1480 protection = 'unprotected';
1482 info_to_cache += 'PROTECTION: ' + protection + '\n';
1483 if (this.position in game.portals) {
1484 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1486 if (this.position in this.annotations) {
1487 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1490 this.info_cached = info_to_cache;
1491 return this.info_cached;
1493 get_thing_info: function(t) {
1494 const symbol = game.thing_types[t.type_];
1495 let info = t.type_ + " / " + symbol;
1497 info += t.thing_char;
1500 info += " (" + t.name_ + ")";
1503 info += " / installed";
1507 annotate: function(msg) {
1508 if (msg.length == 0) {
1509 msg = " "; // triggers annotation deletion
1511 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1513 set_portal: function(msg) {
1514 if (msg.length == 0) {
1515 msg = " "; // triggers portal deletion
1517 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1519 send_tile_control_command: function() {
1520 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1524 tui.inputEl.addEventListener('input', (event) => {
1525 if (tui.mode.has_input_prompt) {
1526 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1527 if (tui.inputEl.value.length > max_length) {
1528 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1530 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1531 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1532 tui.switch_mode('edit');
1536 document.onclick = function() {
1537 if (!tui.mode.is_single_char_entry) {
1538 tui.show_help = false;
1541 tui.inputEl.addEventListener('keydown', (event) => {
1542 tui.show_help = false;
1543 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1544 event.preventDefault();
1546 if ((!tui.mode.is_intro && event.key == 'Escape')
1547 || (tui.mode.has_input_prompt && event.key == 'Enter'
1548 && tui.inputEl.value.length == 0
1549 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1550 'admin_enter'].includes(tui.mode.name))) {
1551 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1552 tui.log_msg('@ aborted');
1554 tui.switch_mode('play');
1555 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1556 tui.show_help = true;
1557 tui.inputEl.value = "";
1558 tui.restore_input_values();
1559 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1560 && !tui.mode.is_single_char_entry) {
1561 tui.show_help = true;
1562 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1563 tui.login_name = tui.inputEl.value;
1564 server.send(['LOGIN', tui.inputEl.value]);
1565 tui.inputEl.value = "";
1566 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1567 tui.enter_ascii_art('PLAYER_FACE');
1568 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1569 tui.enter_ascii_art('PLAYER_HAT');
1570 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1571 server.send(['TASK:COMMAND', tui.inputEl.value]);
1572 tui.inputEl.value = "";
1573 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1574 tui.pick_selectable('PICK_UP');
1575 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1576 tui.pick_selectable('DROP');
1577 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1578 if (tui.inputEl.value.length == 0) {
1579 tui.log_msg('@ aborted');
1581 server.send(['SET_MAP_CONTROL_PASSWORD',
1582 tui.tile_control_char, tui.inputEl.value]);
1583 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1585 tui.switch_mode('admin');
1586 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1587 explorer.set_portal(tui.inputEl.value);
1588 tui.switch_mode('edit');
1589 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1590 if (tui.inputEl.value.length == 0) {
1591 tui.inputEl.value = " ";
1593 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1594 tui.switch_mode('edit');
1595 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1596 explorer.annotate(tui.inputEl.value);
1597 tui.switch_mode('edit');
1598 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1599 if (tui.inputEl.value.length == 0) {
1600 tui.inputEl.value = " ";
1602 tui.password = tui.inputEl.value
1603 tui.switch_mode('edit');
1604 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1605 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1606 tui.switch_mode('play');
1607 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1608 if (tui.inputEl.value.length != 1) {
1609 tui.log_msg('@ entered non-single-char, therefore aborted');
1610 tui.switch_mode('admin');
1612 tui.tile_control_char = tui.inputEl.value[0];
1613 tui.switch_mode('control_pw_pw');
1615 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1616 if (tui.inputEl.value.length != 1) {
1617 tui.log_msg('@ entered non-single-char, therefore aborted');
1618 tui.switch_mode('admin');
1620 tui.tile_control_char = tui.inputEl.value[0];
1621 tui.switch_mode('control_tile_draw');
1623 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1624 if (tui.inputEl.value.length != 1) {
1625 tui.log_msg('@ entered non-single-char, therefore aborted');
1627 server.send(['THING_PROTECTION', tui.inputEl.value])
1628 tui.log_msg('@ sent new protection character for thing');
1630 tui.switch_mode('admin');
1631 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1632 let tokens = parser.tokenize(tui.inputEl.value);
1633 if (tokens.length > 0 && tokens[0].length > 0) {
1634 if (tui.inputEl.value[0][0] == '/') {
1635 if (tokens[0].slice(1) == 'nick') {
1636 if (tokens.length > 1) {
1637 server.send(['NICK', tokens[1]]);
1639 tui.log_msg('? need new name');
1642 tui.log_msg('? unknown command');
1645 server.send(['ALL', tui.inputEl.value]);
1647 } else if (tui.inputEl.valuelength > 0) {
1648 server.send(['ALL', tui.inputEl.value]);
1650 tui.inputEl.value = "";
1651 } else if (tui.mode.name == 'play') {
1652 if (tui.mode.mode_switch_on_key(event)) {
1654 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1655 server.send(["TASK:INTOXICATE"]);
1656 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1657 server.send(["TASK:DOOR"]);
1658 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1659 server.send(["TASK:WEAR"]);
1660 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1661 server.send(["TASK:SPIN"]);
1662 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1663 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1664 } else if (event.key === tui.keys.teleport) {
1667 } else if (tui.mode.name == 'study') {
1668 if (tui.mode.mode_switch_on_key(event)) {
1670 } else if (event.key in tui.movement_keys) {
1671 explorer.move(tui.movement_keys[event.key]);
1672 } else if (event.key == tui.keys.toggle_map_mode) {
1673 tui.toggle_map_mode();
1675 } else if (tui.mode.name == 'control_tile_draw') {
1676 if (tui.mode.mode_switch_on_key(event)) {
1678 } else if (event.key in tui.movement_keys) {
1679 explorer.move(tui.movement_keys[event.key]);
1680 } else if (event.key === tui.keys.toggle_tile_draw) {
1681 tui.toggle_tile_draw();
1683 } else if (tui.mode.name == 'admin') {
1684 if (tui.mode.mode_switch_on_key(event)) {
1686 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1687 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1689 } else if (tui.mode.name == 'edit') {
1690 if (tui.mode.mode_switch_on_key(event)) {
1692 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1693 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1694 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1695 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1696 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1697 server.send(["TASK:INSTALL", tui.password]);
1698 } else if (event.key == tui.keys.toggle_map_mode) {
1699 tui.toggle_map_mode();
1705 rows_selector.addEventListener('input', function() {
1706 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1709 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1710 terminal.initialize();
1713 cols_selector.addEventListener('input', function() {
1714 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1717 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1718 terminal.initialize();
1719 tui.window_width = terminal.cols / 2,
1722 for (let key_selector of key_selectors) {
1723 key_selector.addEventListener('input', function() {
1724 window.localStorage.setItem(key_selector.id, key_selector.value);
1728 window.setInterval(function() {
1729 if (server.websocket.readyState == 1) {
1730 server.send(['PING']);
1731 } else if (server.websocket.readyState != 0) {
1732 server.reconnect_to(server.url);
1733 tui.log_msg('@ attempting reconnect …')
1736 window.setInterval(function() {
1737 if (document.activeElement.tagName.toLowerCase() != 'input') {
1738 tui.inputEl.focus();
1741 document.getElementById("help").onclick = function() {
1742 tui.show_help = true;
1745 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1746 const mode = switchEl.id.slice("switch_to_".length);
1747 switchEl.onclick = function() {
1748 tui.switch_mode(mode);
1752 document.getElementById("toggle_tile_draw").onclick = function() {
1753 tui.toggle_tile_draw();
1755 document.getElementById("toggle_map_mode").onclick = function() {
1756 tui.toggle_map_mode();
1759 document.getElementById("flatten").onclick = function() {
1760 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1762 document.getElementById("door").onclick = function() {
1763 server.send(['TASK:DOOR']);
1765 document.getElementById("consume").onclick = function() {
1766 server.send(['TASK:INTOXICATE']);
1768 document.getElementById("install").onclick = function() {
1769 server.send(['TASK:INSTALL', tui.password]);
1771 document.getElementById("wear").onclick = function() {
1772 server.send(['TASK:WEAR']);
1774 document.getElementById("spin").onclick = function() {
1775 server.send(['TASK:SPIN']);
1777 document.getElementById("teleport").onclick = function() {
1780 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1781 if (move_button.id.startsWith('key_')) { // not a move button
1784 let direction = move_button.id.split('_')[2].toUpperCase();
1787 if (tui.mode.available_actions.includes("move")) {
1788 server.send(['TASK:MOVE', direction]);
1789 } else if (tui.mode.available_actions.includes("move_explorer")) {
1790 explorer.move(direction);
1794 move_button.onmousedown = function() {
1796 move_repeat = window.setInterval(move, 100);
1798 move_button.onmouseup = function() {
1799 window.clearInterval(move_repeat);