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; z-index: -1;"></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 } else if (tokens[0] === 'OTHER_WIPE') {
489 game.portals_new = {};
490 explorer.annotations_new = {};
491 game.things_new = [];
492 } else if (tokens[0] === 'BLADDER_PRESSURE') {
493 game.bladder_pressure_new = parseInt(tokens[1])
494 } else if (tokens[0] === 'THING') {
495 let t = game.get_thing_temp(tokens[4], true);
496 t.position = parser.parse_yx(tokens[1]);
498 t.protection = tokens[3];
499 t.portable = parseInt(tokens[5]);
500 t.commandable = parseInt(tokens[6]);
501 } else if (tokens[0] === 'THING_NAME') {
502 let t = game.get_thing_temp(tokens[1]);
504 } else if (tokens[0] === 'THING_FACE') {
505 let t = game.get_thing_temp(tokens[1]);
507 } else if (tokens[0] === 'THING_HAT') {
508 let t = game.get_thing_temp(tokens[1]);
510 } else if (tokens[0] === 'THING_CHAR') {
511 let t = game.get_thing_temp(tokens[1]);
512 t.thing_char = tokens[2];
513 } else if (tokens[0] === 'TASKS') {
514 game.tasks = tokens[1].split(',');
515 tui.mode_write.legal = game.tasks.includes('WRITE');
516 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
517 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
518 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
519 } else if (tokens[0] === 'THING_TYPE') {
520 game.thing_types[tokens[1]] = tokens[2]
521 } else if (tokens[0] === 'THING_CARRYING') {
522 let t = game.get_thing_temp(tokens[1]);
523 t.carrying = game.get_thing_temp(tokens[2], false);
524 } else if (tokens[0] === 'THING_INSTALLED') {
525 let t = game.get_thing_temp(tokens[1]);
527 } else if (tokens[0] === 'TERRAIN') {
528 game.terrains[tokens[1]] = tokens[2]
529 } else if (tokens[0] === 'MAP') {
530 game.map_geometry_new = tokens[1];
531 game.map_size_new = parser.parse_yx(tokens[2]);
532 game.map_new = tokens[3]
533 } else if (tokens[0] === 'FOV') {
534 game.fov_new = tokens[1]
535 } else if (tokens[0] === 'MAP_CONTROL') {
536 game.map_control_new = tokens[1]
537 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
538 game.portals = game.portals_new;
539 game.map_geometry = game.map_geometry_new;
540 game.map_size = game.map_size_new;
541 game.map = game.map_new;
542 game.fov = game.fov_new;
544 game.map_control = game.map_control_new;
545 explorer.annotations = explorer.annotations_new;
546 explorer.info_cached = false;
547 game.things = game.things_new;
548 game.player = game.things[game.player_id];
549 game.players_hat_chars = game.players_hat_chars_new;
550 game.bladder_pressure = game.bladder_pressure_new
551 game.turn_complete = true;
552 if (tui.mode.name == 'post_login_wait') {
553 tui.switch_mode('play');
557 } else if (tokens[0] === 'CHAT') {
558 tui.log_msg('# ' + tokens[1], 1);
559 } else if (tokens[0] === 'CHATFACE') {
560 tui.draw_face = tokens[1];
562 } else if (tokens[0] === 'REPLY') {
563 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
564 } else if (tokens[0] === 'PLAYER_ID') {
565 game.player_id = parseInt(tokens[1]);
566 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
567 game.players_hat_chars_new = tokens[1];
568 } else if (tokens[0] === 'LOGIN_OK') {
569 this.send(['GET_GAMESTATE']);
570 tui.switch_mode('post_login_wait');
571 } else if (tokens[0] === 'DEFAULT_COLORS') {
572 terminal.set_default_colors();
573 } else if (tokens[0] === 'RANDOM_COLORS') {
574 terminal.set_random_colors();
575 } else if (tokens[0] === 'ADMIN_OK') {
577 tui.log_msg('@ you now have admin rights');
578 tui.switch_mode('admin');
579 } else if (tokens[0] === 'PORTAL') {
580 let position = parser.parse_yx(tokens[1]);
581 game.portals_new[position] = tokens[2];
582 } else if (tokens[0] === 'ANNOTATION') {
583 let position = parser.parse_yx(tokens[1]);
584 explorer.annotations_new[position] = tokens[2];
585 } else if (tokens[0] === 'UNHANDLED_INPUT') {
586 tui.log_msg('? unknown command');
587 } else if (tokens[0] === 'PLAY_ERROR') {
588 tui.log_msg('? ' + tokens[1]);
589 terminal.blink_screen();
590 } else if (tokens[0] === 'ARGUMENT_ERROR') {
591 tui.log_msg('? syntax error: ' + tokens[1]);
592 } else if (tokens[0] === 'GAME_ERROR') {
593 tui.log_msg('? game error: ' + tokens[1]);
594 } else if (tokens[0] === 'PONG') {
597 tui.log_msg('? unhandled input: ' + event.data);
603 quote: function(str) {
605 for (let i = 0; i < str.length; i++) {
607 if (['"', '\\'].includes(c)) {
613 return quoted.join('');
615 to_yx: function(yx_coordinate) {
616 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
618 untokenize: function(tokens) {
619 let quoted_tokens = [];
620 for (let token of tokens) {
621 quoted_tokens.push(this.quote(token));
623 return quoted_tokens.join(" ");
628 constructor(name, has_input_prompt=false, shows_info=false,
629 is_intro=false, is_single_char_entry=false) {
631 this.short_desc = mode_helps[name].short;
632 this.available_modes = [];
633 this.available_actions = [];
634 this.has_input_prompt = has_input_prompt;
635 this.shows_info= shows_info;
636 this.is_intro = is_intro;
637 this.help_intro = mode_helps[name].long;
638 this.intro_msg = mode_helps[name].intro;
639 this.is_single_char_entry = is_single_char_entry;
642 *iter_available_modes() {
643 for (let mode_name of this.available_modes) {
644 let mode = tui['mode_' + mode_name];
648 let key = tui.keys['switch_to_' + mode.name];
652 list_available_modes() {
654 if (this.available_modes.length > 0) {
655 msg += 'Other modes available from here:\n';
656 for (let [mode, key] of this.iter_available_modes()) {
657 msg += '[' + key + '] – ' + mode.short_desc + '\n';
662 mode_switch_on_key(key_event) {
663 for (let [mode, key] of this.iter_available_modes()) {
664 if (key_event.key == key) {
665 event.preventDefault();
666 tui.switch_mode(mode.name);
678 window_width: terminal.cols / 2,
686 mode_waiting_for_server: new Mode('waiting_for_server',
688 mode_login: new Mode('login', true, false, true),
689 mode_post_login_wait: new Mode('post_login_wait'),
690 mode_chat: new Mode('chat', true),
691 mode_annotate: new Mode('annotate', true, true),
692 mode_play: new Mode('play'),
693 mode_study: new Mode('study', false, true),
694 mode_write: new Mode('write', false, false, false, true),
695 mode_edit: new Mode('edit'),
696 mode_control_pw_type: new Mode('control_pw_type', true),
697 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
698 mode_portal: new Mode('portal', true, true),
699 mode_password: new Mode('password', true),
700 mode_name_thing: new Mode('name_thing', true, true),
701 mode_command_thing: new Mode('command_thing', true),
702 mode_take_thing: new Mode('take_thing', true),
703 mode_drop_thing: new Mode('drop_thing', true),
704 mode_enter_face: new Mode('enter_face', true),
705 mode_enter_hat: new Mode('enter_hat', true),
706 mode_admin_enter: new Mode('admin_enter', true),
707 mode_admin: new Mode('admin'),
708 mode_control_pw_pw: new Mode('control_pw_pw', true),
709 mode_control_tile_type: new Mode('control_tile_type', true),
710 mode_control_tile_draw: new Mode('control_tile_draw'),
712 'flatten': 'FLATTEN_SURROUNDINGS',
713 'take_thing': 'PICK_UP',
714 'drop_thing': 'DROP',
717 'install': 'INSTALL',
719 'command': 'COMMAND',
720 'consume': 'INTOXICATE',
730 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
731 "command_thing", "take_thing", "drop_thing"]
732 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
734 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
735 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
736 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
737 "control_tile_type", "chat",
738 "study", "play", "edit"]
739 this.mode_admin.available_actions = ["move"];
740 this.mode_control_tile_draw.available_modes = ["admin_enter"]
741 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
742 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
743 "password", "chat", "study", "play",
744 "admin_enter", "enter_face", "enter_hat"]
745 this.mode_edit.available_actions = ["move", "flatten", "install",
747 this.inputEl = document.getElementById("input");
748 this.switch_mode('waiting_for_server');
749 this.recalc_input_lines();
750 this.height_header = this.height_turn_line + this.height_mode_line;
753 init_keys: function() {
754 document.getElementById("move_table").hidden = true;
756 for (let key_selector of key_selectors) {
757 this.keys[key_selector.id.slice(4)] = key_selector.value;
759 this.movement_keys = {};
760 let geometry_prefix = 'undefinedMapGeometry_';
761 if (game.map_geometry) {
762 geometry_prefix = game.map_geometry.toLowerCase() + '_';
764 for (const key_name of Object.keys(key_descriptions)) {
765 if (key_name.startsWith(geometry_prefix)) {
766 let direction = key_name.split('_')[2].toUpperCase();
767 let key = this.keys[key_name];
768 this.movement_keys[key] = direction;
771 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
772 if (move_button.id.startsWith('key_')) {
775 move_button.hidden = true;
777 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
778 document.getElementById("move_table").hidden = false;
779 move_button.hidden = false;
781 for (let el of document.getElementsByTagName("button")) {
782 let action_desc = key_descriptions[el.id];
783 let action_key = '[' + this.keys[el.id] + ']';
784 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
787 task_action_on: function(action) {
788 return game.tasks.includes(this.action_tasks[action]);
790 switch_mode: function(mode_name) {
792 function fail(msg, return_mode='play') {
793 tui.log_msg('? ' + msg);
794 terminal.blink_screen();
795 tui.switch_mode(return_mode);
798 if (this.mode && this.mode.name == 'control_tile_draw') {
799 tui.log_msg('@ finished tile protection drawing.')
801 this.draw_face = false;
802 this.tile_draw = false;
803 if (mode_name == 'command_thing' && (!game.player.carrying
804 || !game.player.carrying.commandable)) {
805 return fail('not carrying anything commandable');
806 } else if (mode_name == 'name_thing' && !game.player.carrying) {
807 return fail('not carrying anything to re-name');
808 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
809 return fail('not carrying anything to protect')
810 } else if (mode_name == 'take_thing' && game.player.carrying) {
811 return fail('already carrying something');
812 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
813 return fail('not carrying anything droppable');
814 } else if (mode_name == 'enter_hat' && !game.player.hat) {
815 return fail('not wearing hat to edit', 'edit');
817 if (mode_name == 'admin_enter' && this.is_admin) {
820 this.mode = this['mode_' + mode_name];
821 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
822 this.map_mode = 'protections';
823 } else if (this.mode.name != "edit") {
824 this.map_mode = 'terrain + things';
826 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
827 explorer.position = game.player.position;
829 this.inputEl.value = "";
830 this.restore_input_values();
831 for (let el of document.getElementsByTagName("button")) {
834 document.getElementById("help").disabled = false;
835 for (const action of this.mode.available_actions) {
836 if (["move", "move_explorer"].includes(action)) {
837 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
838 move_key.disabled = false;
840 } else if (Object.keys(this.action_tasks).includes(action)) {
841 if (this.task_action_on(action)) {
842 document.getElementById(action).disabled = false;
845 document.getElementById(action).disabled = false;
848 for (const mode_name of this.mode.available_modes) {
849 document.getElementById('switch_to_' + mode_name).disabled = false;
851 if (this.mode.intro_msg.length > 0) {
852 this.log_msg(this.mode.intro_msg);
854 if (this.mode.name == 'login') {
855 if (this.login_name) {
856 server.send(['LOGIN', this.login_name]);
858 this.log_msg("? need login name");
860 } else if (this.mode.is_single_char_entry) {
861 this.show_help = true;
862 } else if (this.mode.name == 'take_thing') {
863 this.log_msg("Portable things in reach for pick-up:");
864 const y = game.player.position[0]
865 const x = game.player.position[1]
866 let directed_moves = {
867 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
869 if (game.map_geometry == 'Square') {
870 directed_moves['UP'] = [-1, 0];
871 directed_moves['DOWN'] = [1, 0];
872 } else if (game.map_geometry == 'Hex') {
874 directed_moves['UPLEFT'] = [-1, 0];
875 directed_moves['UPRIGHT'] = [-1, 1];
876 directed_moves['DOWNLEFT'] = [1, 0];
877 directed_moves['DOWNRIGHT'] = [1, 1];
879 directed_moves['UPLEFT'] = [-1, -1];
880 directed_moves['UPRIGHT'] = [-1, 0];
881 directed_moves['DOWNLEFT'] = [1, -1];
882 directed_moves['DOWNRIGHT'] = [1, 0];
885 console.log(directed_moves);
886 let select_range = {};
887 for (const direction in directed_moves) {
888 const move = directed_moves[direction];
889 select_range[direction] = [y + move[0], x + move[1]];
891 this.selectables = [];
893 for (const direction in select_range) {
894 for (const t_id in game.things) {
895 const t = game.things[t_id];
896 const position = select_range[direction];
898 && t.position[0] == position[0]
899 && t.position[1] == position[1]) {
900 this.selectables.push(t_id);
901 directions.push(direction);
905 if (this.selectables.length == 0) {
906 this.log_msg('none');
907 terminal.blink_screen();
908 this.switch_mode('play');
911 for (let [i, t_id] of this.selectables.entries()) {
912 const t = game.things[t_id];
913 const direction = directions[i];
914 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
917 } else if (this.mode.name == 'drop_thing') {
918 this.log_msg('Direction to drop thing to:');
919 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
920 for (let [i, direction] of this.selectables.entries()) {
921 this.log_msg(i + ': ' + direction);
923 } else if (this.mode.name == 'enter_hat') {
924 this.log_msg('legal characters: ' + game.players_hat_chars);
925 } else if (this.mode.name == 'command_thing') {
926 server.send(['TASK:COMMAND', 'HELP']);
927 } else if (this.mode.name == 'control_pw_pw') {
928 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
929 } else if (this.mode.name == 'control_tile_draw') {
930 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 + '].')
934 offset_links: function(offset, links) {
935 for (let y in links) {
936 let real_y = offset[0] + parseInt(y);
937 if (!this.links[real_y]) {
938 this.links[real_y] = [];
940 for (let link of links[y]) {
941 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
942 this.links[real_y].push(offset_link);
946 restore_input_values: function() {
947 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
948 let info = explorer.annotations[explorer.position];
949 if (info != "(none)") {
950 this.inputEl.value = info;
952 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
953 let portal = game.portals[explorer.position]
954 this.inputEl.value = portal;
955 } else if (this.mode.name == 'password') {
956 this.inputEl.value = this.password;
957 } else if (this.mode.name == 'name_thing') {
958 if (game.player.carrying && game.player.carrying.name_) {
959 this.inputEl.value = game.player.carrying.name_;
961 } else if (this.mode.name == 'admin_thing_protect') {
962 if (game.player.carrying && game.player.carrying.protection) {
963 this.inputEl.value = game.player.carrying.protection;
965 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
966 const start = this.ascii_draw_stage * 6;
967 const end = (this.ascii_draw_stage + 1) * 6;
968 if (this.mode.name == 'enter_face') {
969 this.inputEl.value = game.player.face.slice(start, end);
970 } else if (this.mode.name == 'enter_hat') {
971 this.inputEl.value = game.player.hat.slice(start, end);
975 recalc_input_lines: function() {
976 if (this.mode.has_input_prompt) {
978 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
980 this.input_lines = [];
982 this.height_input = this.input_lines.length;
984 msg_into_lines_of_width: function(msg, width) {
985 function push_inner_link(y, end_x) {
986 if (!inner_links[y]) {
989 inner_links[y].push([url_start_x, end_x, url]);
993 const regexp = RegExp('https?://[^\\s]+', 'g');
995 while ((match = regexp.exec(msg)) !== null) {
996 const url = match[0];
997 const url_start = match.index;
998 const url_end = match.index + match[0].length;
999 link_data[url_start] = url;
1000 url_ends.push(url_end);
1002 let url_start_x = 0;
1004 let inner_links = {};
1005 let in_link = false;
1008 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1009 if (x >= width || msg[i] == "\n") {
1011 push_inner_link(y, chunk.length);
1013 if (url_ends[0] == i) {
1021 if (msg[i] == "\n") {
1026 if (msg[i] != "\n") {
1029 if (i in link_data) {
1033 } else if (url_ends[0] == i) {
1035 push_inner_link(y, x);
1041 push_inner_link(lines.length - 1, chunk.length);
1043 return [lines, inner_links];
1045 log_msg: function(msg) {
1047 while (this.log.length > 100) {
1050 this.full_refresh();
1052 pick_selectable: function(task_name) {
1053 const i = parseInt(this.inputEl.value);
1054 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1055 tui.log_msg('? invalid index, aborted');
1057 server.send(['TASK:' + task_name, tui.selectables[i]]);
1059 this.inputEl.value = "";
1060 this.switch_mode('play');
1062 enter_ascii_art: function(command) {
1063 if (this.inputEl.value.length != 6) {
1064 this.log_msg('? wrong input length, must be 6; try again');
1067 this.log_msg(' ' + this.inputEl.value);
1068 this.full_ascii_draw += this.inputEl.value;
1069 this.ascii_draw_stage += 1;
1070 if (this.ascii_draw_stage < 3) {
1071 this.restore_input_values();
1073 server.send([command, this.full_ascii_draw]);
1074 this.full_ascii_draw = '';
1075 this.ascii_draw_stage = 0;
1076 this.inputEl.value = '';
1077 this.switch_mode('edit');
1080 draw_map: function() {
1081 if (!game.turn_complete && this.map_lines.length == 0) {
1084 if (game.turn_complete) {
1085 let map_lines_split = [];
1087 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1088 if (j == game.map_size[1]) {
1089 map_lines_split.push(line);
1093 if (this.map_mode == 'protections') {
1094 line.push(game.map_control[i] + ' ');
1096 line.push(game.map[i] + ' ');
1099 map_lines_split.push(line);
1100 if (this.map_mode == 'terrain + annotations') {
1101 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1102 const yx = coordinate.split(',')
1103 map_lines_split[yx[0]][yx[1]] = 'A ';
1105 } else if (this.map_mode == 'terrain + things') {
1106 for (const p in game.portals) {
1107 let coordinate = p.split(',')
1108 let original = map_lines_split[coordinate[0]][coordinate[1]];
1109 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1111 let used_positions = [];
1112 function draw_thing(t, used_positions) {
1113 let symbol = game.thing_types[t.type_];
1114 let meta_char = ' ';
1116 meta_char = t.thing_char;
1118 if (used_positions.includes(t.position.toString())) {
1124 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1125 used_positions.push(t.position.toString());
1127 for (const thing_id in game.things) {
1128 let t = game.things[thing_id];
1129 if (t.type_ != 'Player') {
1130 draw_thing(t, used_positions);
1133 for (const thing_id in game.things) {
1134 let t = game.things[thing_id];
1135 if (t.type_ == 'Player') {
1136 draw_thing(t, used_positions);
1140 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1141 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1142 } else if (tui.map_mode != 'terrain + things') {
1143 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1146 if (game.map_geometry == 'Square') {
1147 for (let line_split of map_lines_split) {
1148 this.map_lines.push(line_split.join(''));
1150 } else if (game.map_geometry == 'Hex') {
1152 for (let line_split of map_lines_split) {
1153 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1161 let window_center = [terminal.rows / 2, this.window_width / 2];
1162 let center_position = [game.player.position[0], game.player.position[1]];
1163 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1164 center_position = [explorer.position[0], explorer.position[1]];
1166 center_position[1] = center_position[1] * 2;
1167 this.offset = [center_position[0] - window_center[0],
1168 center_position[1] - window_center[1]]
1169 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1170 this.offset[1] += 1;
1173 let term_y = Math.max(0, -this.offset[0]);
1174 let term_x = Math.max(0, -this.offset[1]);
1175 let map_y = Math.max(0, this.offset[0]);
1176 let map_x = Math.max(0, this.offset[1]);
1177 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1178 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1179 terminal.write(term_y, term_x, to_draw);
1182 draw_face_popup: function() {
1183 const t = game.things[this.draw_face];
1184 if (!t || !t.face) {
1185 this.draw_face = false;
1188 const start_x = tui.window_width - 10;
1191 t_char = t.thing_char;
1193 function draw_body_part(body_part, end_y) {
1194 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1195 terminal.write(end_y - 3, start_x, '| |');
1196 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1197 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1198 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1201 draw_body_part(t.face, terminal.rows - 2);
1204 draw_body_part(t.hat, terminal.rows - 5);
1206 terminal.write(terminal.rows - 1, start_x, '| |');
1208 draw_mode_line: function() {
1209 let help = 'hit [' + this.keys.help + '] for help';
1210 if (this.mode.has_input_prompt) {
1211 help = 'enter /help for help';
1213 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1215 draw_stats_line: function(n) {
1216 terminal.write(1, this.window_width, 'BLADDER: ' + game.bladder_pressure);
1218 draw_history: function() {
1219 let log_display_lines = [];
1221 let y_offset_in_log = 0;
1222 for (let line of this.log) {
1223 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1225 log_display_lines = log_display_lines.concat(new_lines);
1226 for (const y in link_data) {
1227 const rel_y = y_offset_in_log + parseInt(y);
1228 log_links[rel_y] = [];
1229 for (let link of link_data[y]) {
1230 log_links[rel_y].push(link);
1233 y_offset_in_log += new_lines.length;
1235 let i = log_display_lines.length - 1;
1236 for (let y = terminal.rows - 1 - this.height_input;
1237 y >= this.height_header && i >= 0;
1239 terminal.write(y, this.window_width, log_display_lines[i]);
1241 for (const key of Object.keys(log_links)) {
1242 if (parseInt(key) <= i) {
1243 delete log_links[key];
1246 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1248 this.offset_links(offset, log_links);
1250 draw_info: function() {
1251 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1252 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1253 let offset = [this.height_header, this.window_width];
1254 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1255 terminal.write(y, offset[1], lines[i]);
1257 this.offset_links(offset, link_data);
1259 draw_input: function() {
1260 if (this.mode.has_input_prompt) {
1261 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1262 terminal.write(y, this.window_width, this.input_lines[i]);
1266 draw_help: function() {
1267 let movement_keys_desc = '';
1268 if (!this.mode.is_intro) {
1269 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1271 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1272 if (this.mode.available_actions.length > 0) {
1273 content += "Available actions:\n";
1274 for (let action of this.mode.available_actions) {
1275 if (Object.keys(this.action_tasks).includes(action)) {
1276 if (!this.task_action_on(action)) {
1280 if (action == 'move_explorer') {
1283 if (action == 'move') {
1284 content += "[" + movement_keys_desc + "] – move\n"
1286 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1291 content += this.mode.list_available_modes();
1293 if (!this.mode.has_input_prompt) {
1294 start_x = this.window_width;
1295 this.draw_links = false;
1297 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1298 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1299 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1300 terminal.write(y, start_x, lines[i]);
1303 toggle_tile_draw: function() {
1304 if (tui.tile_draw) {
1305 tui.tile_draw = false;
1307 tui.tile_draw = true;
1310 toggle_map_mode: function() {
1311 if (tui.map_mode == 'terrain only') {
1312 tui.map_mode = 'terrain + annotations';
1313 } else if (tui.map_mode == 'terrain + annotations') {
1314 tui.map_mode = 'terrain + things';
1315 } else if (tui.map_mode == 'terrain + things') {
1316 tui.map_mode = 'protections';
1317 } else if (tui.map_mode == 'protections') {
1318 tui.map_mode = 'terrain only';
1321 full_refresh: function() {
1322 this.draw_links = true;
1324 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1325 this.recalc_input_lines();
1326 if (this.mode.is_intro) {
1327 this.draw_history();
1331 this.draw_stats_line();
1332 this.draw_mode_line();
1333 if (this.mode.shows_info) {
1336 this.draw_history();
1340 if (this.show_help) {
1343 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1344 this.draw_face_popup();
1346 if (!this.draw_links) {
1356 this.player_id = -1;
1359 this.things_new = {};
1364 this.map_control = "";
1365 this.map_control_new = "";
1366 this.map_size = [0,0];
1367 this.map_size_new = [0,0];
1369 this.portals_new = {};
1370 this.players_hat_chars = "";
1371 this.bladder_pressure = 0;
1372 this.bladder_pressure_new = 0;
1374 get_thing_temp: function(id_, create_if_not_found=false) {
1375 if (id_ in game.things_new) {
1376 return game.things_new[id_];
1377 } else if (create_if_not_found) {
1378 let t = new Thing([0,0]);
1379 game.things_new[id_] = t;
1383 get_thing: function(id_, create_if_not_found=false) {
1384 if (id_ in game.things) {
1385 return game.things[id_];
1388 move: function(start_position, direction) {
1389 let target = [start_position[0], start_position[1]];
1390 if (direction == 'LEFT') {
1392 } else if (direction == 'RIGHT') {
1394 } else if (game.map_geometry == 'Square') {
1395 if (direction == 'UP') {
1397 } else if (direction == 'DOWN') {
1400 } else if (game.map_geometry == 'Hex') {
1401 let start_indented = start_position[0] % 2;
1402 if (direction == 'UPLEFT') {
1404 if (!start_indented) {
1407 } else if (direction == 'UPRIGHT') {
1409 if (start_indented) {
1412 } else if (direction == 'DOWNLEFT') {
1414 if (!start_indented) {
1417 } else if (direction == 'DOWNRIGHT') {
1419 if (start_indented) {
1424 if (target[0] < 0 || target[1] < 0 ||
1425 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1430 teleport: function() {
1431 if (game.player.position in this.portals) {
1432 server.reconnect_to(this.portals[game.player.position]);
1434 terminal.blink_screen();
1435 tui.log_msg('? not standing on portal')
1443 server.init(websocket_location);
1448 annotations_new: {},
1450 move: function(direction) {
1451 let target = game.move(this.position, direction);
1453 this.position = target
1454 this.info_cached = false;
1455 if (tui.tile_draw) {
1456 this.send_tile_control_command();
1459 terminal.blink_screen();
1462 get_info: function() {
1463 if (this.info_cached) {
1464 return this.info_cached;
1466 let info_to_cache = '';
1467 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1468 if (game.fov[position_i] != '.') {
1469 info_to_cache += 'outside field of view';
1471 for (let t_id in game.things) {
1472 let t = game.things[t_id];
1473 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1474 info_to_cache += "THING: " + this.get_thing_info(t);
1475 let protection = t.protection;
1476 if (protection == '.') {
1477 protection = 'none';
1479 info_to_cache += " / protection: " + protection + "\n";
1481 info_to_cache += t.hat.slice(0, 6) + '\n';
1482 info_to_cache += t.hat.slice(6, 12) + '\n';
1483 info_to_cache += t.hat.slice(12, 18) + '\n';
1486 info_to_cache += t.face.slice(0, 6) + '\n';
1487 info_to_cache += t.face.slice(6, 12) + '\n';
1488 info_to_cache += t.face.slice(12, 18) + '\n';
1492 let terrain_char = game.map[position_i]
1493 let terrain_desc = '?'
1494 if (game.terrains[terrain_char]) {
1495 terrain_desc = game.terrains[terrain_char];
1497 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1498 let protection = game.map_control[position_i];
1499 if (protection == '.') {
1500 protection = 'unprotected';
1502 info_to_cache += 'PROTECTION: ' + protection + '\n';
1503 if (this.position in game.portals) {
1504 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1506 if (this.position in this.annotations) {
1507 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1510 this.info_cached = info_to_cache;
1511 return this.info_cached;
1513 get_thing_info: function(t) {
1514 const symbol = game.thing_types[t.type_];
1515 let info = t.type_ + " / " + symbol;
1517 info += t.thing_char;
1520 info += " (" + t.name_ + ")";
1523 info += " / installed";
1527 annotate: function(msg) {
1528 if (msg.length == 0) {
1529 msg = " "; // triggers annotation deletion
1531 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1533 set_portal: function(msg) {
1534 if (msg.length == 0) {
1535 msg = " "; // triggers portal deletion
1537 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1539 send_tile_control_command: function() {
1540 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1544 tui.inputEl.addEventListener('input', (event) => {
1545 if (tui.mode.has_input_prompt) {
1546 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1547 if (tui.inputEl.value.length > max_length) {
1548 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1550 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1551 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1552 tui.switch_mode('edit');
1556 document.onclick = function() {
1557 if (!tui.mode.is_single_char_entry) {
1558 tui.show_help = false;
1561 tui.inputEl.addEventListener('keydown', (event) => {
1562 tui.show_help = false;
1563 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1564 event.preventDefault();
1566 if ((!tui.mode.is_intro && event.key == 'Escape')
1567 || (tui.mode.has_input_prompt && event.key == 'Enter'
1568 && tui.inputEl.value.length == 0
1569 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1570 'admin_enter'].includes(tui.mode.name))) {
1571 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1572 tui.log_msg('@ aborted');
1574 tui.switch_mode('play');
1575 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1576 tui.show_help = true;
1577 tui.inputEl.value = "";
1578 tui.restore_input_values();
1579 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1580 && !tui.mode.is_single_char_entry) {
1581 tui.show_help = true;
1582 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1583 tui.login_name = tui.inputEl.value;
1584 server.send(['LOGIN', tui.inputEl.value]);
1585 tui.inputEl.value = "";
1586 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1587 tui.enter_ascii_art('PLAYER_FACE');
1588 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1589 tui.enter_ascii_art('PLAYER_HAT');
1590 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1591 server.send(['TASK:COMMAND', tui.inputEl.value]);
1592 tui.inputEl.value = "";
1593 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1594 tui.pick_selectable('PICK_UP');
1595 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1596 tui.pick_selectable('DROP');
1597 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1598 if (tui.inputEl.value.length == 0) {
1599 tui.log_msg('@ aborted');
1601 server.send(['SET_MAP_CONTROL_PASSWORD',
1602 tui.tile_control_char, tui.inputEl.value]);
1603 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1605 tui.switch_mode('admin');
1606 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1607 explorer.set_portal(tui.inputEl.value);
1608 tui.switch_mode('edit');
1609 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1610 if (tui.inputEl.value.length == 0) {
1611 tui.inputEl.value = " ";
1613 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1614 tui.switch_mode('edit');
1615 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1616 explorer.annotate(tui.inputEl.value);
1617 tui.switch_mode('edit');
1618 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1619 if (tui.inputEl.value.length == 0) {
1620 tui.inputEl.value = " ";
1622 tui.password = tui.inputEl.value
1623 tui.switch_mode('edit');
1624 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1625 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1626 tui.switch_mode('play');
1627 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1628 if (tui.inputEl.value.length != 1) {
1629 tui.log_msg('@ entered non-single-char, therefore aborted');
1630 tui.switch_mode('admin');
1632 tui.tile_control_char = tui.inputEl.value[0];
1633 tui.switch_mode('control_pw_pw');
1635 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1636 if (tui.inputEl.value.length != 1) {
1637 tui.log_msg('@ entered non-single-char, therefore aborted');
1638 tui.switch_mode('admin');
1640 tui.tile_control_char = tui.inputEl.value[0];
1641 tui.switch_mode('control_tile_draw');
1643 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1644 if (tui.inputEl.value.length != 1) {
1645 tui.log_msg('@ entered non-single-char, therefore aborted');
1647 server.send(['THING_PROTECTION', tui.inputEl.value])
1648 tui.log_msg('@ sent new protection character for thing');
1650 tui.switch_mode('admin');
1651 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1652 let tokens = parser.tokenize(tui.inputEl.value);
1653 if (tokens.length > 0 && tokens[0].length > 0) {
1654 if (tui.inputEl.value[0][0] == '/') {
1655 if (tokens[0].slice(1) == 'nick') {
1656 if (tokens.length > 1) {
1657 server.send(['NICK', tokens[1]]);
1659 tui.log_msg('? need new name');
1662 tui.log_msg('? unknown command');
1665 server.send(['ALL', tui.inputEl.value]);
1667 } else if (tui.inputEl.valuelength > 0) {
1668 server.send(['ALL', tui.inputEl.value]);
1670 tui.inputEl.value = "";
1671 } else if (tui.mode.name == 'play') {
1672 if (tui.mode.mode_switch_on_key(event)) {
1674 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1675 server.send(["TASK:INTOXICATE"]);
1676 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1677 server.send(["TASK:DOOR"]);
1678 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1679 server.send(["TASK:WEAR"]);
1680 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1681 server.send(["TASK:SPIN"]);
1682 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1683 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1684 } else if (event.key === tui.keys.teleport) {
1687 } else if (tui.mode.name == 'study') {
1688 if (tui.mode.mode_switch_on_key(event)) {
1690 } else if (event.key in tui.movement_keys) {
1691 explorer.move(tui.movement_keys[event.key]);
1692 } else if (event.key == tui.keys.toggle_map_mode) {
1693 tui.toggle_map_mode();
1695 } else if (tui.mode.name == 'control_tile_draw') {
1696 if (tui.mode.mode_switch_on_key(event)) {
1698 } else if (event.key in tui.movement_keys) {
1699 explorer.move(tui.movement_keys[event.key]);
1700 } else if (event.key === tui.keys.toggle_tile_draw) {
1701 tui.toggle_tile_draw();
1703 } else if (tui.mode.name == 'admin') {
1704 if (tui.mode.mode_switch_on_key(event)) {
1706 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1707 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1709 } else if (tui.mode.name == 'edit') {
1710 if (tui.mode.mode_switch_on_key(event)) {
1712 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1713 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1714 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1715 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1716 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1717 server.send(["TASK:INSTALL", tui.password]);
1718 } else if (event.key == tui.keys.toggle_map_mode) {
1719 tui.toggle_map_mode();
1725 rows_selector.addEventListener('input', function() {
1726 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1729 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1730 terminal.initialize();
1733 cols_selector.addEventListener('input', function() {
1734 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1737 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1738 terminal.initialize();
1739 tui.window_width = terminal.cols / 2,
1742 for (let key_selector of key_selectors) {
1743 key_selector.addEventListener('input', function() {
1744 window.localStorage.setItem(key_selector.id, key_selector.value);
1748 window.setInterval(function() {
1749 if (server.websocket.readyState == 1) {
1750 server.send(['PING']);
1751 } else if (server.websocket.readyState != 0) {
1752 server.reconnect_to(server.url);
1753 tui.log_msg('@ attempting reconnect …')
1756 window.setInterval(function() {
1757 if (document.activeElement.tagName.toLowerCase() != 'input') {
1758 tui.inputEl.focus();
1761 document.getElementById("help").onclick = function() {
1762 tui.show_help = true;
1765 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1766 const mode = switchEl.id.slice("switch_to_".length);
1767 switchEl.onclick = function() {
1768 tui.switch_mode(mode);
1772 document.getElementById("toggle_tile_draw").onclick = function() {
1773 tui.toggle_tile_draw();
1775 document.getElementById("toggle_map_mode").onclick = function() {
1776 tui.toggle_map_mode();
1779 document.getElementById("flatten").onclick = function() {
1780 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1782 document.getElementById("door").onclick = function() {
1783 server.send(['TASK:DOOR']);
1785 document.getElementById("consume").onclick = function() {
1786 server.send(['TASK:INTOXICATE']);
1788 document.getElementById("install").onclick = function() {
1789 server.send(['TASK:INSTALL', tui.password]);
1791 document.getElementById("wear").onclick = function() {
1792 server.send(['TASK:WEAR']);
1794 document.getElementById("spin").onclick = function() {
1795 server.send(['TASK:SPIN']);
1797 document.getElementById("teleport").onclick = function() {
1800 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1801 if (move_button.id.startsWith('key_')) { // not a move button
1804 let direction = move_button.id.split('_')[2].toUpperCase();
1807 if (tui.mode.available_actions.includes("move")) {
1808 server.send(['TASK:MOVE', direction]);
1809 } else if (tui.mode.available_actions.includes("move_explorer")) {
1810 explorer.move(direction);
1814 move_button.onmousedown = function() {
1816 move_repeat = window.setInterval(move, 100);
1818 move_button.onmouseup = function() {
1819 window.clearInterval(move_repeat);