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.weariness_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.weariness = game.weariness_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 } else if (tokens[0] === 'DEFAULT_COLORS') {
574 terminal.set_default_colors();
575 } else if (tokens[0] === 'RANDOM_COLORS') {
576 terminal.set_random_colors();
577 } else if (tokens[0] === 'ADMIN_OK') {
579 tui.log_msg('@ you now have admin rights');
580 tui.switch_mode('admin');
581 } else if (tokens[0] === 'PORTAL') {
582 let position = parser.parse_yx(tokens[1]);
583 game.portals_new[position] = tokens[2];
584 } else if (tokens[0] === 'ANNOTATION') {
585 let position = parser.parse_yx(tokens[1]);
586 explorer.annotations_new[position] = tokens[2];
587 } else if (tokens[0] === 'UNHANDLED_INPUT') {
588 tui.log_msg('? unknown command');
589 } else if (tokens[0] === 'PLAY_ERROR') {
590 tui.log_msg('? ' + tokens[1]);
591 terminal.blink_screen();
592 } else if (tokens[0] === 'ARGUMENT_ERROR') {
593 tui.log_msg('? syntax error: ' + tokens[1]);
594 } else if (tokens[0] === 'GAME_ERROR') {
595 tui.log_msg('? game error: ' + tokens[1]);
596 } else if (tokens[0] === 'PONG') {
599 tui.log_msg('? unhandled input: ' + event.data);
605 quote: function(str) {
607 for (let i = 0; i < str.length; i++) {
609 if (['"', '\\'].includes(c)) {
615 return quoted.join('');
617 to_yx: function(yx_coordinate) {
618 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
620 untokenize: function(tokens) {
621 let quoted_tokens = [];
622 for (let token of tokens) {
623 quoted_tokens.push(this.quote(token));
625 return quoted_tokens.join(" ");
630 constructor(name, has_input_prompt=false, shows_info=false,
631 is_intro=false, is_single_char_entry=false) {
633 this.short_desc = mode_helps[name].short;
634 this.available_modes = [];
635 this.available_actions = [];
636 this.has_input_prompt = has_input_prompt;
637 this.shows_info= shows_info;
638 this.is_intro = is_intro;
639 this.help_intro = mode_helps[name].long;
640 this.intro_msg = mode_helps[name].intro;
641 this.is_single_char_entry = is_single_char_entry;
644 *iter_available_modes() {
645 for (let mode_name of this.available_modes) {
646 let mode = tui['mode_' + mode_name];
650 let key = tui.keys['switch_to_' + mode.name];
654 list_available_modes() {
656 if (this.available_modes.length > 0) {
657 msg += 'Other modes available from here:\n';
658 for (let [mode, key] of this.iter_available_modes()) {
659 msg += '[' + key + '] – ' + mode.short_desc + '\n';
664 mode_switch_on_key(key_event) {
665 for (let [mode, key] of this.iter_available_modes()) {
666 if (key_event.key == key) {
667 event.preventDefault();
668 tui.switch_mode(mode.name);
680 window_width: terminal.cols / 2,
688 mode_waiting_for_server: new Mode('waiting_for_server',
690 mode_login: new Mode('login', true, false, true),
691 mode_post_login_wait: new Mode('post_login_wait'),
692 mode_chat: new Mode('chat', true),
693 mode_annotate: new Mode('annotate', true, true),
694 mode_play: new Mode('play'),
695 mode_study: new Mode('study', false, true),
696 mode_write: new Mode('write', false, false, false, true),
697 mode_edit: new Mode('edit'),
698 mode_control_pw_type: new Mode('control_pw_type', true),
699 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
700 mode_portal: new Mode('portal', true, true),
701 mode_password: new Mode('password', true),
702 mode_name_thing: new Mode('name_thing', true, true),
703 mode_command_thing: new Mode('command_thing', true),
704 mode_take_thing: new Mode('take_thing', true),
705 mode_drop_thing: new Mode('drop_thing', true),
706 mode_enter_face: new Mode('enter_face', true),
707 mode_enter_hat: new Mode('enter_hat', true),
708 mode_admin_enter: new Mode('admin_enter', true),
709 mode_admin: new Mode('admin'),
710 mode_control_pw_pw: new Mode('control_pw_pw', true),
711 mode_control_tile_type: new Mode('control_tile_type', true),
712 mode_control_tile_draw: new Mode('control_tile_draw'),
714 'flatten': 'FLATTEN_SURROUNDINGS',
715 'take_thing': 'PICK_UP',
716 'drop_thing': 'DROP',
719 'install': 'INSTALL',
721 'command': 'COMMAND',
722 'consume': 'INTOXICATE',
732 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
733 "command_thing", "take_thing", "drop_thing"]
734 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
736 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
737 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
738 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
739 "control_tile_type", "chat",
740 "study", "play", "edit"]
741 this.mode_admin.available_actions = ["move"];
742 this.mode_control_tile_draw.available_modes = ["admin_enter"]
743 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
744 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
745 "password", "chat", "study", "play",
746 "admin_enter", "enter_face", "enter_hat"]
747 this.mode_edit.available_actions = ["move", "flatten", "install",
749 this.inputEl = document.getElementById("input");
750 this.switch_mode('waiting_for_server');
751 this.recalc_input_lines();
752 this.height_header = this.height_turn_line + this.height_mode_line;
755 init_keys: function() {
756 document.getElementById("move_table").hidden = true;
758 for (let key_selector of key_selectors) {
759 this.keys[key_selector.id.slice(4)] = key_selector.value;
761 this.movement_keys = {};
762 let geometry_prefix = 'undefinedMapGeometry_';
763 if (game.map_geometry) {
764 geometry_prefix = game.map_geometry.toLowerCase() + '_';
766 for (const key_name of Object.keys(key_descriptions)) {
767 if (key_name.startsWith(geometry_prefix)) {
768 let direction = key_name.split('_')[2].toUpperCase();
769 let key = this.keys[key_name];
770 this.movement_keys[key] = direction;
773 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
774 if (move_button.id.startsWith('key_')) {
777 move_button.hidden = true;
779 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
780 document.getElementById("move_table").hidden = false;
781 move_button.hidden = false;
783 for (let el of document.getElementsByTagName("button")) {
784 let action_desc = key_descriptions[el.id];
785 let action_key = '[' + this.keys[el.id] + ']';
786 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
789 task_action_on: function(action) {
790 return game.tasks.includes(this.action_tasks[action]);
792 switch_mode: function(mode_name) {
794 function fail(msg, return_mode='play') {
795 tui.log_msg('? ' + msg);
796 terminal.blink_screen();
797 tui.switch_mode(return_mode);
800 if (this.mode && this.mode.name == 'control_tile_draw') {
801 tui.log_msg('@ finished tile protection drawing.')
803 this.draw_face = false;
804 this.tile_draw = false;
805 if (mode_name == 'command_thing' && (!game.player.carrying
806 || !game.player.carrying.commandable)) {
807 return fail('not carrying anything commandable');
808 } else if (mode_name == 'name_thing' && !game.player.carrying) {
809 return fail('not carrying anything to re-name');
810 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
811 return fail('not carrying anything to protect')
812 } else if (mode_name == 'take_thing' && game.player.carrying) {
813 return fail('already carrying something');
814 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
815 return fail('not carrying anything droppable');
816 } else if (mode_name == 'enter_hat' && !game.player.hat) {
817 return fail('not wearing hat to edit', 'edit');
819 if (mode_name == 'admin_enter' && this.is_admin) {
822 this.mode = this['mode_' + mode_name];
823 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
824 this.map_mode = 'protections';
825 } else if (this.mode.name != "edit") {
826 this.map_mode = 'terrain + things';
828 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
829 explorer.position = game.player.position;
831 this.inputEl.value = "";
832 this.restore_input_values();
833 for (let el of document.getElementsByTagName("button")) {
836 document.getElementById("help").disabled = false;
837 for (const action of this.mode.available_actions) {
838 if (["move", "move_explorer"].includes(action)) {
839 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
840 move_key.disabled = false;
842 } else if (Object.keys(this.action_tasks).includes(action)) {
843 if (this.task_action_on(action)) {
844 document.getElementById(action).disabled = false;
847 document.getElementById(action).disabled = false;
850 for (const mode_name of this.mode.available_modes) {
851 document.getElementById('switch_to_' + mode_name).disabled = false;
853 if (this.mode.intro_msg.length > 0) {
854 this.log_msg(this.mode.intro_msg);
856 if (this.mode.name == 'login') {
857 if (this.login_name) {
858 server.send(['LOGIN', this.login_name]);
860 this.log_msg("? need login name");
862 } else if (this.mode.is_single_char_entry) {
863 this.show_help = true;
864 } else if (this.mode.name == 'take_thing') {
865 this.log_msg("Portable things in reach for pick-up:");
866 const y = game.player.position[0]
867 const x = game.player.position[1]
868 let directed_moves = {
869 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
871 if (game.map_geometry == 'Square') {
872 directed_moves['UP'] = [-1, 0];
873 directed_moves['DOWN'] = [1, 0];
874 } else if (game.map_geometry == 'Hex') {
876 directed_moves['UPLEFT'] = [-1, 0];
877 directed_moves['UPRIGHT'] = [-1, 1];
878 directed_moves['DOWNLEFT'] = [1, 0];
879 directed_moves['DOWNRIGHT'] = [1, 1];
881 directed_moves['UPLEFT'] = [-1, -1];
882 directed_moves['UPRIGHT'] = [-1, 0];
883 directed_moves['DOWNLEFT'] = [1, -1];
884 directed_moves['DOWNRIGHT'] = [1, 0];
887 console.log(directed_moves);
888 let select_range = {};
889 for (const direction in directed_moves) {
890 const move = directed_moves[direction];
891 select_range[direction] = [y + move[0], x + move[1]];
893 this.selectables = [];
895 for (const direction in select_range) {
896 for (const t_id in game.things) {
897 const t = game.things[t_id];
898 const position = select_range[direction];
900 && t.position[0] == position[0]
901 && t.position[1] == position[1]) {
902 this.selectables.push(t_id);
903 directions.push(direction);
907 if (this.selectables.length == 0) {
908 this.log_msg('none');
909 terminal.blink_screen();
910 this.switch_mode('play');
913 for (let [i, t_id] of this.selectables.entries()) {
914 const t = game.things[t_id];
915 const direction = directions[i];
916 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
919 } else if (this.mode.name == 'drop_thing') {
920 this.log_msg('Direction to drop thing to:');
921 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
922 for (let [i, direction] of this.selectables.entries()) {
923 this.log_msg(i + ': ' + direction);
925 } else if (this.mode.name == 'enter_hat') {
926 this.log_msg('legal characters: ' + game.players_hat_chars);
927 } else if (this.mode.name == 'command_thing') {
928 server.send(['TASK:COMMAND', 'HELP']);
929 } else if (this.mode.name == 'control_pw_pw') {
930 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
931 } else if (this.mode.name == 'control_tile_draw') {
932 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 + '].')
936 offset_links: function(offset, links) {
937 for (let y in links) {
938 let real_y = offset[0] + parseInt(y);
939 if (!this.links[real_y]) {
940 this.links[real_y] = [];
942 for (let link of links[y]) {
943 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
944 this.links[real_y].push(offset_link);
948 restore_input_values: function() {
949 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
950 let info = explorer.annotations[explorer.position];
951 if (info != "(none)") {
952 this.inputEl.value = info;
954 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
955 let portal = game.portals[explorer.position]
956 this.inputEl.value = portal;
957 } else if (this.mode.name == 'password') {
958 this.inputEl.value = this.password;
959 } else if (this.mode.name == 'name_thing') {
960 if (game.player.carrying && game.player.carrying.name_) {
961 this.inputEl.value = game.player.carrying.name_;
963 } else if (this.mode.name == 'admin_thing_protect') {
964 if (game.player.carrying && game.player.carrying.protection) {
965 this.inputEl.value = game.player.carrying.protection;
967 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
968 const start = this.ascii_draw_stage * 6;
969 const end = (this.ascii_draw_stage + 1) * 6;
970 if (this.mode.name == 'enter_face') {
971 this.inputEl.value = game.player.face.slice(start, end);
972 } else if (this.mode.name == 'enter_hat') {
973 this.inputEl.value = game.player.hat.slice(start, end);
977 recalc_input_lines: function() {
978 if (this.mode.has_input_prompt) {
980 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
982 this.input_lines = [];
984 this.height_input = this.input_lines.length;
986 msg_into_lines_of_width: function(msg, width) {
987 function push_inner_link(y, end_x) {
988 if (!inner_links[y]) {
991 inner_links[y].push([url_start_x, end_x, url]);
995 const regexp = RegExp('https?://[^\\s]+', 'g');
997 while ((match = regexp.exec(msg)) !== null) {
998 const url = match[0];
999 const url_start = match.index;
1000 const url_end = match.index + match[0].length;
1001 link_data[url_start] = url;
1002 url_ends.push(url_end);
1004 let url_start_x = 0;
1006 let inner_links = {};
1007 let in_link = false;
1010 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1011 if (x >= width || msg[i] == "\n") {
1013 push_inner_link(y, chunk.length);
1015 if (url_ends[0] == i) {
1023 if (msg[i] == "\n") {
1028 if (msg[i] != "\n") {
1031 if (i in link_data) {
1035 } else if (url_ends[0] == i) {
1037 push_inner_link(y, x);
1043 push_inner_link(lines.length - 1, chunk.length);
1045 return [lines, inner_links];
1047 log_msg: function(msg) {
1049 while (this.log.length > 100) {
1052 this.full_refresh();
1054 pick_selectable: function(task_name) {
1055 const i = parseInt(this.inputEl.value);
1056 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1057 tui.log_msg('? invalid index, aborted');
1059 server.send(['TASK:' + task_name, tui.selectables[i]]);
1061 this.inputEl.value = "";
1062 this.switch_mode('play');
1064 enter_ascii_art: function(command) {
1065 if (this.inputEl.value.length != 6) {
1066 this.log_msg('? wrong input length, must be 6; try again');
1069 this.log_msg(' ' + this.inputEl.value);
1070 this.full_ascii_draw += this.inputEl.value;
1071 this.ascii_draw_stage += 1;
1072 if (this.ascii_draw_stage < 3) {
1073 this.restore_input_values();
1075 server.send([command, this.full_ascii_draw]);
1076 this.full_ascii_draw = '';
1077 this.ascii_draw_stage = 0;
1078 this.inputEl.value = '';
1079 this.switch_mode('edit');
1082 draw_map: function() {
1083 if (!game.turn_complete && this.map_lines.length == 0) {
1086 if (game.turn_complete) {
1087 let map_lines_split = [];
1089 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1090 if (j == game.map_size[1]) {
1091 map_lines_split.push(line);
1095 if (this.map_mode == 'protections') {
1096 line.push(game.map_control[i] + ' ');
1098 line.push(game.map[i] + ' ');
1101 map_lines_split.push(line);
1102 if (this.map_mode == 'terrain + annotations') {
1103 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1104 const yx = coordinate.split(',')
1105 map_lines_split[yx[0]][yx[1]] = 'A ';
1107 } else if (this.map_mode == 'terrain + things') {
1108 for (const p in game.portals) {
1109 let coordinate = p.split(',')
1110 let original = map_lines_split[coordinate[0]][coordinate[1]];
1111 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1113 let used_positions = [];
1114 function draw_thing(t, used_positions) {
1115 let symbol = game.thing_types[t.type_];
1116 let meta_char = ' ';
1118 meta_char = t.thing_char;
1120 if (used_positions.includes(t.position.toString())) {
1126 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1127 used_positions.push(t.position.toString());
1129 for (const thing_id in game.things) {
1130 let t = game.things[thing_id];
1131 if (t.type_ != 'Player') {
1132 draw_thing(t, used_positions);
1135 for (const thing_id in game.things) {
1136 let t = game.things[thing_id];
1137 if (t.type_ == 'Player') {
1138 draw_thing(t, used_positions);
1142 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1143 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1144 } else if (tui.map_mode != 'terrain + things') {
1145 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1148 if (game.map_geometry == 'Square') {
1149 for (let line_split of map_lines_split) {
1150 this.map_lines.push(line_split.join(''));
1152 } else if (game.map_geometry == 'Hex') {
1154 for (let line_split of map_lines_split) {
1155 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1163 let window_center = [terminal.rows / 2, this.window_width / 2];
1164 let center_position = [game.player.position[0], game.player.position[1]];
1165 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1166 center_position = [explorer.position[0], explorer.position[1]];
1168 center_position[1] = center_position[1] * 2;
1169 this.offset = [center_position[0] - window_center[0],
1170 center_position[1] - window_center[1]]
1171 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1172 this.offset[1] += 1;
1175 let term_y = Math.max(0, -this.offset[0]);
1176 let term_x = Math.max(0, -this.offset[1]);
1177 let map_y = Math.max(0, this.offset[0]);
1178 let map_x = Math.max(0, this.offset[1]);
1179 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1180 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1181 terminal.write(term_y, term_x, to_draw);
1184 draw_face_popup: function() {
1185 const t = game.things[this.draw_face];
1186 if (!t || !t.face) {
1187 this.draw_face = false;
1190 const start_x = tui.window_width - 10;
1193 t_char = t.thing_char;
1195 function draw_body_part(body_part, end_y) {
1196 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1197 terminal.write(end_y - 3, start_x, '| |');
1198 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1199 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1200 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1203 draw_body_part(t.face, terminal.rows - 2);
1206 draw_body_part(t.hat, terminal.rows - 5);
1208 terminal.write(terminal.rows - 1, start_x, '| |');
1210 draw_mode_line: function() {
1211 let help = 'hit [' + this.keys.help + '] for help';
1212 if (this.mode.has_input_prompt) {
1213 help = 'enter /help for help';
1215 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1217 draw_stats_line: function(n) {
1218 terminal.write(1, this.window_width,
1219 'WEARINESS: ' + game.weariness +
1220 ' BLADDER: ' + game.bladder_pressure);
1222 draw_history: function() {
1223 let log_display_lines = [];
1225 let y_offset_in_log = 0;
1226 for (let line of this.log) {
1227 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1229 log_display_lines = log_display_lines.concat(new_lines);
1230 for (const y in link_data) {
1231 const rel_y = y_offset_in_log + parseInt(y);
1232 log_links[rel_y] = [];
1233 for (let link of link_data[y]) {
1234 log_links[rel_y].push(link);
1237 y_offset_in_log += new_lines.length;
1239 let i = log_display_lines.length - 1;
1240 for (let y = terminal.rows - 1 - this.height_input;
1241 y >= this.height_header && i >= 0;
1243 terminal.write(y, this.window_width, log_display_lines[i]);
1245 for (const key of Object.keys(log_links)) {
1246 if (parseInt(key) <= i) {
1247 delete log_links[key];
1250 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1252 this.offset_links(offset, log_links);
1254 draw_info: function() {
1255 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1256 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1257 let offset = [this.height_header, this.window_width];
1258 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1259 terminal.write(y, offset[1], lines[i]);
1261 this.offset_links(offset, link_data);
1263 draw_input: function() {
1264 if (this.mode.has_input_prompt) {
1265 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1266 terminal.write(y, this.window_width, this.input_lines[i]);
1270 draw_help: function() {
1271 let movement_keys_desc = '';
1272 if (!this.mode.is_intro) {
1273 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1275 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1276 if (this.mode.available_actions.length > 0) {
1277 content += "Available actions:\n";
1278 for (let action of this.mode.available_actions) {
1279 if (Object.keys(this.action_tasks).includes(action)) {
1280 if (!this.task_action_on(action)) {
1284 if (action == 'move_explorer') {
1287 if (action == 'move') {
1288 content += "[" + movement_keys_desc + "] – move\n"
1290 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1295 content += this.mode.list_available_modes();
1297 if (!this.mode.has_input_prompt) {
1298 start_x = this.window_width;
1299 this.draw_links = false;
1301 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1302 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1303 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1304 terminal.write(y, start_x, lines[i]);
1307 toggle_tile_draw: function() {
1308 if (tui.tile_draw) {
1309 tui.tile_draw = false;
1311 tui.tile_draw = true;
1314 toggle_map_mode: function() {
1315 if (tui.map_mode == 'terrain only') {
1316 tui.map_mode = 'terrain + annotations';
1317 } else if (tui.map_mode == 'terrain + annotations') {
1318 tui.map_mode = 'terrain + things';
1319 } else if (tui.map_mode == 'terrain + things') {
1320 tui.map_mode = 'protections';
1321 } else if (tui.map_mode == 'protections') {
1322 tui.map_mode = 'terrain only';
1325 full_refresh: function() {
1326 this.draw_links = true;
1328 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1329 this.recalc_input_lines();
1330 if (this.mode.is_intro) {
1331 this.draw_history();
1335 this.draw_stats_line();
1336 this.draw_mode_line();
1337 if (this.mode.shows_info) {
1340 this.draw_history();
1344 if (this.show_help) {
1347 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1348 this.draw_face_popup();
1350 if (!this.draw_links) {
1360 this.player_id = -1;
1363 this.things_new = {};
1368 this.map_control = "";
1369 this.map_control_new = "";
1370 this.map_size = [0,0];
1371 this.map_size_new = [0,0];
1373 this.portals_new = {};
1374 this.players_hat_chars = "";
1375 this.bladder_pressure = 0;
1376 this.bladder_pressure_new = 0;
1378 get_thing_temp: function(id_, create_if_not_found=false) {
1379 if (id_ in game.things_new) {
1380 return game.things_new[id_];
1381 } else if (create_if_not_found) {
1382 let t = new Thing([0,0]);
1383 game.things_new[id_] = t;
1387 get_thing: function(id_, create_if_not_found=false) {
1388 if (id_ in game.things) {
1389 return game.things[id_];
1392 move: function(start_position, direction) {
1393 let target = [start_position[0], start_position[1]];
1394 if (direction == 'LEFT') {
1396 } else if (direction == 'RIGHT') {
1398 } else if (game.map_geometry == 'Square') {
1399 if (direction == 'UP') {
1401 } else if (direction == 'DOWN') {
1404 } else if (game.map_geometry == 'Hex') {
1405 let start_indented = start_position[0] % 2;
1406 if (direction == 'UPLEFT') {
1408 if (!start_indented) {
1411 } else if (direction == 'UPRIGHT') {
1413 if (start_indented) {
1416 } else if (direction == 'DOWNLEFT') {
1418 if (!start_indented) {
1421 } else if (direction == 'DOWNRIGHT') {
1423 if (start_indented) {
1428 if (target[0] < 0 || target[1] < 0 ||
1429 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1434 teleport: function() {
1435 if (game.player.position in this.portals) {
1436 server.reconnect_to(this.portals[game.player.position]);
1438 terminal.blink_screen();
1439 tui.log_msg('? not standing on portal')
1447 server.init(websocket_location);
1452 annotations_new: {},
1454 move: function(direction) {
1455 let target = game.move(this.position, direction);
1457 this.position = target
1458 this.info_cached = false;
1459 if (tui.tile_draw) {
1460 this.send_tile_control_command();
1463 terminal.blink_screen();
1466 get_info: function() {
1467 if (this.info_cached) {
1468 return this.info_cached;
1470 let info_to_cache = '';
1471 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1472 if (game.fov[position_i] != '.') {
1473 info_to_cache += 'outside field of view';
1475 for (let t_id in game.things) {
1476 let t = game.things[t_id];
1477 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1478 info_to_cache += "THING: " + this.get_thing_info(t);
1479 let protection = t.protection;
1480 if (protection == '.') {
1481 protection = 'none';
1483 info_to_cache += " / protection: " + protection + "\n";
1485 info_to_cache += t.hat.slice(0, 6) + '\n';
1486 info_to_cache += t.hat.slice(6, 12) + '\n';
1487 info_to_cache += t.hat.slice(12, 18) + '\n';
1490 info_to_cache += t.face.slice(0, 6) + '\n';
1491 info_to_cache += t.face.slice(6, 12) + '\n';
1492 info_to_cache += t.face.slice(12, 18) + '\n';
1496 let terrain_char = game.map[position_i]
1497 let terrain_desc = '?'
1498 if (game.terrains[terrain_char]) {
1499 terrain_desc = game.terrains[terrain_char];
1501 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1502 let protection = game.map_control[position_i];
1503 if (protection == '.') {
1504 protection = 'unprotected';
1506 info_to_cache += 'PROTECTION: ' + protection + '\n';
1507 if (this.position in game.portals) {
1508 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1510 if (this.position in this.annotations) {
1511 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1514 this.info_cached = info_to_cache;
1515 return this.info_cached;
1517 get_thing_info: function(t) {
1518 const symbol = game.thing_types[t.type_];
1519 let info = t.type_ + " / " + symbol;
1521 info += t.thing_char;
1524 info += " (" + t.name_ + ")";
1527 info += " / installed";
1531 annotate: function(msg) {
1532 if (msg.length == 0) {
1533 msg = " "; // triggers annotation deletion
1535 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1537 set_portal: function(msg) {
1538 if (msg.length == 0) {
1539 msg = " "; // triggers portal deletion
1541 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1543 send_tile_control_command: function() {
1544 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1548 tui.inputEl.addEventListener('input', (event) => {
1549 if (tui.mode.has_input_prompt) {
1550 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1551 if (tui.inputEl.value.length > max_length) {
1552 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1554 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1555 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1556 tui.switch_mode('edit');
1560 document.onclick = function() {
1561 if (!tui.mode.is_single_char_entry) {
1562 tui.show_help = false;
1565 tui.inputEl.addEventListener('keydown', (event) => {
1566 tui.show_help = false;
1567 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1568 event.preventDefault();
1570 if ((!tui.mode.is_intro && event.key == 'Escape')
1571 || (tui.mode.has_input_prompt && event.key == 'Enter'
1572 && tui.inputEl.value.length == 0
1573 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1574 'admin_enter'].includes(tui.mode.name))) {
1575 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1576 tui.log_msg('@ aborted');
1578 tui.switch_mode('play');
1579 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1580 tui.show_help = true;
1581 tui.inputEl.value = "";
1582 tui.restore_input_values();
1583 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1584 && !tui.mode.is_single_char_entry) {
1585 tui.show_help = true;
1586 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1587 tui.login_name = tui.inputEl.value;
1588 server.send(['LOGIN', tui.inputEl.value]);
1589 tui.inputEl.value = "";
1590 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1591 tui.enter_ascii_art('PLAYER_FACE');
1592 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1593 tui.enter_ascii_art('PLAYER_HAT');
1594 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1595 server.send(['TASK:COMMAND', tui.inputEl.value]);
1596 tui.inputEl.value = "";
1597 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1598 tui.pick_selectable('PICK_UP');
1599 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1600 tui.pick_selectable('DROP');
1601 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1602 if (tui.inputEl.value.length == 0) {
1603 tui.log_msg('@ aborted');
1605 server.send(['SET_MAP_CONTROL_PASSWORD',
1606 tui.tile_control_char, tui.inputEl.value]);
1607 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1609 tui.switch_mode('admin');
1610 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1611 explorer.set_portal(tui.inputEl.value);
1612 tui.switch_mode('edit');
1613 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1614 if (tui.inputEl.value.length == 0) {
1615 tui.inputEl.value = " ";
1617 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1618 tui.switch_mode('edit');
1619 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1620 explorer.annotate(tui.inputEl.value);
1621 tui.switch_mode('edit');
1622 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1623 if (tui.inputEl.value.length == 0) {
1624 tui.inputEl.value = " ";
1626 tui.password = tui.inputEl.value
1627 tui.switch_mode('edit');
1628 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1629 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1630 tui.switch_mode('play');
1631 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1632 if (tui.inputEl.value.length != 1) {
1633 tui.log_msg('@ entered non-single-char, therefore aborted');
1634 tui.switch_mode('admin');
1636 tui.tile_control_char = tui.inputEl.value[0];
1637 tui.switch_mode('control_pw_pw');
1639 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1640 if (tui.inputEl.value.length != 1) {
1641 tui.log_msg('@ entered non-single-char, therefore aborted');
1642 tui.switch_mode('admin');
1644 tui.tile_control_char = tui.inputEl.value[0];
1645 tui.switch_mode('control_tile_draw');
1647 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1648 if (tui.inputEl.value.length != 1) {
1649 tui.log_msg('@ entered non-single-char, therefore aborted');
1651 server.send(['THING_PROTECTION', tui.inputEl.value])
1652 tui.log_msg('@ sent new protection character for thing');
1654 tui.switch_mode('admin');
1655 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1656 let tokens = parser.tokenize(tui.inputEl.value);
1657 if (tokens.length > 0 && tokens[0].length > 0) {
1658 if (tui.inputEl.value[0][0] == '/') {
1659 if (tokens[0].slice(1) == 'nick') {
1660 if (tokens.length > 1) {
1661 server.send(['NICK', tokens[1]]);
1663 tui.log_msg('? need new name');
1666 tui.log_msg('? unknown command');
1669 server.send(['ALL', tui.inputEl.value]);
1671 } else if (tui.inputEl.valuelength > 0) {
1672 server.send(['ALL', tui.inputEl.value]);
1674 tui.inputEl.value = "";
1675 } else if (tui.mode.name == 'play') {
1676 if (tui.mode.mode_switch_on_key(event)) {
1678 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1679 server.send(["TASK:INTOXICATE"]);
1680 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1681 server.send(["TASK:DOOR"]);
1682 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1683 server.send(["TASK:WEAR"]);
1684 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1685 server.send(["TASK:SPIN"]);
1686 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1687 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1688 } else if (event.key === tui.keys.teleport) {
1691 } else if (tui.mode.name == 'study') {
1692 if (tui.mode.mode_switch_on_key(event)) {
1694 } else if (event.key in tui.movement_keys) {
1695 explorer.move(tui.movement_keys[event.key]);
1696 } else if (event.key == tui.keys.toggle_map_mode) {
1697 tui.toggle_map_mode();
1699 } else if (tui.mode.name == 'control_tile_draw') {
1700 if (tui.mode.mode_switch_on_key(event)) {
1702 } else if (event.key in tui.movement_keys) {
1703 explorer.move(tui.movement_keys[event.key]);
1704 } else if (event.key === tui.keys.toggle_tile_draw) {
1705 tui.toggle_tile_draw();
1707 } else if (tui.mode.name == 'admin') {
1708 if (tui.mode.mode_switch_on_key(event)) {
1710 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1711 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1713 } else if (tui.mode.name == 'edit') {
1714 if (tui.mode.mode_switch_on_key(event)) {
1716 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1717 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1718 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1719 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1720 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1721 server.send(["TASK:INSTALL", tui.password]);
1722 } else if (event.key == tui.keys.toggle_map_mode) {
1723 tui.toggle_map_mode();
1729 rows_selector.addEventListener('input', function() {
1730 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1733 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1734 terminal.initialize();
1737 cols_selector.addEventListener('input', function() {
1738 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1741 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1742 terminal.initialize();
1743 tui.window_width = terminal.cols / 2,
1746 for (let key_selector of key_selectors) {
1747 key_selector.addEventListener('input', function() {
1748 window.localStorage.setItem(key_selector.id, key_selector.value);
1752 window.setInterval(function() {
1753 if (server.websocket.readyState == 1) {
1754 server.send(['PING']);
1755 } else if (server.websocket.readyState != 0) {
1756 server.reconnect_to(server.url);
1757 tui.log_msg('@ attempting reconnect …')
1760 window.setInterval(function() {
1761 if (document.activeElement.tagName.toLowerCase() != 'input') {
1762 tui.inputEl.focus();
1765 document.getElementById("help").onclick = function() {
1766 tui.show_help = true;
1769 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1770 const mode = switchEl.id.slice("switch_to_".length);
1771 switchEl.onclick = function() {
1772 tui.switch_mode(mode);
1776 document.getElementById("toggle_tile_draw").onclick = function() {
1777 tui.toggle_tile_draw();
1779 document.getElementById("toggle_map_mode").onclick = function() {
1780 tui.toggle_map_mode();
1783 document.getElementById("flatten").onclick = function() {
1784 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1786 document.getElementById("door").onclick = function() {
1787 server.send(['TASK:DOOR']);
1789 document.getElementById("consume").onclick = function() {
1790 server.send(['TASK:INTOXICATE']);
1792 document.getElementById("install").onclick = function() {
1793 server.send(['TASK:INSTALL', tui.password]);
1795 document.getElementById("wear").onclick = function() {
1796 server.send(['TASK:WEAR']);
1798 document.getElementById("spin").onclick = function() {
1799 server.send(['TASK:SPIN']);
1801 document.getElementById("teleport").onclick = function() {
1804 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1805 if (move_button.id.startsWith('key_')) { // not a move button
1808 let direction = move_button.id.split('_')[2].toUpperCase();
1811 if (tui.mode.available_actions.includes("move")) {
1812 server.send(['TASK:MOVE', direction]);
1813 } else if (tui.mode.available_actions.includes("move_explorer")) {
1814 explorer.move(direction);
1818 move_button.onmousedown = function() {
1820 move_repeat = window.setInterval(move, 100);
1822 move_button.onmouseup = function() {
1823 window.clearInterval(move_repeat);