13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <div style="position: relative; display: inline-block;">
18 <pre id="terminal"></pre>
19 <textarea id="input" style="position: absolute; left: 0; height: 100%; width: 100%; opacity: 0"></textarea>
21 <h3>button controls for hard-to-remember keybindings</h3>
22 <table id="move_table" style="float: left">
24 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
25 <td style="text-align: center"><button id="square_move_up"></button></td>
26 <td><button id="hex_move_upright"></button></td>
29 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
30 <td stlye="text-align: center;">move</td>
31 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
34 <td><button id="hex_move_downleft"></button></td>
35 <td style="text-align: center"><button id="square_move_down"></button></td>
36 <td><button id="hex_move_downright"></button></td>
41 <td><button id="help"></button></td>
44 <td><button id="switch_to_chat"></button><br /></td>
47 <td><button id="switch_to_study"></button></td>
48 <td><button id="toggle_map_mode"></button>
51 <td><button id="switch_to_play"></button></td>
53 <button id="switch_to_take_thing"></button>
54 <button id="switch_to_drop_thing"></button>
55 <button id="door"></button>
56 <button id="consume"></button>
57 <button id="switch_to_command_thing"></button>
58 <button id="teleport"></button>
59 <button id="wear"></button>
60 <button id="spin"></button>
64 <td><button id="switch_to_edit"></button></td>
66 <button id="switch_to_write"></button>
67 <button id="flatten"></button>
68 <button id="install"></button>
69 <button id="switch_to_annotate"></button>
70 <button id="switch_to_portal"></button>
71 <button id="switch_to_name_thing"></button>
72 <button id="switch_to_password"></button>
73 <button id="switch_to_enter_face"></button>
74 <button id="switch_to_enter_hat"></button>
78 <td><button id="switch_to_admin_enter"></button></td>
80 <button id="switch_to_control_pw_type"></button>
81 <button id="switch_to_control_tile_type"></button>
82 <button id="switch_to_admin_thing_protect"></button>
83 <button id="toggle_tile_draw"></button>
88 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
90 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
91 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
92 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
93 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
94 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
95 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
96 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
97 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
98 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
99 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
100 <li>help: <input id="key_help" type="text" value="h" />
101 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
102 <li>teleport: <input id="key_teleport" type="text" value="p" />
103 <li>spin: <input id="key_spin" type="text" value="S" />
104 <li>open/close: <input id="key_door" type="text" value="D" />
105 <li>consume: <input id="key_consume" type="text" value="C" />
106 <li>install: <input id="key_install" type="text" value="I" />
107 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
108 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
109 <li><input id="key_switch_to_enter_face" type="text" value="f" />
110 <li><input id="key_switch_to_enter_hat" type="text" value="H" />
111 <li><input id="key_switch_to_take_thing" type="text" value="z" />
112 <li><input id="key_switch_to_chat" type="text" value="t" />
113 <li><input id="key_switch_to_play" type="text" value="p" />
114 <li><input id="key_switch_to_study" type="text" value="?" />
115 <li><input id="key_switch_to_edit" type="text" value="E" />
116 <li><input id="key_switch_to_write" type="text" value="m" />
117 <li><input id="key_switch_to_name_thing" type="text" value="N" />
118 <li><input id="key_switch_to_command_thing" type="text" value="O" />
119 <li><input id="key_switch_to_password" type="text" value="P" />
120 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
121 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
122 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
123 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
124 <li><input id="key_switch_to_annotate" type="text" value="M" />
125 <li><input id="key_switch_to_portal" type="text" value="T" />
126 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
127 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
132 let websocket_location = "wss://plomlompom.com/rogue_chat/";
133 //let websocket_location = "ws://localhost:8000/";
139 'long': 'This mode allows you to interact with the map in various ways.'
144 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
146 'short': 'world edit',
148 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
151 'short': 'name thing',
153 'long': 'Give name to/change name of thing here.'
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 thing here.'
176 'short': 'edit face',
177 'intro': '@ enter face line (enter nothing to abort):',
178 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom. Eat cookies to extend the ASCII characters available for drawing.'
182 'intro': '@ enter hat line (enter nothing to abort):',
183 'long': 'Draw your hat as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
186 'short': 'edit tile',
188 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
191 'short': 'change protection character password',
192 'intro': '@ enter protection character for which you want to change the password:',
193 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
196 'short': 'change protection character password',
198 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
200 'control_tile_type': {
201 'short': 'change tiles protection',
202 'intro': '@ enter protection character which you want to draw:',
203 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
205 'control_tile_draw': {
206 'short': 'change tiles protection',
208 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
211 'short': 'annotate tile',
213 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
216 'short': 'edit portal',
218 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
223 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
228 'long': 'Enter your player name.'
230 'waiting_for_server': {
231 'short': 'waiting for server response',
232 'intro': '@ waiting for server …',
233 'long': 'Waiting for a server response.'
236 'short': 'waiting for server response',
238 'long': 'Waiting for a server response.'
241 'short': 'set world edit password',
243 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
246 'short': 'become admin',
247 'intro': '@ enter admin password:',
248 'long': 'This mode allows you to become admin if you know an admin password.'
253 'long': 'This mode allows you access to actions limited to administrators.'
256 let key_descriptions = {
258 'flatten': 'flatten surroundings',
259 'teleport': 'teleport',
260 'door': 'open/close',
261 'consume': 'consume',
262 'install': '(un-)install',
265 'toggle_map_mode': 'toggle map view',
266 'toggle_tile_draw': 'toggle protection character drawing',
267 'hex_move_upleft': 'up-left',
268 'hex_move_upright': 'up-right',
269 'hex_move_right': 'right',
270 'hex_move_left': 'left',
271 'hex_move_downleft': 'down-left',
272 'hex_move_downright': 'down-right',
273 'square_move_up': 'up',
274 'square_move_left': 'left',
275 'square_move_down': 'down',
276 'square_move_right': 'right',
278 for (const mode_name of Object.keys(mode_helps)) {
279 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
282 let rows_selector = document.getElementById("n_rows");
283 let cols_selector = document.getElementById("n_cols");
284 let key_selectors = document.querySelectorAll('[id^="key_"]');
286 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
287 const action = key_switch_selector.id.slice("key_switch_to_".length);
288 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
291 function restore_selector_value(selector) {
292 let stored_selection = window.localStorage.getItem(selector.id);
293 if (stored_selection) {
294 selector.value = stored_selection;
297 restore_selector_value(rows_selector);
298 restore_selector_value(cols_selector);
299 for (let key_selector of key_selectors) {
300 restore_selector_value(key_selector);
303 function escapeHTML(str) {
305 replace(/&/g, '&').
306 replace(/</g, '<').
307 replace(/>/g, '>').
308 replace(/'/g, ''').
309 replace(/"/g, '"');
313 initialize: function() {
314 this.rows = rows_selector.value;
315 this.cols = cols_selector.value;
316 this.pre_el = document.getElementById("terminal");
317 this.set_default_colors();
321 for (let y = 0, x = 0; y <= this.rows; x++) {
322 if (x == this.cols) {
325 this.content.push(line);
327 if (y == this.rows) {
334 apply_colors: function() {
335 this.pre_el.style.color = this.foreground;
336 this.pre_el.style.backgroundColor = this.background;
338 set_default_colors: function() {
339 this.foreground = 'white';
340 this.background = 'black';
343 set_random_colors: function() {
344 function rand(offset) {
345 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
347 this.foreground = '#' + rand(159) + rand(159) + rand(159);
348 this.background = '#' + rand(0) + rand(0) + rand(0);
351 blink_screen: function() {
352 this.pre_el.style.color = this.background;
353 this.pre_el.style.backgroundColor = this.foreground;
355 this.pre_el.style.color = this.foreground;
356 this.pre_el.style.backgroundColor = this.background;
359 refresh: function() {
360 let pre_content = '';
361 for (let y = 0; y < this.rows; y++) {
362 let line = this.content[y].join('');
364 if (y in tui.links) {
366 for (let span of tui.links[y]) {
367 chunks.push(escapeHTML(line.slice(start_x, span[0])));
368 chunks.push('<a target="_blank" href="');
369 chunks.push(escapeHTML(span[2]));
371 chunks.push(escapeHTML(line.slice(span[0], span[1])));
375 chunks.push(escapeHTML(line.slice(start_x)));
377 chunks = [escapeHTML(line)];
379 for (const chunk of chunks) {
380 pre_content += chunk;
384 this.pre_el.innerHTML = pre_content;
386 write: function(start_y, start_x, msg) {
387 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
388 this.content[start_y][x] = msg[i];
391 drawBox: function(start_y, start_x, height, width) {
392 let end_y = start_y + height;
393 let end_x = start_x + width;
394 for (let y = start_y, x = start_x; y < this.rows; x++) {
402 this.content[y][x] = ' ';
406 terminal.initialize();
409 tokenize: function(str) {
414 for (let i = 0; i < str.length; i++) {
420 } else if (c == '\\') {
422 } else if (c == '"') {
427 } else if (c == '"') {
429 } else if (c === ' ') {
430 if (token.length > 0) {
438 if (token.length > 0) {
443 parse_yx: function(position_string) {
444 let coordinate_strings = position_string.split(',')
445 let position = [0, 0];
446 position[0] = parseInt(coordinate_strings[0].slice(2));
447 position[1] = parseInt(coordinate_strings[1].slice(2));
459 init: function(url) {
461 this.websocket = new WebSocket(this.url);
462 this.websocket.onopen = function(event) {
463 game.thing_types = {};
465 server.send(['TASKS']);
466 server.send(['TERRAINS']);
467 server.send(['THING_TYPES']);
468 tui.log_msg("@ server connected! :)");
469 tui.switch_mode('login');
471 this.websocket.onclose = function(event) {
472 tui.switch_mode('waiting_for_server');
473 tui.log_msg("@ server disconnected :(");
475 this.websocket.onmessage = this.handle_event;
477 reconnect_to: function(url) {
478 this.websocket.close();
481 send: function(tokens) {
482 this.websocket.send(unparser.untokenize(tokens));
484 handle_event: function(event) {
485 let tokens = parser.tokenize(event.data);
486 if (tokens[0] === 'TURN') {
487 game.turn_complete = false;
488 game.turn = parseInt(tokens[1]);
489 } else if (tokens[0] === 'OTHER_WIPE') {
490 game.portals_new = {};
491 explorer.annotations_new = {};
492 game.things_new = [];
493 } else if (tokens[0] === 'THING') {
494 let t = game.get_thing_temp(tokens[4], true);
495 t.position = parser.parse_yx(tokens[1]);
497 t.protection = tokens[3];
498 t.portable = parseInt(tokens[5]);
499 t.commandable = parseInt(tokens[6]);
500 } else if (tokens[0] === 'THING_NAME') {
501 let t = game.get_thing_temp(tokens[1]);
503 } else if (tokens[0] === 'THING_FACE') {
504 let t = game.get_thing_temp(tokens[1]);
506 } else if (tokens[0] === 'THING_HAT') {
507 let t = game.get_thing_temp(tokens[1]);
509 } else if (tokens[0] === 'THING_CHAR') {
510 let t = game.get_thing_temp(tokens[1]);
511 t.thing_char = tokens[2];
512 } else if (tokens[0] === 'TASKS') {
513 game.tasks = tokens[1].split(',');
514 tui.mode_write.legal = game.tasks.includes('WRITE');
515 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
516 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
517 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
518 } else if (tokens[0] === 'THING_TYPE') {
519 game.thing_types[tokens[1]] = tokens[2]
520 } else if (tokens[0] === 'THING_CARRYING') {
521 let t = game.get_thing_temp(tokens[1]);
522 t.carrying = game.get_thing(tokens[2], false);
523 } else if (tokens[0] === 'THING_INSTALLED') {
524 let t = game.get_thing_temp(tokens[1]);
526 } else if (tokens[0] === 'TERRAIN') {
527 game.terrains[tokens[1]] = tokens[2]
528 } else if (tokens[0] === 'MAP') {
529 game.map_geometry_new = tokens[1];
530 game.map_size_new = parser.parse_yx(tokens[2]);
531 game.map_new = tokens[3]
532 } else if (tokens[0] === 'FOV') {
533 game.fov_new = tokens[1]
534 } else if (tokens[0] === 'MAP_CONTROL') {
535 game.map_control_new = tokens[1]
536 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
537 game.portals = game.portals_new;
538 game.map_geometry = game.map_geometry_new;
539 game.map_size = game.map_size_new;
540 game.map = game.map_new;
541 game.fov = game.fov_new;
543 game.map_control = game.map_control_new;
544 explorer.annotations = explorer.annotations_new;
545 explorer.info_cached = false;
546 game.things = game.things_new;
547 game.player = game.things[game.player_id];
548 game.players_hat_chars = game.players_hat_chars_new;
549 game.turn_complete = true;
550 if (tui.mode.name == 'post_login_wait') {
551 tui.switch_mode('play');
555 } else if (tokens[0] === 'CHAT') {
556 tui.log_msg('# ' + tokens[1], 1);
557 } else if (tokens[0] === 'CHATFACE') {
558 tui.draw_face = tokens[1];
560 } else if (tokens[0] === 'REPLY') {
561 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
562 } else if (tokens[0] === 'PLAYER_ID') {
563 game.player_id = parseInt(tokens[1]);
564 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
565 game.players_hat_chars_new = tokens[1];
566 } else if (tokens[0] === 'LOGIN_OK') {
567 this.send(['GET_GAMESTATE']);
568 tui.switch_mode('post_login_wait');
569 } else if (tokens[0] === 'DEFAULT_COLORS') {
570 terminal.set_default_colors();
571 } else if (tokens[0] === 'RANDOM_COLORS') {
572 terminal.set_random_colors();
573 } else if (tokens[0] === 'ADMIN_OK') {
575 tui.log_msg('@ you now have admin rights');
576 tui.switch_mode('admin');
577 } else if (tokens[0] === 'PORTAL') {
578 let position = parser.parse_yx(tokens[1]);
579 game.portals_new[position] = tokens[2];
580 } else if (tokens[0] === 'ANNOTATION') {
581 let position = parser.parse_yx(tokens[1]);
582 explorer.annotations_new[position] = tokens[2];
583 } else if (tokens[0] === 'UNHANDLED_INPUT') {
584 tui.log_msg('? unknown command');
585 } else if (tokens[0] === 'PLAY_ERROR') {
586 tui.log_msg('? ' + tokens[1]);
587 terminal.blink_screen();
588 } else if (tokens[0] === 'ARGUMENT_ERROR') {
589 tui.log_msg('? syntax error: ' + tokens[1]);
590 } else if (tokens[0] === 'GAME_ERROR') {
591 tui.log_msg('? game error: ' + tokens[1]);
592 } else if (tokens[0] === 'PONG') {
595 tui.log_msg('? unhandled input: ' + event.data);
601 quote: function(str) {
603 for (let i = 0; i < str.length; i++) {
605 if (['"', '\\'].includes(c)) {
611 return quoted.join('');
613 to_yx: function(yx_coordinate) {
614 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
616 untokenize: function(tokens) {
617 let quoted_tokens = [];
618 for (let token of tokens) {
619 quoted_tokens.push(this.quote(token));
621 return quoted_tokens.join(" ");
626 constructor(name, has_input_prompt=false, shows_info=false,
627 is_intro=false, is_single_char_entry=false) {
629 this.short_desc = mode_helps[name].short;
630 this.available_modes = [];
631 this.available_actions = [];
632 this.has_input_prompt = has_input_prompt;
633 this.shows_info= shows_info;
634 this.is_intro = is_intro;
635 this.help_intro = mode_helps[name].long;
636 this.intro_msg = mode_helps[name].intro;
637 this.is_single_char_entry = is_single_char_entry;
640 *iter_available_modes() {
641 for (let mode_name of this.available_modes) {
642 let mode = tui['mode_' + mode_name];
646 let key = tui.keys['switch_to_' + mode.name];
650 list_available_modes() {
652 if (this.available_modes.length > 0) {
653 msg += 'Other modes available from here:\n';
654 for (let [mode, key] of this.iter_available_modes()) {
655 msg += '[' + key + '] – ' + mode.short_desc + '\n';
660 mode_switch_on_key(key_event) {
661 for (let [mode, key] of this.iter_available_modes()) {
662 if (key_event.key == key) {
663 event.preventDefault();
664 tui.switch_mode(mode.name);
676 window_width: terminal.cols / 2,
684 mode_waiting_for_server: new Mode('waiting_for_server',
686 mode_login: new Mode('login', true, false, true),
687 mode_post_login_wait: new Mode('post_login_wait'),
688 mode_chat: new Mode('chat', true),
689 mode_annotate: new Mode('annotate', true, true),
690 mode_play: new Mode('play'),
691 mode_study: new Mode('study', false, true),
692 mode_write: new Mode('write', false, false, false, true),
693 mode_edit: new Mode('edit'),
694 mode_control_pw_type: new Mode('control_pw_type', true),
695 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
696 mode_portal: new Mode('portal', true, true),
697 mode_password: new Mode('password', true),
698 mode_name_thing: new Mode('name_thing', true, true),
699 mode_command_thing: new Mode('command_thing', true),
700 mode_take_thing: new Mode('take_thing', true),
701 mode_drop_thing: new Mode('drop_thing', true),
702 mode_enter_face: new Mode('enter_face', true),
703 mode_enter_hat: new Mode('enter_hat', true),
704 mode_admin_enter: new Mode('admin_enter', true),
705 mode_admin: new Mode('admin'),
706 mode_control_pw_pw: new Mode('control_pw_pw', true),
707 mode_control_tile_type: new Mode('control_tile_type', true),
708 mode_control_tile_draw: new Mode('control_tile_draw'),
710 'flatten': 'FLATTEN_SURROUNDINGS',
711 'take_thing': 'PICK_UP',
712 'drop_thing': 'DROP',
715 'install': 'INSTALL',
717 'command': 'COMMAND',
718 'consume': 'INTOXICATE',
728 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
729 "command_thing", "take_thing", "drop_thing"]
730 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
732 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
733 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
734 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
735 "control_tile_type", "chat",
736 "study", "play", "edit"]
737 this.mode_admin.available_actions = ["move"];
738 this.mode_control_tile_draw.available_modes = ["admin_enter"]
739 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
740 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
741 "password", "chat", "study", "play",
742 "admin_enter", "enter_face", "enter_hat"]
743 this.mode_edit.available_actions = ["move", "flatten", "install",
745 this.inputEl = document.getElementById("input");
746 this.switch_mode('waiting_for_server');
747 this.recalc_input_lines();
748 this.height_header = this.height_turn_line + this.height_mode_line;
751 init_keys: function() {
752 document.getElementById("move_table").hidden = true;
754 for (let key_selector of key_selectors) {
755 this.keys[key_selector.id.slice(4)] = key_selector.value;
757 this.movement_keys = {};
758 let geometry_prefix = 'undefinedMapGeometry_';
759 if (game.map_geometry) {
760 geometry_prefix = game.map_geometry.toLowerCase() + '_';
762 for (const key_name of Object.keys(key_descriptions)) {
763 if (key_name.startsWith(geometry_prefix)) {
764 let direction = key_name.split('_')[2].toUpperCase();
765 let key = this.keys[key_name];
766 this.movement_keys[key] = direction;
769 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
770 if (move_button.id.startsWith('key_')) {
773 move_button.hidden = true;
775 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
776 document.getElementById("move_table").hidden = false;
777 move_button.hidden = false;
779 for (let el of document.getElementsByTagName("button")) {
780 let action_desc = key_descriptions[el.id];
781 let action_key = '[' + this.keys[el.id] + ']';
782 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
785 task_action_on: function(action) {
786 return game.tasks.includes(this.action_tasks[action]);
788 switch_mode: function(mode_name) {
790 function fail(msg, return_mode) {
791 tui.log_msg('? ' + msg);
792 terminal.blink_screen();
793 tui.switch_mode(return_mode);
796 if (this.mode && this.mode.name == 'control_tile_draw') {
797 tui.log_msg('@ finished tile protection drawing.')
799 this.draw_face = false;
800 this.tile_draw = false;
801 if (mode_name == 'command_thing' && (!game.player.carrying
802 || !game.player.carrying.commandable)) {
803 return fail('not carrying anything commandable', 'play');
804 } else if (mode_name == 'take_thing' && game.player.carrying) {
805 return fail('already carrying something', 'play');
806 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
807 return fail('not carrying anything droppable', 'play');
808 } else if (mode_name == 'enter_hat' && !game.player.hat) {
809 return fail('not wearing hat to edit', 'edit');
811 if (mode_name == 'admin_enter' && this.is_admin) {
813 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
815 for (let t_id in game.things) {
816 if (t_id == game.player_id) {
819 let t = game.things[t_id];
820 if (game.player.position[0] == t.position[0]
821 && game.player.position[1] == t.position[1]) {
827 return fail('not standing over thing', 'fail');
829 this.selected_thing_id = thing_id;
832 this.mode = this['mode_' + mode_name];
833 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
834 this.map_mode = 'protections';
835 } else if (this.mode.name != "edit") {
836 this.map_mode = 'terrain + things';
838 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
839 explorer.position = game.player.position;
841 this.inputEl.value = "";
842 this.restore_input_values();
843 for (let el of document.getElementsByTagName("button")) {
846 document.getElementById("help").disabled = false;
847 for (const action of this.mode.available_actions) {
848 if (["move", "move_explorer"].includes(action)) {
849 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
850 move_key.disabled = false;
852 } else if (Object.keys(this.action_tasks).includes(action)) {
853 if (this.task_action_on(action)) {
854 document.getElementById(action).disabled = false;
857 document.getElementById(action).disabled = false;
860 for (const mode_name of this.mode.available_modes) {
861 document.getElementById('switch_to_' + mode_name).disabled = false;
863 if (this.mode.intro_msg.length > 0) {
864 this.log_msg(this.mode.intro_msg);
866 if (this.mode.name == 'login') {
867 if (this.login_name) {
868 server.send(['LOGIN', this.login_name]);
870 this.log_msg("? need login name");
872 } else if (this.mode.is_single_char_entry) {
873 this.show_help = true;
874 } else if (this.mode.name == 'take_thing') {
875 this.log_msg("Portable things in reach for pick-up:");
876 const y = game.player.position[0]
877 const x = game.player.position[1]
878 let select_range = [y.toString() + ':' + x.toString(),
879 (y + 0).toString() + ':' + (x - 1).toString(),
880 (y + 0).toString() + ':' + (x + 1).toString(),
881 (y - 1).toString() + ':' + (x).toString(),
882 (y + 1).toString() + ':' + (x).toString()];
883 if (game.map_geometry == 'Hex') {
885 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
886 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
888 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
889 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
892 this.selectables = [];
893 for (const t_id in game.things) {
894 const t = game.things[t_id];
895 if (select_range.includes(t.position[0].toString()
896 + ':' + t.position[1].toString())
898 this.selectables.push(t_id);
901 if (this.selectables.length == 0) {
902 this.log_msg('none');
903 terminal.blink_screen();
904 this.switch_mode('play');
907 for (let [i, t_id] of this.selectables.entries()) {
908 const t = game.things[t_id];
909 this.log_msg(i + ': ' + explorer.get_thing_info(t));
912 } else if (this.mode.name == 'drop_thing') {
913 this.log_msg('Direction to drop thing to:');
914 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
915 for (let [i, direction] of this.selectables.entries()) {
916 this.log_msg(i + ': ' + direction);
918 } else if (this.mode.name == 'enter_hat') {
919 this.log_msg('legal characters: ' + game.players_hat_chars);
920 } else if (this.mode.name == 'command_thing') {
921 server.send(['TASK:COMMAND', 'HELP']);
922 } else if (this.mode.name == 'control_pw_pw') {
923 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
924 } else if (this.mode.name == 'control_tile_draw') {
925 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 + '].')
929 offset_links: function(offset, links) {
930 for (let y in links) {
931 let real_y = offset[0] + parseInt(y);
932 if (!this.links[real_y]) {
933 this.links[real_y] = [];
935 for (let link of links[y]) {
936 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
937 this.links[real_y].push(offset_link);
941 restore_input_values: function() {
942 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
943 let info = explorer.annotations[explorer.position];
944 if (info != "(none)") {
945 this.inputEl.value = info;
947 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
948 let portal = game.portals[explorer.position]
949 this.inputEl.value = portal;
950 } else if (this.mode.name == 'password') {
951 this.inputEl.value = this.password;
952 } else if (this.mode.name == 'name_thing') {
953 let t = game.get_thing(this.selected_thing_id);
955 this.inputEl.value = t.name_;
957 } else if (this.mode.name == 'admin_thing_protect') {
958 let t = game.get_thing(this.selected_thing_id);
959 if (t && t.protection) {
960 this.inputEl.value = t.protection;
962 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
963 const start = this.ascii_draw_stage * 6;
964 const end = (this.ascii_draw_stage + 1) * 6;
965 if (this.mode.name == 'enter_face') {
966 this.inputEl.value = game.player.face.slice(start, end);
967 } else if (this.mode.name == 'enter_hat') {
968 this.inputEl.value = game.player.hat.slice(start, end);
972 recalc_input_lines: function() {
973 if (this.mode.has_input_prompt) {
975 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
977 this.input_lines = [];
979 this.height_input = this.input_lines.length;
981 msg_into_lines_of_width: function(msg, width) {
982 function push_inner_link(y, end_x) {
983 if (!inner_links[y]) {
986 inner_links[y].push([url_start_x, end_x, url]);
988 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
991 for (const match of matches) {
992 const url = match[0];
993 const url_start = match.index;
994 const url_end = match.index + match[0].length;
995 link_data[url_start] = url;
996 url_ends.push(url_end);
1000 let inner_links = {};
1001 let in_link = false;
1004 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1005 if (x >= width || msg[i] == "\n") {
1007 push_inner_link(y, chunk.length);
1009 if (url_ends[0] == i) {
1017 if (msg[i] == "\n") {
1022 if (msg[i] != "\n") {
1025 if (i in link_data) {
1029 } else if (url_ends[0] == i) {
1031 push_inner_link(y, x);
1037 push_inner_link(lines.length - 1, chunk.length);
1039 return [lines, inner_links];
1041 log_msg: function(msg) {
1043 while (this.log.length > 100) {
1046 this.full_refresh();
1048 pick_selectable: function(task_name) {
1049 const i = parseInt(this.inputEl.value);
1050 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1051 tui.log_msg('? invalid index, aborted');
1053 server.send(['TASK:' + task_name, tui.selectables[i]]);
1055 this.inputEl.value = "";
1056 this.switch_mode('play');
1058 enter_ascii_art: function(command) {
1059 if (this.inputEl.value.length != 6) {
1060 this.log_msg('? wrong input length, must be 6; try again');
1063 this.log_msg(' ' + this.inputEl.value);
1064 this.full_ascii_draw += this.inputEl.value;
1065 this.ascii_draw_stage += 1;
1066 if (this.ascii_draw_stage < 3) {
1067 this.restore_input_values();
1069 server.send([command, this.full_ascii_draw]);
1070 this.full_ascii_draw = '';
1071 this.ascii_draw_stage = 0;
1072 this.inputEl.value = '';
1073 this.switch_mode('edit');
1076 draw_map: function() {
1077 if (!game.turn_complete && this.map_lines.length == 0) {
1080 if (game.turn_complete) {
1081 let map_lines_split = [];
1083 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1084 if (j == game.map_size[1]) {
1085 map_lines_split.push(line);
1089 if (this.map_mode == 'protections') {
1090 line.push(game.map_control[i] + ' ');
1092 line.push(game.map[i] + ' ');
1095 map_lines_split.push(line);
1096 if (this.map_mode == 'terrain + annotations') {
1097 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1098 const yx = coordinate.split(',')
1099 map_lines_split[yx[0]][yx[1]] = 'A ';
1101 } else if (this.map_mode == 'terrain + things') {
1102 for (const p in game.portals) {
1103 let coordinate = p.split(',')
1104 let original = map_lines_split[coordinate[0]][coordinate[1]];
1105 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1107 let used_positions = [];
1108 function draw_thing(t, used_positions) {
1109 let symbol = game.thing_types[t.type_];
1110 let meta_char = ' ';
1112 meta_char = t.thing_char;
1114 if (used_positions.includes(t.position.toString())) {
1120 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1121 used_positions.push(t.position.toString());
1123 for (const thing_id in game.things) {
1124 let t = game.things[thing_id];
1125 if (t.type_ != 'Player') {
1126 draw_thing(t, used_positions);
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);
1136 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1137 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1138 } else if (tui.map_mode != 'terrain + things') {
1139 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1142 if (game.map_geometry == 'Square') {
1143 for (let line_split of map_lines_split) {
1144 this.map_lines.push(line_split.join(''));
1146 } else if (game.map_geometry == 'Hex') {
1148 for (let line_split of map_lines_split) {
1149 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1157 let window_center = [terminal.rows / 2, this.window_width / 2];
1158 let center_position = [game.player.position[0], game.player.position[1]];
1159 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1160 center_position = [explorer.position[0], explorer.position[1]];
1162 center_position[1] = center_position[1] * 2;
1163 this.offset = [center_position[0] - window_center[0],
1164 center_position[1] - window_center[1]]
1165 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1166 this.offset[1] += 1;
1169 let term_y = Math.max(0, -this.offset[0]);
1170 let term_x = Math.max(0, -this.offset[1]);
1171 let map_y = Math.max(0, this.offset[0]);
1172 let map_x = Math.max(0, this.offset[1]);
1173 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1174 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1175 terminal.write(term_y, term_x, to_draw);
1178 draw_face_popup: function() {
1179 const t = game.things[this.draw_face];
1180 if (!t || !t.face) {
1181 this.draw_face = false;
1184 const start_x = tui.window_width - 10;
1187 t_char = t.thing_char;
1189 function draw_body_part(body_part, end_y) {
1190 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1191 terminal.write(end_y - 3, start_x, '| |');
1192 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1193 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1194 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1197 draw_body_part(t.face, terminal.rows - 2);
1200 draw_body_part(t.hat, terminal.rows - 5);
1202 terminal.write(terminal.rows - 1, start_x, '| |');
1204 draw_mode_line: function() {
1205 let help = 'hit [' + this.keys.help + '] for help';
1206 if (this.mode.has_input_prompt) {
1207 help = 'enter /help for help';
1209 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1211 draw_turn_line: function(n) {
1212 if (game.turn_complete) {
1213 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1216 draw_history: function() {
1217 let log_display_lines = [];
1219 let y_offset_in_log = 0;
1220 for (let line of this.log) {
1221 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1223 log_display_lines = log_display_lines.concat(new_lines);
1224 for (const y in link_data) {
1225 const rel_y = y_offset_in_log + parseInt(y);
1226 log_links[rel_y] = [];
1227 for (let link of link_data[y]) {
1228 log_links[rel_y].push(link);
1231 y_offset_in_log += new_lines.length;
1233 let i = log_display_lines.length - 1;
1234 for (let y = terminal.rows - 1 - this.height_input;
1235 y >= this.height_header && i >= 0;
1237 terminal.write(y, this.window_width, log_display_lines[i]);
1239 for (const key of Object.keys(log_links)) {
1240 if (parseInt(key) <= i) {
1241 delete log_links[key];
1244 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1246 this.offset_links(offset, log_links);
1248 draw_info: function() {
1249 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1250 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1251 let offset = [this.height_header, this.window_width];
1252 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1253 terminal.write(y, offset[1], lines[i]);
1255 this.offset_links(offset, link_data);
1257 draw_input: function() {
1258 if (this.mode.has_input_prompt) {
1259 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1260 terminal.write(y, this.window_width, this.input_lines[i]);
1264 draw_help: function() {
1265 let movement_keys_desc = '';
1266 if (!this.mode.is_intro) {
1267 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1269 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1270 if (this.mode.available_actions.length > 0) {
1271 content += "Available actions:\n";
1272 for (let action of this.mode.available_actions) {
1273 if (Object.keys(this.action_tasks).includes(action)) {
1274 if (!this.task_action_on(action)) {
1278 if (action == 'move_explorer') {
1281 if (action == 'move') {
1282 content += "[" + movement_keys_desc + "] – move\n"
1284 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1289 content += this.mode.list_available_modes();
1291 if (!this.mode.has_input_prompt) {
1292 start_x = this.window_width;
1293 this.draw_links = false;
1295 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1296 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1297 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1298 terminal.write(y, start_x, lines[i]);
1301 toggle_tile_draw: function() {
1302 if (tui.tile_draw) {
1303 tui.tile_draw = false;
1305 tui.tile_draw = true;
1308 toggle_map_mode: function() {
1309 if (tui.map_mode == 'terrain only') {
1310 tui.map_mode = 'terrain + annotations';
1311 } else if (tui.map_mode == 'terrain + annotations') {
1312 tui.map_mode = 'terrain + things';
1313 } else if (tui.map_mode == 'terrain + things') {
1314 tui.map_mode = 'protections';
1315 } else if (tui.map_mode == 'protections') {
1316 tui.map_mode = 'terrain only';
1319 full_refresh: function() {
1320 this.draw_links = true;
1322 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1323 this.recalc_input_lines();
1324 if (this.mode.is_intro) {
1325 this.draw_history();
1329 this.draw_turn_line();
1330 this.draw_mode_line();
1331 if (this.mode.shows_info) {
1334 this.draw_history();
1338 if (this.show_help) {
1341 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1342 this.draw_face_popup();
1344 if (!this.draw_links) {
1354 this.player_id = -1;
1357 this.things_new = {};
1362 this.map_control = "";
1363 this.map_control_new = "";
1364 this.map_size = [0,0];
1365 this.map_size_new = [0,0];
1367 this.portals_new = {};
1368 this.players_hat_chars = "";
1370 get_thing_temp: function(id_, create_if_not_found=false) {
1371 if (id_ in game.things_new) {
1372 return game.things_new[id_];
1373 } else if (create_if_not_found) {
1374 let t = new Thing([0,0]);
1375 game.things_new[id_] = t;
1379 get_thing: function(id_, create_if_not_found=false) {
1380 if (id_ in game.things) {
1381 return game.things[id_];
1384 move: function(start_position, direction) {
1385 let target = [start_position[0], start_position[1]];
1386 if (direction == 'LEFT') {
1388 } else if (direction == 'RIGHT') {
1390 } else if (game.map_geometry == 'Square') {
1391 if (direction == 'UP') {
1393 } else if (direction == 'DOWN') {
1396 } else if (game.map_geometry == 'Hex') {
1397 let start_indented = start_position[0] % 2;
1398 if (direction == 'UPLEFT') {
1400 if (!start_indented) {
1403 } else if (direction == 'UPRIGHT') {
1405 if (start_indented) {
1408 } else if (direction == 'DOWNLEFT') {
1410 if (!start_indented) {
1413 } else if (direction == 'DOWNRIGHT') {
1415 if (start_indented) {
1420 if (target[0] < 0 || target[1] < 0 ||
1421 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1426 teleport: function() {
1427 if (game.player.position in this.portals) {
1428 server.reconnect_to(this.portals[game.player.position]);
1430 terminal.blink_screen();
1431 tui.log_msg('? not standing on portal')
1439 server.init(websocket_location);
1444 annotations_new: {},
1446 move: function(direction) {
1447 let target = game.move(this.position, direction);
1449 this.position = target
1450 this.info_cached = false;
1451 if (tui.tile_draw) {
1452 this.send_tile_control_command();
1455 terminal.blink_screen();
1458 get_info: function() {
1459 if (this.info_cached) {
1460 return this.info_cached;
1462 let info_to_cache = '';
1463 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1464 if (game.fov[position_i] != '.') {
1465 info_to_cache += 'outside field of view';
1467 for (let t_id in game.things) {
1468 let t = game.things[t_id];
1469 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1470 info_to_cache += "THING: " + this.get_thing_info(t);
1471 let protection = t.protection;
1472 if (protection == '.') {
1473 protection = 'none';
1475 info_to_cache += " / protection: " + protection + "\n";
1477 info_to_cache += t.hat.slice(0, 6) + '\n';
1478 info_to_cache += t.hat.slice(6, 12) + '\n';
1479 info_to_cache += t.hat.slice(12, 18) + '\n';
1482 info_to_cache += t.face.slice(0, 6) + '\n';
1483 info_to_cache += t.face.slice(6, 12) + '\n';
1484 info_to_cache += t.face.slice(12, 18) + '\n';
1488 let terrain_char = game.map[position_i]
1489 let terrain_desc = '?'
1490 if (game.terrains[terrain_char]) {
1491 terrain_desc = game.terrains[terrain_char];
1493 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1494 let protection = game.map_control[position_i];
1495 if (protection == '.') {
1496 protection = 'unprotected';
1498 info_to_cache += 'PROTECTION: ' + protection + '\n';
1499 if (this.position in game.portals) {
1500 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1502 if (this.position in this.annotations) {
1503 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1506 this.info_cached = info_to_cache;
1507 return this.info_cached;
1509 get_thing_info: function(t) {
1510 const symbol = game.thing_types[t.type_];
1511 let info = t.type_ + " / " + symbol;
1513 info += t.thing_char;
1516 info += " (" + t.name_ + ")";
1519 info += " / installed";
1523 annotate: function(msg) {
1524 if (msg.length == 0) {
1525 msg = " "; // triggers annotation deletion
1527 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1529 set_portal: function(msg) {
1530 if (msg.length == 0) {
1531 msg = " "; // triggers portal deletion
1533 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1535 send_tile_control_command: function() {
1536 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1540 tui.inputEl.addEventListener('input', (event) => {
1541 if (tui.mode.has_input_prompt) {
1542 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1543 if (tui.inputEl.value.length > max_length) {
1544 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1546 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1547 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1548 tui.switch_mode('edit');
1552 document.onclick = function() {
1553 if (!tui.mode.is_single_char_entry) {
1554 tui.show_help = false;
1557 tui.inputEl.addEventListener('keydown', (event) => {
1558 tui.show_help = false;
1559 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1560 event.preventDefault();
1562 if ((!tui.mode.is_intro && event.key == 'Escape')
1563 || (tui.mode.has_input_prompt && event.key == 'Enter'
1564 && tui.inputEl.value.length == 0
1565 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1566 'admin_enter'].includes(tui.mode.name))) {
1567 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1568 tui.log_msg('@ aborted');
1570 tui.switch_mode('play');
1571 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1572 tui.show_help = true;
1573 tui.inputEl.value = "";
1574 tui.restore_input_values();
1575 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1576 && !tui.mode.is_single_char_entry) {
1577 tui.show_help = true;
1578 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1579 tui.login_name = tui.inputEl.value;
1580 server.send(['LOGIN', tui.inputEl.value]);
1581 tui.inputEl.value = "";
1582 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1583 tui.enter_ascii_art('PLAYER_FACE');
1584 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1585 tui.enter_ascii_art('PLAYER_HAT');
1586 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1587 server.send(['TASK:COMMAND', tui.inputEl.value]);
1588 tui.inputEl.value = "";
1589 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1590 tui.pick_selectable('PICK_UP');
1591 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1592 tui.pick_selectable('DROP');
1593 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1594 if (tui.inputEl.value.length == 0) {
1595 tui.log_msg('@ aborted');
1597 server.send(['SET_MAP_CONTROL_PASSWORD',
1598 tui.tile_control_char, tui.inputEl.value]);
1599 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1601 tui.switch_mode('admin');
1602 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1603 explorer.set_portal(tui.inputEl.value);
1604 tui.switch_mode('edit');
1605 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1606 if (tui.inputEl.value.length == 0) {
1607 tui.inputEl.value = " ";
1609 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1611 tui.switch_mode('edit');
1612 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1613 explorer.annotate(tui.inputEl.value);
1614 tui.switch_mode('edit');
1615 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1616 if (tui.inputEl.value.length == 0) {
1617 tui.inputEl.value = " ";
1619 tui.password = tui.inputEl.value
1620 tui.switch_mode('edit');
1621 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1622 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1623 tui.switch_mode('play');
1624 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1625 if (tui.inputEl.value.length != 1) {
1626 tui.log_msg('@ entered non-single-char, therefore aborted');
1627 tui.switch_mode('admin');
1629 tui.tile_control_char = tui.inputEl.value[0];
1630 tui.switch_mode('control_pw_pw');
1632 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1633 if (tui.inputEl.value.length != 1) {
1634 tui.log_msg('@ entered non-single-char, therefore aborted');
1635 tui.switch_mode('admin');
1637 tui.tile_control_char = tui.inputEl.value[0];
1638 tui.switch_mode('control_tile_draw');
1640 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1641 if (tui.inputEl.value.length != 1) {
1642 tui.log_msg('@ entered non-single-char, therefore aborted');
1644 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1645 tui.log_msg('@ sent new protection character for thing');
1647 tui.switch_mode('admin');
1648 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1649 let tokens = parser.tokenize(tui.inputEl.value);
1650 if (tokens.length > 0 && tokens[0].length > 0) {
1651 if (tui.inputEl.value[0][0] == '/') {
1652 if (tokens[0].slice(1) == 'nick') {
1653 if (tokens.length > 1) {
1654 server.send(['NICK', tokens[1]]);
1656 tui.log_msg('? need new name');
1659 tui.log_msg('? unknown command');
1662 server.send(['ALL', tui.inputEl.value]);
1664 } else if (tui.inputEl.valuelength > 0) {
1665 server.send(['ALL', tui.inputEl.value]);
1667 tui.inputEl.value = "";
1668 } else if (tui.mode.name == 'play') {
1669 if (tui.mode.mode_switch_on_key(event)) {
1671 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1672 server.send(["TASK:INTOXICATE"]);
1673 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1674 server.send(["TASK:DOOR"]);
1675 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1676 server.send(["TASK:WEAR"]);
1677 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1678 server.send(["TASK:SPIN"]);
1679 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1680 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1681 } else if (event.key === tui.keys.teleport) {
1684 } else if (tui.mode.name == 'study') {
1685 if (tui.mode.mode_switch_on_key(event)) {
1687 } else if (event.key in tui.movement_keys) {
1688 explorer.move(tui.movement_keys[event.key]);
1689 } else if (event.key == tui.keys.toggle_map_mode) {
1690 tui.toggle_map_mode();
1692 } else if (tui.mode.name == 'control_tile_draw') {
1693 if (tui.mode.mode_switch_on_key(event)) {
1695 } else if (event.key in tui.movement_keys) {
1696 explorer.move(tui.movement_keys[event.key]);
1697 } else if (event.key === tui.keys.toggle_tile_draw) {
1698 tui.toggle_tile_draw();
1700 } else if (tui.mode.name == 'admin') {
1701 if (tui.mode.mode_switch_on_key(event)) {
1703 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1704 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1706 } else if (tui.mode.name == 'edit') {
1707 if (tui.mode.mode_switch_on_key(event)) {
1709 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1710 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1711 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1712 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1713 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1714 server.send(["TASK:INSTALL", tui.password]);
1715 } else if (event.key == tui.keys.toggle_map_mode) {
1716 tui.toggle_map_mode();
1722 rows_selector.addEventListener('input', function() {
1723 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1726 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1727 terminal.initialize();
1730 cols_selector.addEventListener('input', function() {
1731 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1734 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1735 terminal.initialize();
1736 tui.window_width = terminal.cols / 2,
1739 for (let key_selector of key_selectors) {
1740 key_selector.addEventListener('input', function() {
1741 window.localStorage.setItem(key_selector.id, key_selector.value);
1745 window.setInterval(function() {
1746 if (server.websocket.readyState == 1) {
1747 server.send(['PING']);
1748 } else if (server.websocket.readyState != 0) {
1749 server.reconnect_to(server.url);
1750 tui.log_msg('@ attempting reconnect …')
1753 window.setInterval(function() {
1754 if (document.activeElement.tagName.toLowerCase() != 'input') {
1755 const scroll_x = window.scrollX;
1756 const scroll_y = window.scrollY;
1757 tui.inputEl.focus();
1758 window.scrollTo(scroll_x, scroll_y);
1761 document.getElementById("help").onclick = function() {
1762 tui.show_help = true;
1765 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1766 const mode = switchEl.id.slice("switch_to_".length);
1767 switchEl.onclick = function() {
1768 tui.switch_mode(mode);
1772 document.getElementById("toggle_tile_draw").onclick = function() {
1773 tui.toggle_tile_draw();
1775 document.getElementById("toggle_map_mode").onclick = function() {
1776 tui.toggle_map_mode();
1779 document.getElementById("flatten").onclick = function() {
1780 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1782 document.getElementById("door").onclick = function() {
1783 server.send(['TASK:DOOR']);
1785 document.getElementById("consume").onclick = function() {
1786 server.send(['TASK:INTOXICATE']);
1788 document.getElementById("install").onclick = function() {
1789 server.send(['TASK:INSTALL', tui.password]);
1791 document.getElementById("wear").onclick = function() {
1792 server.send(['TASK:WEAR']);
1794 document.getElementById("spin").onclick = function() {
1795 server.send(['TASK:SPIN']);
1797 document.getElementById("teleport").onclick = function() {
1800 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1801 if (move_button.id.startsWith('key_')) { // not a move button
1804 let direction = move_button.id.split('_')[2].toUpperCase();
1807 if (tui.mode.available_actions.includes("move")) {
1808 server.send(['TASK:MOVE', direction]);
1809 } else if (tui.mode.available_actions.includes("move_explorer")) {
1810 explorer.move(direction);
1814 move_button.onmousedown = function() {
1816 move_repeat = window.setInterval(move, 100);
1818 move_button.onmouseup = function() {
1819 window.clearInterval(move_repeat);