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] === 'STATS') {
493 game.bladder_pressure_new = parseInt(tokens[1])
494 game.energy_new = parseInt(tokens[2])
495 } else if (tokens[0] === 'THING') {
496 let t = game.get_thing_temp(tokens[4], true);
497 t.position = parser.parse_yx(tokens[1]);
499 t.protection = tokens[3];
500 t.portable = parseInt(tokens[5]);
501 t.commandable = parseInt(tokens[6]);
502 } else if (tokens[0] === 'THING_NAME') {
503 let t = game.get_thing_temp(tokens[1]);
505 } else if (tokens[0] === 'THING_FACE') {
506 let t = game.get_thing_temp(tokens[1]);
508 } else if (tokens[0] === 'THING_HAT') {
509 let t = game.get_thing_temp(tokens[1]);
511 } else if (tokens[0] === 'THING_CHAR') {
512 let t = game.get_thing_temp(tokens[1]);
513 t.thing_char = tokens[2];
514 } else if (tokens[0] === 'TASKS') {
515 game.tasks = tokens[1].split(',');
516 tui.mode_write.legal = game.tasks.includes('WRITE');
517 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
518 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
519 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
520 } else if (tokens[0] === 'THING_TYPE') {
521 game.thing_types[tokens[1]] = tokens[2]
522 } else if (tokens[0] === 'THING_CARRYING') {
523 let t = game.get_thing_temp(tokens[1]);
524 t.carrying = game.get_thing_temp(tokens[2], false);
525 } else if (tokens[0] === 'THING_INSTALLED') {
526 let t = game.get_thing_temp(tokens[1]);
528 } else if (tokens[0] === 'TERRAIN') {
529 game.terrains[tokens[1]] = tokens[2]
530 } else if (tokens[0] === 'MAP') {
531 game.map_geometry_new = tokens[1];
532 game.map_size_new = parser.parse_yx(tokens[2]);
533 game.map_new = tokens[3]
534 } else if (tokens[0] === 'FOV') {
535 game.fov_new = tokens[1]
536 } else if (tokens[0] === 'MAP_CONTROL') {
537 game.map_control_new = tokens[1]
538 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
539 game.portals = game.portals_new;
540 game.map_geometry = game.map_geometry_new;
541 game.map_size = game.map_size_new;
542 game.map = game.map_new;
543 game.fov = game.fov_new;
545 game.map_control = game.map_control_new;
546 explorer.annotations = explorer.annotations_new;
547 explorer.info_cached = false;
548 game.things = game.things_new;
549 game.player = game.things[game.player_id];
550 game.players_hat_chars = game.players_hat_chars_new;
551 game.bladder_pressure = game.bladder_pressure_new
552 game.energy = game.energy_new
553 game.turn_complete = true;
554 if (tui.mode.name == 'post_login_wait') {
555 tui.switch_mode('play');
559 } else if (tokens[0] === 'CHAT') {
560 tui.log_msg('# ' + tokens[1], 1);
561 } else if (tokens[0] === 'CHATFACE') {
562 tui.draw_face = tokens[1];
564 } else if (tokens[0] === 'REPLY') {
565 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
566 } else if (tokens[0] === 'PLAYER_ID') {
567 game.player_id = parseInt(tokens[1]);
568 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
569 game.players_hat_chars_new = tokens[1];
570 } else if (tokens[0] === 'LOGIN_OK') {
571 this.send(['GET_GAMESTATE']);
572 tui.switch_mode('post_login_wait');
573 tui.log_msg('@ welcome!')
574 tui.log_msg('@ hint: see top of terminal for how to get help.')
575 tui.log_msg('@ hint: enter study mode to understand your environment.')
576 } else if (tokens[0] === 'DEFAULT_COLORS') {
577 terminal.set_default_colors();
578 } else if (tokens[0] === 'RANDOM_COLORS') {
579 terminal.set_random_colors();
580 } else if (tokens[0] === 'ADMIN_OK') {
582 tui.log_msg('@ you now have admin rights');
583 tui.switch_mode('admin');
584 } else if (tokens[0] === 'PORTAL') {
585 let position = parser.parse_yx(tokens[1]);
586 game.portals_new[position] = tokens[2];
587 } else if (tokens[0] === 'ANNOTATION') {
588 let position = parser.parse_yx(tokens[1]);
589 explorer.annotations_new[position] = tokens[2];
590 } else if (tokens[0] === 'UNHANDLED_INPUT') {
591 tui.log_msg('? unknown command');
592 } else if (tokens[0] === 'PLAY_ERROR') {
593 tui.log_msg('? ' + tokens[1]);
594 terminal.blink_screen();
595 } else if (tokens[0] === 'ARGUMENT_ERROR') {
596 tui.log_msg('? syntax error: ' + tokens[1]);
597 } else if (tokens[0] === 'GAME_ERROR') {
598 tui.log_msg('? game error: ' + tokens[1]);
599 } else if (tokens[0] === 'PONG') {
602 tui.log_msg('? unhandled input: ' + event.data);
608 quote: function(str) {
610 for (let i = 0; i < str.length; i++) {
612 if (['"', '\\'].includes(c)) {
618 return quoted.join('');
620 to_yx: function(yx_coordinate) {
621 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
623 untokenize: function(tokens) {
624 let quoted_tokens = [];
625 for (let token of tokens) {
626 quoted_tokens.push(this.quote(token));
628 return quoted_tokens.join(" ");
633 constructor(name, has_input_prompt=false, shows_info=false,
634 is_intro=false, is_single_char_entry=false) {
636 this.short_desc = mode_helps[name].short;
637 this.available_modes = [];
638 this.available_actions = [];
639 this.has_input_prompt = has_input_prompt;
640 this.shows_info= shows_info;
641 this.is_intro = is_intro;
642 this.help_intro = mode_helps[name].long;
643 this.intro_msg = mode_helps[name].intro;
644 this.is_single_char_entry = is_single_char_entry;
647 *iter_available_modes() {
648 for (let mode_name of this.available_modes) {
649 let mode = tui['mode_' + mode_name];
653 let key = tui.keys['switch_to_' + mode.name];
657 list_available_modes() {
659 if (this.available_modes.length > 0) {
660 msg += 'Other modes available from here:\n';
661 for (let [mode, key] of this.iter_available_modes()) {
662 msg += '[' + key + '] – ' + mode.short_desc + '\n';
667 mode_switch_on_key(key_event) {
668 for (let [mode, key] of this.iter_available_modes()) {
669 if (key_event.key == key) {
670 event.preventDefault();
671 tui.switch_mode(mode.name);
683 window_width: terminal.cols / 2,
691 mode_waiting_for_server: new Mode('waiting_for_server',
693 mode_login: new Mode('login', true, false, true),
694 mode_post_login_wait: new Mode('post_login_wait'),
695 mode_chat: new Mode('chat', true),
696 mode_annotate: new Mode('annotate', true, true),
697 mode_play: new Mode('play'),
698 mode_study: new Mode('study', false, true),
699 mode_write: new Mode('write', false, false, false, true),
700 mode_edit: new Mode('edit'),
701 mode_control_pw_type: new Mode('control_pw_type', true),
702 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
703 mode_portal: new Mode('portal', true, true),
704 mode_password: new Mode('password', true),
705 mode_name_thing: new Mode('name_thing', true, true),
706 mode_command_thing: new Mode('command_thing', true),
707 mode_take_thing: new Mode('take_thing', true),
708 mode_drop_thing: new Mode('drop_thing', true),
709 mode_enter_face: new Mode('enter_face', true),
710 mode_enter_hat: new Mode('enter_hat', true),
711 mode_admin_enter: new Mode('admin_enter', true),
712 mode_admin: new Mode('admin'),
713 mode_control_pw_pw: new Mode('control_pw_pw', true),
714 mode_control_tile_type: new Mode('control_tile_type', true),
715 mode_control_tile_draw: new Mode('control_tile_draw'),
717 'flatten': 'FLATTEN_SURROUNDINGS',
718 'take_thing': 'PICK_UP',
719 'drop_thing': 'DROP',
722 'install': 'INSTALL',
724 'command': 'COMMAND',
725 'consume': 'INTOXICATE',
735 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
736 "command_thing", "take_thing", "drop_thing"]
737 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
739 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
740 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
741 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
742 "control_tile_type", "chat",
743 "study", "play", "edit"]
744 this.mode_admin.available_actions = ["move"];
745 this.mode_control_tile_draw.available_modes = ["admin_enter"]
746 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
747 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
748 "password", "chat", "study", "play",
749 "admin_enter", "enter_face", "enter_hat"]
750 this.mode_edit.available_actions = ["move", "flatten", "install",
752 this.inputEl = document.getElementById("input");
753 this.switch_mode('waiting_for_server');
754 this.recalc_input_lines();
755 this.height_header = this.height_turn_line + this.height_mode_line;
758 init_keys: function() {
759 document.getElementById("move_table").hidden = true;
761 for (let key_selector of key_selectors) {
762 this.keys[key_selector.id.slice(4)] = key_selector.value;
764 this.movement_keys = {};
765 let geometry_prefix = 'undefinedMapGeometry_';
766 if (game.map_geometry) {
767 geometry_prefix = game.map_geometry.toLowerCase() + '_';
769 for (const key_name of Object.keys(key_descriptions)) {
770 if (key_name.startsWith(geometry_prefix)) {
771 let direction = key_name.split('_')[2].toUpperCase();
772 let key = this.keys[key_name];
773 this.movement_keys[key] = direction;
776 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
777 if (move_button.id.startsWith('key_')) {
780 move_button.hidden = true;
782 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
783 document.getElementById("move_table").hidden = false;
784 move_button.hidden = false;
786 for (let el of document.getElementsByTagName("button")) {
787 let action_desc = key_descriptions[el.id];
788 let action_key = '[' + this.keys[el.id] + ']';
789 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
792 task_action_on: function(action) {
793 return game.tasks.includes(this.action_tasks[action]);
795 switch_mode: function(mode_name) {
797 function fail(msg, return_mode='play') {
798 tui.log_msg('? ' + msg);
799 terminal.blink_screen();
800 tui.switch_mode(return_mode);
803 if (this.mode && this.mode.name == 'control_tile_draw') {
804 tui.log_msg('@ finished tile protection drawing.')
806 this.draw_face = false;
807 this.tile_draw = false;
808 if (mode_name == 'command_thing' && (!game.player.carrying
809 || !game.player.carrying.commandable)) {
810 return fail('not carrying anything commandable');
811 } else if (mode_name == 'name_thing' && !game.player.carrying) {
812 return fail('not carrying anything to re-name');
813 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
814 return fail('not carrying anything to protect')
815 } else if (mode_name == 'take_thing' && game.player.carrying) {
816 return fail('already carrying something');
817 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
818 return fail('not carrying anything droppable');
819 } else if (mode_name == 'enter_hat' && !game.player.hat) {
820 return fail('not wearing hat to edit', 'edit');
822 if (mode_name == 'admin_enter' && this.is_admin) {
825 this.mode = this['mode_' + mode_name];
826 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
827 this.map_mode = 'protections';
828 } else if (this.mode.name != "edit") {
829 this.map_mode = 'terrain + things';
831 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
832 explorer.position = game.player.position;
834 this.inputEl.value = "";
835 this.restore_input_values();
836 for (let el of document.getElementsByTagName("button")) {
839 document.getElementById("help").disabled = false;
840 for (const action of this.mode.available_actions) {
841 if (["move", "move_explorer"].includes(action)) {
842 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
843 move_key.disabled = false;
845 } else if (Object.keys(this.action_tasks).includes(action)) {
846 if (this.task_action_on(action)) {
847 document.getElementById(action).disabled = false;
850 document.getElementById(action).disabled = false;
853 for (const mode_name of this.mode.available_modes) {
854 document.getElementById('switch_to_' + mode_name).disabled = false;
856 if (this.mode.intro_msg.length > 0) {
857 this.log_msg(this.mode.intro_msg);
859 if (this.mode.name == 'login') {
860 if (this.login_name) {
861 server.send(['LOGIN', this.login_name]);
863 this.log_msg("? need login name");
865 } else if (this.mode.is_single_char_entry) {
866 this.show_help = true;
867 } else if (this.mode.name == 'take_thing') {
868 this.log_msg("Portable things in reach for pick-up:");
869 const y = game.player.position[0]
870 const x = game.player.position[1]
871 let directed_moves = {
872 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
874 if (game.map_geometry == 'Square') {
875 directed_moves['UP'] = [-1, 0];
876 directed_moves['DOWN'] = [1, 0];
877 } else if (game.map_geometry == 'Hex') {
879 directed_moves['UPLEFT'] = [-1, 0];
880 directed_moves['UPRIGHT'] = [-1, 1];
881 directed_moves['DOWNLEFT'] = [1, 0];
882 directed_moves['DOWNRIGHT'] = [1, 1];
884 directed_moves['UPLEFT'] = [-1, -1];
885 directed_moves['UPRIGHT'] = [-1, 0];
886 directed_moves['DOWNLEFT'] = [1, -1];
887 directed_moves['DOWNRIGHT'] = [1, 0];
890 console.log(directed_moves);
891 let select_range = {};
892 for (const direction in directed_moves) {
893 const move = directed_moves[direction];
894 select_range[direction] = [y + move[0], x + move[1]];
896 this.selectables = [];
898 for (const direction in select_range) {
899 for (const t_id in game.things) {
900 const t = game.things[t_id];
901 const position = select_range[direction];
903 && t.position[0] == position[0]
904 && t.position[1] == position[1]) {
905 this.selectables.push(t_id);
906 directions.push(direction);
910 if (this.selectables.length == 0) {
911 this.log_msg('none');
912 terminal.blink_screen();
913 this.switch_mode('play');
916 for (let [i, t_id] of this.selectables.entries()) {
917 const t = game.things[t_id];
918 const direction = directions[i];
919 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
922 } else if (this.mode.name == 'drop_thing') {
923 this.log_msg('Direction to drop thing to:');
924 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
925 for (let [i, direction] of this.selectables.entries()) {
926 this.log_msg(i + ': ' + direction);
928 } else if (this.mode.name == 'enter_hat') {
929 this.log_msg('legal characters: ' + game.players_hat_chars);
930 } else if (this.mode.name == 'command_thing') {
931 server.send(['TASK:COMMAND', 'HELP']);
932 } else if (this.mode.name == 'control_pw_pw') {
933 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
934 } else if (this.mode.name == 'control_tile_draw') {
935 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 + '].')
939 offset_links: function(offset, links) {
940 for (let y in links) {
941 let real_y = offset[0] + parseInt(y);
942 if (!this.links[real_y]) {
943 this.links[real_y] = [];
945 for (let link of links[y]) {
946 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
947 this.links[real_y].push(offset_link);
951 restore_input_values: function() {
952 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
953 let info = explorer.annotations[explorer.position];
954 if (info != "(none)") {
955 this.inputEl.value = info;
957 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
958 let portal = game.portals[explorer.position]
959 this.inputEl.value = portal;
960 } else if (this.mode.name == 'password') {
961 this.inputEl.value = this.password;
962 } else if (this.mode.name == 'name_thing') {
963 if (game.player.carrying && game.player.carrying.name_) {
964 this.inputEl.value = game.player.carrying.name_;
966 } else if (this.mode.name == 'admin_thing_protect') {
967 if (game.player.carrying && game.player.carrying.protection) {
968 this.inputEl.value = game.player.carrying.protection;
970 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
971 const start = this.ascii_draw_stage * 6;
972 const end = (this.ascii_draw_stage + 1) * 6;
973 if (this.mode.name == 'enter_face') {
974 this.inputEl.value = game.player.face.slice(start, end);
975 } else if (this.mode.name == 'enter_hat') {
976 this.inputEl.value = game.player.hat.slice(start, end);
980 recalc_input_lines: function() {
981 if (this.mode.has_input_prompt) {
983 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
985 this.input_lines = [];
987 this.height_input = this.input_lines.length;
989 msg_into_lines_of_width: function(msg, width) {
990 function push_inner_link(y, end_x) {
991 if (!inner_links[y]) {
994 inner_links[y].push([url_start_x, end_x, url]);
998 const regexp = RegExp('https?://[^\\s]+', 'g');
1000 while ((match = regexp.exec(msg)) !== null) {
1001 const url = match[0];
1002 const url_start = match.index;
1003 const url_end = match.index + match[0].length;
1004 link_data[url_start] = url;
1005 url_ends.push(url_end);
1007 let url_start_x = 0;
1009 let inner_links = {};
1010 let in_link = false;
1013 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1014 if (x >= width || msg[i] == "\n") {
1016 push_inner_link(y, chunk.length);
1018 if (url_ends[0] == i) {
1026 if (msg[i] == "\n") {
1031 if (msg[i] != "\n") {
1034 if (i in link_data) {
1038 } else if (url_ends[0] == i) {
1040 push_inner_link(y, x);
1046 push_inner_link(lines.length - 1, chunk.length);
1048 return [lines, inner_links];
1050 log_msg: function(msg) {
1052 while (this.log.length > 100) {
1055 this.full_refresh();
1057 pick_selectable: function(task_name) {
1058 const i = parseInt(this.inputEl.value);
1059 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1060 tui.log_msg('? invalid index, aborted');
1062 server.send(['TASK:' + task_name, tui.selectables[i]]);
1064 this.inputEl.value = "";
1065 this.switch_mode('play');
1067 enter_ascii_art: function(command) {
1068 if (this.inputEl.value.length != 6) {
1069 this.log_msg('? wrong input length, must be 6; try again');
1072 this.log_msg(' ' + this.inputEl.value);
1073 this.full_ascii_draw += this.inputEl.value;
1074 this.ascii_draw_stage += 1;
1075 if (this.ascii_draw_stage < 3) {
1076 this.restore_input_values();
1078 server.send([command, this.full_ascii_draw]);
1079 this.full_ascii_draw = '';
1080 this.ascii_draw_stage = 0;
1081 this.inputEl.value = '';
1082 this.switch_mode('edit');
1085 draw_map: function() {
1086 if (!game.turn_complete && this.map_lines.length == 0) {
1089 if (game.turn_complete) {
1090 let map_lines_split = [];
1092 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1093 if (j == game.map_size[1]) {
1094 map_lines_split.push(line);
1098 if (this.map_mode == 'protections') {
1099 line.push(game.map_control[i] + ' ');
1101 line.push(game.map[i] + ' ');
1104 map_lines_split.push(line);
1105 if (this.map_mode == 'terrain + annotations') {
1106 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1107 const yx = coordinate.split(',')
1108 map_lines_split[yx[0]][yx[1]] = 'A ';
1110 } else if (this.map_mode == 'terrain + things') {
1111 for (const p in game.portals) {
1112 let coordinate = p.split(',')
1113 let original = map_lines_split[coordinate[0]][coordinate[1]];
1114 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1116 let used_positions = [];
1117 function draw_thing(t, used_positions) {
1118 let symbol = game.thing_types[t.type_];
1119 let meta_char = ' ';
1121 meta_char = t.thing_char;
1123 if (used_positions.includes(t.position.toString())) {
1129 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1130 used_positions.push(t.position.toString());
1132 for (const thing_id in game.things) {
1133 let t = game.things[thing_id];
1134 if (t.type_ != 'Player') {
1135 draw_thing(t, used_positions);
1138 for (const thing_id in game.things) {
1139 let t = game.things[thing_id];
1140 if (t.type_ == 'Player') {
1141 draw_thing(t, used_positions);
1145 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1146 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1147 } else if (tui.map_mode != 'terrain + things') {
1148 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1151 if (game.map_geometry == 'Square') {
1152 for (let line_split of map_lines_split) {
1153 this.map_lines.push(line_split.join(''));
1155 } else if (game.map_geometry == 'Hex') {
1157 for (let line_split of map_lines_split) {
1158 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1166 let window_center = [terminal.rows / 2, this.window_width / 2];
1167 let center_position = [game.player.position[0], game.player.position[1]];
1168 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1169 center_position = [explorer.position[0], explorer.position[1]];
1171 center_position[1] = center_position[1] * 2;
1172 this.offset = [center_position[0] - window_center[0],
1173 center_position[1] - window_center[1]]
1174 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1175 this.offset[1] += 1;
1178 let term_y = Math.max(0, -this.offset[0]);
1179 let term_x = Math.max(0, -this.offset[1]);
1180 let map_y = Math.max(0, this.offset[0]);
1181 let map_x = Math.max(0, this.offset[1]);
1182 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1183 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1184 terminal.write(term_y, term_x, to_draw);
1187 draw_face_popup: function() {
1188 const t = game.things[this.draw_face];
1189 if (!t || !t.face) {
1190 this.draw_face = false;
1193 const start_x = tui.window_width - 10;
1196 t_char = t.thing_char;
1198 function draw_body_part(body_part, end_y) {
1199 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1200 terminal.write(end_y - 3, start_x, '| |');
1201 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1202 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1203 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1206 draw_body_part(t.face, terminal.rows - 2);
1209 draw_body_part(t.hat, terminal.rows - 5);
1211 terminal.write(terminal.rows - 1, start_x, '| |');
1213 draw_mode_line: function() {
1214 let help = 'hit [' + this.keys.help + '] for help';
1215 if (this.mode.has_input_prompt) {
1216 help = 'enter /help for help';
1218 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1220 draw_stats_line: function(n) {
1221 terminal.write(1, this.window_width,
1222 'ENERGY: ' + game.energy +
1223 ' BLADDER: ' + game.bladder_pressure);
1225 draw_history: function() {
1226 let log_display_lines = [];
1228 let y_offset_in_log = 0;
1229 for (let line of this.log) {
1230 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1232 log_display_lines = log_display_lines.concat(new_lines);
1233 for (const y in link_data) {
1234 const rel_y = y_offset_in_log + parseInt(y);
1235 log_links[rel_y] = [];
1236 for (let link of link_data[y]) {
1237 log_links[rel_y].push(link);
1240 y_offset_in_log += new_lines.length;
1242 let i = log_display_lines.length - 1;
1243 for (let y = terminal.rows - 1 - this.height_input;
1244 y >= this.height_header && i >= 0;
1246 terminal.write(y, this.window_width, log_display_lines[i]);
1248 for (const key of Object.keys(log_links)) {
1249 if (parseInt(key) <= i) {
1250 delete log_links[key];
1253 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1255 this.offset_links(offset, log_links);
1257 draw_info: function() {
1258 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1259 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1260 let offset = [this.height_header, this.window_width];
1261 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1262 terminal.write(y, offset[1], lines[i]);
1264 this.offset_links(offset, link_data);
1266 draw_input: function() {
1267 if (this.mode.has_input_prompt) {
1268 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1269 terminal.write(y, this.window_width, this.input_lines[i]);
1273 draw_help: function() {
1274 let movement_keys_desc = '';
1275 if (!this.mode.is_intro) {
1276 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1278 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1279 if (this.mode.available_actions.length > 0) {
1280 content += "Available actions:\n";
1281 for (let action of this.mode.available_actions) {
1282 if (Object.keys(this.action_tasks).includes(action)) {
1283 if (!this.task_action_on(action)) {
1287 if (action == 'move_explorer') {
1290 if (action == 'move') {
1291 content += "[" + movement_keys_desc + "] – move\n"
1293 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1298 content += this.mode.list_available_modes();
1300 if (!this.mode.has_input_prompt) {
1301 start_x = this.window_width;
1302 this.draw_links = false;
1304 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1305 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1306 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1307 terminal.write(y, start_x, lines[i]);
1310 toggle_tile_draw: function() {
1311 if (tui.tile_draw) {
1312 tui.tile_draw = false;
1314 tui.tile_draw = true;
1317 toggle_map_mode: function() {
1318 if (tui.map_mode == 'terrain only') {
1319 tui.map_mode = 'terrain + annotations';
1320 } else if (tui.map_mode == 'terrain + annotations') {
1321 tui.map_mode = 'terrain + things';
1322 } else if (tui.map_mode == 'terrain + things') {
1323 tui.map_mode = 'protections';
1324 } else if (tui.map_mode == 'protections') {
1325 tui.map_mode = 'terrain only';
1328 full_refresh: function() {
1329 this.draw_links = true;
1331 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1332 this.recalc_input_lines();
1333 if (this.mode.is_intro) {
1334 this.draw_history();
1338 this.draw_stats_line();
1339 this.draw_mode_line();
1340 if (this.mode.shows_info) {
1343 this.draw_history();
1347 if (this.show_help) {
1350 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1351 this.draw_face_popup();
1353 if (!this.draw_links) {
1363 this.player_id = -1;
1366 this.things_new = {};
1371 this.map_control = "";
1372 this.map_control_new = "";
1373 this.map_size = [0,0];
1374 this.map_size_new = [0,0];
1376 this.portals_new = {};
1377 this.players_hat_chars = "";
1378 this.bladder_pressure = 0;
1379 this.bladder_pressure_new = 0;
1381 get_thing_temp: function(id_, create_if_not_found=false) {
1382 if (id_ in game.things_new) {
1383 return game.things_new[id_];
1384 } else if (create_if_not_found) {
1385 let t = new Thing([0,0]);
1386 game.things_new[id_] = t;
1390 get_thing: function(id_, create_if_not_found=false) {
1391 if (id_ in game.things) {
1392 return game.things[id_];
1395 move: function(start_position, direction) {
1396 let target = [start_position[0], start_position[1]];
1397 if (direction == 'LEFT') {
1399 } else if (direction == 'RIGHT') {
1401 } else if (game.map_geometry == 'Square') {
1402 if (direction == 'UP') {
1404 } else if (direction == 'DOWN') {
1407 } else if (game.map_geometry == 'Hex') {
1408 let start_indented = start_position[0] % 2;
1409 if (direction == 'UPLEFT') {
1411 if (!start_indented) {
1414 } else if (direction == 'UPRIGHT') {
1416 if (start_indented) {
1419 } else if (direction == 'DOWNLEFT') {
1421 if (!start_indented) {
1424 } else if (direction == 'DOWNRIGHT') {
1426 if (start_indented) {
1431 if (target[0] < 0 || target[1] < 0 ||
1432 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1437 teleport: function() {
1438 if (game.player.position in this.portals) {
1439 server.reconnect_to(this.portals[game.player.position]);
1441 terminal.blink_screen();
1442 tui.log_msg('? not standing on portal')
1450 server.init(websocket_location);
1455 annotations_new: {},
1457 move: function(direction) {
1458 let target = game.move(this.position, direction);
1460 this.position = target
1461 this.info_cached = false;
1462 if (tui.tile_draw) {
1463 this.send_tile_control_command();
1466 terminal.blink_screen();
1469 get_info: function() {
1470 if (this.info_cached) {
1471 return this.info_cached;
1473 let info_to_cache = '';
1474 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1475 if (game.fov[position_i] != '.') {
1476 info_to_cache += 'outside field of view';
1478 for (let t_id in game.things) {
1479 let t = game.things[t_id];
1480 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1481 info_to_cache += "THING: " + this.get_thing_info(t);
1482 let protection = t.protection;
1483 if (protection == '.') {
1484 protection = 'none';
1486 info_to_cache += " / protection: " + protection + "\n";
1488 info_to_cache += t.hat.slice(0, 6) + '\n';
1489 info_to_cache += t.hat.slice(6, 12) + '\n';
1490 info_to_cache += t.hat.slice(12, 18) + '\n';
1493 info_to_cache += t.face.slice(0, 6) + '\n';
1494 info_to_cache += t.face.slice(6, 12) + '\n';
1495 info_to_cache += t.face.slice(12, 18) + '\n';
1499 let terrain_char = game.map[position_i]
1500 let terrain_desc = '?'
1501 if (game.terrains[terrain_char]) {
1502 terrain_desc = game.terrains[terrain_char];
1504 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1505 let protection = game.map_control[position_i];
1506 if (protection == '.') {
1507 protection = 'unprotected';
1509 info_to_cache += 'PROTECTION: ' + protection + '\n';
1510 if (this.position in game.portals) {
1511 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1513 if (this.position in this.annotations) {
1514 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1517 this.info_cached = info_to_cache;
1518 return this.info_cached;
1520 get_thing_info: function(t) {
1521 const symbol = game.thing_types[t.type_];
1522 let info = t.type_ + " / " + symbol;
1524 info += t.thing_char;
1527 info += " (" + t.name_ + ")";
1530 info += " / installed";
1534 annotate: function(msg) {
1535 if (msg.length == 0) {
1536 msg = " "; // triggers annotation deletion
1538 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1540 set_portal: function(msg) {
1541 if (msg.length == 0) {
1542 msg = " "; // triggers portal deletion
1544 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1546 send_tile_control_command: function() {
1547 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1551 tui.inputEl.addEventListener('input', (event) => {
1552 if (tui.mode.has_input_prompt) {
1553 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1554 if (tui.inputEl.value.length > max_length) {
1555 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1557 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1558 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1559 tui.switch_mode('edit');
1563 document.onclick = function() {
1564 if (!tui.mode.is_single_char_entry) {
1565 tui.show_help = false;
1568 tui.inputEl.addEventListener('keydown', (event) => {
1569 tui.show_help = false;
1570 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1571 event.preventDefault();
1573 if ((!tui.mode.is_intro && event.key == 'Escape')
1574 || (tui.mode.has_input_prompt && event.key == 'Enter'
1575 && tui.inputEl.value.length == 0
1576 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1577 'admin_enter'].includes(tui.mode.name))) {
1578 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1579 tui.log_msg('@ aborted');
1581 tui.switch_mode('play');
1582 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1583 tui.show_help = true;
1584 tui.inputEl.value = "";
1585 tui.restore_input_values();
1586 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1587 && !tui.mode.is_single_char_entry) {
1588 tui.show_help = true;
1589 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1590 tui.login_name = tui.inputEl.value;
1591 server.send(['LOGIN', tui.inputEl.value]);
1592 tui.inputEl.value = "";
1593 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1594 tui.enter_ascii_art('PLAYER_FACE');
1595 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1596 tui.enter_ascii_art('PLAYER_HAT');
1597 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1598 server.send(['TASK:COMMAND', tui.inputEl.value]);
1599 tui.inputEl.value = "";
1600 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1601 tui.pick_selectable('PICK_UP');
1602 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1603 tui.pick_selectable('DROP');
1604 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1605 if (tui.inputEl.value.length == 0) {
1606 tui.log_msg('@ aborted');
1608 server.send(['SET_MAP_CONTROL_PASSWORD',
1609 tui.tile_control_char, tui.inputEl.value]);
1610 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1612 tui.switch_mode('admin');
1613 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1614 explorer.set_portal(tui.inputEl.value);
1615 tui.switch_mode('edit');
1616 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1617 if (tui.inputEl.value.length == 0) {
1618 tui.inputEl.value = " ";
1620 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1621 tui.switch_mode('edit');
1622 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1623 explorer.annotate(tui.inputEl.value);
1624 tui.switch_mode('edit');
1625 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1626 if (tui.inputEl.value.length == 0) {
1627 tui.inputEl.value = " ";
1629 tui.password = tui.inputEl.value
1630 tui.switch_mode('edit');
1631 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1632 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1633 tui.switch_mode('play');
1634 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1635 if (tui.inputEl.value.length != 1) {
1636 tui.log_msg('@ entered non-single-char, therefore aborted');
1637 tui.switch_mode('admin');
1639 tui.tile_control_char = tui.inputEl.value[0];
1640 tui.switch_mode('control_pw_pw');
1642 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1643 if (tui.inputEl.value.length != 1) {
1644 tui.log_msg('@ entered non-single-char, therefore aborted');
1645 tui.switch_mode('admin');
1647 tui.tile_control_char = tui.inputEl.value[0];
1648 tui.switch_mode('control_tile_draw');
1650 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1651 if (tui.inputEl.value.length != 1) {
1652 tui.log_msg('@ entered non-single-char, therefore aborted');
1654 server.send(['THING_PROTECTION', tui.inputEl.value])
1655 tui.log_msg('@ sent new protection character for thing');
1657 tui.switch_mode('admin');
1658 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1659 let tokens = parser.tokenize(tui.inputEl.value);
1660 if (tokens.length > 0 && tokens[0].length > 0) {
1661 if (tui.inputEl.value[0][0] == '/') {
1662 if (tokens[0].slice(1) == 'nick') {
1663 if (tokens.length > 1) {
1664 server.send(['NICK', tokens[1]]);
1666 tui.log_msg('? need new name');
1669 tui.log_msg('? unknown command');
1672 server.send(['ALL', tui.inputEl.value]);
1674 } else if (tui.inputEl.valuelength > 0) {
1675 server.send(['ALL', tui.inputEl.value]);
1677 tui.inputEl.value = "";
1678 } else if (tui.mode.name == 'play') {
1679 if (tui.mode.mode_switch_on_key(event)) {
1681 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1682 server.send(["TASK:INTOXICATE"]);
1683 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1684 server.send(["TASK:DOOR"]);
1685 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1686 server.send(["TASK:WEAR"]);
1687 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1688 server.send(["TASK:SPIN"]);
1689 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1690 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1691 } else if (event.key === tui.keys.teleport) {
1694 } else if (tui.mode.name == 'study') {
1695 if (tui.mode.mode_switch_on_key(event)) {
1697 } else if (event.key in tui.movement_keys) {
1698 explorer.move(tui.movement_keys[event.key]);
1699 } else if (event.key == tui.keys.toggle_map_mode) {
1700 tui.toggle_map_mode();
1702 } else if (tui.mode.name == 'control_tile_draw') {
1703 if (tui.mode.mode_switch_on_key(event)) {
1705 } else if (event.key in tui.movement_keys) {
1706 explorer.move(tui.movement_keys[event.key]);
1707 } else if (event.key === tui.keys.toggle_tile_draw) {
1708 tui.toggle_tile_draw();
1710 } else if (tui.mode.name == 'admin') {
1711 if (tui.mode.mode_switch_on_key(event)) {
1713 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1714 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1716 } else if (tui.mode.name == 'edit') {
1717 if (tui.mode.mode_switch_on_key(event)) {
1719 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1720 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1721 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1722 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1723 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1724 server.send(["TASK:INSTALL", tui.password]);
1725 } else if (event.key == tui.keys.toggle_map_mode) {
1726 tui.toggle_map_mode();
1732 rows_selector.addEventListener('input', function() {
1733 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1736 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1737 terminal.initialize();
1740 cols_selector.addEventListener('input', function() {
1741 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1744 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1745 terminal.initialize();
1746 tui.window_width = terminal.cols / 2,
1749 for (let key_selector of key_selectors) {
1750 key_selector.addEventListener('input', function() {
1751 window.localStorage.setItem(key_selector.id, key_selector.value);
1755 window.setInterval(function() {
1756 if (server.websocket.readyState == 1) {
1757 server.send(['PING']);
1758 } else if (server.websocket.readyState != 0) {
1759 server.reconnect_to(server.url);
1760 tui.log_msg('@ attempting reconnect …')
1763 window.setInterval(function() {
1764 if (document.activeElement.tagName.toLowerCase() != 'input') {
1765 tui.inputEl.focus();
1768 document.getElementById("help").onclick = function() {
1769 tui.show_help = true;
1772 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1773 const mode = switchEl.id.slice("switch_to_".length);
1774 switchEl.onclick = function() {
1775 tui.switch_mode(mode);
1779 document.getElementById("toggle_tile_draw").onclick = function() {
1780 tui.toggle_tile_draw();
1782 document.getElementById("toggle_map_mode").onclick = function() {
1783 tui.toggle_map_mode();
1786 document.getElementById("flatten").onclick = function() {
1787 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1789 document.getElementById("door").onclick = function() {
1790 server.send(['TASK:DOOR']);
1792 document.getElementById("consume").onclick = function() {
1793 server.send(['TASK:INTOXICATE']);
1795 document.getElementById("install").onclick = function() {
1796 server.send(['TASK:INSTALL', tui.password]);
1798 document.getElementById("wear").onclick = function() {
1799 server.send(['TASK:WEAR']);
1801 document.getElementById("spin").onclick = function() {
1802 server.send(['TASK:SPIN']);
1804 document.getElementById("teleport").onclick = function() {
1807 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1808 if (move_button.id.startsWith('key_')) { // not a move button
1811 let direction = move_button.id.split('_')[2].toUpperCase();
1814 if (tui.mode.available_actions.includes("move")) {
1815 server.send(['TASK:MOVE', direction]);
1816 } else if (tui.mode.available_actions.includes("move_explorer")) {
1817 explorer.move(direction);
1821 move_button.onmousedown = function() {
1823 move_repeat = window.setInterval(move, 100);
1825 move_button.onmouseup = function() {
1826 window.clearInterval(move_repeat);