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 game.turn = parseInt(tokens[1]);
489 } else if (tokens[0] === 'OTHER_WIPE') {
490 game.portals_new = {};
491 explorer.annotations_new = {};
492 game.things_new = [];
493 } else if (tokens[0] === 'THING') {
494 let t = game.get_thing_temp(tokens[4], true);
495 t.position = parser.parse_yx(tokens[1]);
497 t.protection = tokens[3];
498 t.portable = parseInt(tokens[5]);
499 t.commandable = parseInt(tokens[6]);
500 } else if (tokens[0] === 'THING_NAME') {
501 let t = game.get_thing_temp(tokens[1]);
503 } else if (tokens[0] === 'THING_FACE') {
504 let t = game.get_thing_temp(tokens[1]);
506 } else if (tokens[0] === 'THING_HAT') {
507 let t = game.get_thing_temp(tokens[1]);
509 } else if (tokens[0] === 'THING_CHAR') {
510 let t = game.get_thing_temp(tokens[1]);
511 t.thing_char = tokens[2];
512 } else if (tokens[0] === 'TASKS') {
513 game.tasks = tokens[1].split(',');
514 tui.mode_write.legal = game.tasks.includes('WRITE');
515 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
516 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
517 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
518 } else if (tokens[0] === 'THING_TYPE') {
519 game.thing_types[tokens[1]] = tokens[2]
520 } else if (tokens[0] === 'THING_CARRYING') {
521 let t = game.get_thing_temp(tokens[1]);
522 t.carrying = game.get_thing_temp(tokens[2], false);
523 } else if (tokens[0] === 'THING_INSTALLED') {
524 let t = game.get_thing_temp(tokens[1]);
526 } else if (tokens[0] === 'TERRAIN') {
527 game.terrains[tokens[1]] = tokens[2]
528 } else if (tokens[0] === 'MAP') {
529 game.map_geometry_new = tokens[1];
530 game.map_size_new = parser.parse_yx(tokens[2]);
531 game.map_new = tokens[3]
532 } else if (tokens[0] === 'FOV') {
533 game.fov_new = tokens[1]
534 } else if (tokens[0] === 'MAP_CONTROL') {
535 game.map_control_new = tokens[1]
536 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
537 game.portals = game.portals_new;
538 game.map_geometry = game.map_geometry_new;
539 game.map_size = game.map_size_new;
540 game.map = game.map_new;
541 game.fov = game.fov_new;
543 game.map_control = game.map_control_new;
544 explorer.annotations = explorer.annotations_new;
545 explorer.info_cached = false;
546 game.things = game.things_new;
547 game.player = game.things[game.player_id];
548 game.players_hat_chars = game.players_hat_chars_new;
549 game.turn_complete = true;
550 if (tui.mode.name == 'post_login_wait') {
551 tui.switch_mode('play');
555 } else if (tokens[0] === 'CHAT') {
556 tui.log_msg('# ' + tokens[1], 1);
557 } else if (tokens[0] === 'CHATFACE') {
558 tui.draw_face = tokens[1];
560 } else if (tokens[0] === 'REPLY') {
561 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
562 } else if (tokens[0] === 'PLAYER_ID') {
563 game.player_id = parseInt(tokens[1]);
564 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
565 game.players_hat_chars_new = tokens[1];
566 } else if (tokens[0] === 'LOGIN_OK') {
567 this.send(['GET_GAMESTATE']);
568 tui.switch_mode('post_login_wait');
569 } else if (tokens[0] === 'DEFAULT_COLORS') {
570 terminal.set_default_colors();
571 } else if (tokens[0] === 'RANDOM_COLORS') {
572 terminal.set_random_colors();
573 } else if (tokens[0] === 'ADMIN_OK') {
575 tui.log_msg('@ you now have admin rights');
576 tui.switch_mode('admin');
577 } else if (tokens[0] === 'PORTAL') {
578 let position = parser.parse_yx(tokens[1]);
579 game.portals_new[position] = tokens[2];
580 } else if (tokens[0] === 'ANNOTATION') {
581 let position = parser.parse_yx(tokens[1]);
582 explorer.annotations_new[position] = tokens[2];
583 } else if (tokens[0] === 'UNHANDLED_INPUT') {
584 tui.log_msg('? unknown command');
585 } else if (tokens[0] === 'PLAY_ERROR') {
586 tui.log_msg('? ' + tokens[1]);
587 terminal.blink_screen();
588 } else if (tokens[0] === 'ARGUMENT_ERROR') {
589 tui.log_msg('? syntax error: ' + tokens[1]);
590 } else if (tokens[0] === 'GAME_ERROR') {
591 tui.log_msg('? game error: ' + tokens[1]);
592 } else if (tokens[0] === 'PONG') {
595 tui.log_msg('? unhandled input: ' + event.data);
601 quote: function(str) {
603 for (let i = 0; i < str.length; i++) {
605 if (['"', '\\'].includes(c)) {
611 return quoted.join('');
613 to_yx: function(yx_coordinate) {
614 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
616 untokenize: function(tokens) {
617 let quoted_tokens = [];
618 for (let token of tokens) {
619 quoted_tokens.push(this.quote(token));
621 return quoted_tokens.join(" ");
626 constructor(name, has_input_prompt=false, shows_info=false,
627 is_intro=false, is_single_char_entry=false) {
629 this.short_desc = mode_helps[name].short;
630 this.available_modes = [];
631 this.available_actions = [];
632 this.has_input_prompt = has_input_prompt;
633 this.shows_info= shows_info;
634 this.is_intro = is_intro;
635 this.help_intro = mode_helps[name].long;
636 this.intro_msg = mode_helps[name].intro;
637 this.is_single_char_entry = is_single_char_entry;
640 *iter_available_modes() {
641 for (let mode_name of this.available_modes) {
642 let mode = tui['mode_' + mode_name];
646 let key = tui.keys['switch_to_' + mode.name];
650 list_available_modes() {
652 if (this.available_modes.length > 0) {
653 msg += 'Other modes available from here:\n';
654 for (let [mode, key] of this.iter_available_modes()) {
655 msg += '[' + key + '] – ' + mode.short_desc + '\n';
660 mode_switch_on_key(key_event) {
661 for (let [mode, key] of this.iter_available_modes()) {
662 if (key_event.key == key) {
663 event.preventDefault();
664 tui.switch_mode(mode.name);
676 window_width: terminal.cols / 2,
684 mode_waiting_for_server: new Mode('waiting_for_server',
686 mode_login: new Mode('login', true, false, true),
687 mode_post_login_wait: new Mode('post_login_wait'),
688 mode_chat: new Mode('chat', true),
689 mode_annotate: new Mode('annotate', true, true),
690 mode_play: new Mode('play'),
691 mode_study: new Mode('study', false, true),
692 mode_write: new Mode('write', false, false, false, true),
693 mode_edit: new Mode('edit'),
694 mode_control_pw_type: new Mode('control_pw_type', true),
695 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
696 mode_portal: new Mode('portal', true, true),
697 mode_password: new Mode('password', true),
698 mode_name_thing: new Mode('name_thing', true, true),
699 mode_command_thing: new Mode('command_thing', true),
700 mode_take_thing: new Mode('take_thing', true),
701 mode_drop_thing: new Mode('drop_thing', true),
702 mode_enter_face: new Mode('enter_face', true),
703 mode_enter_hat: new Mode('enter_hat', true),
704 mode_admin_enter: new Mode('admin_enter', true),
705 mode_admin: new Mode('admin'),
706 mode_control_pw_pw: new Mode('control_pw_pw', true),
707 mode_control_tile_type: new Mode('control_tile_type', true),
708 mode_control_tile_draw: new Mode('control_tile_draw'),
710 'flatten': 'FLATTEN_SURROUNDINGS',
711 'take_thing': 'PICK_UP',
712 'drop_thing': 'DROP',
715 'install': 'INSTALL',
717 'command': 'COMMAND',
718 'consume': 'INTOXICATE',
728 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
729 "command_thing", "take_thing", "drop_thing"]
730 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
732 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
733 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
734 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
735 "control_tile_type", "chat",
736 "study", "play", "edit"]
737 this.mode_admin.available_actions = ["move"];
738 this.mode_control_tile_draw.available_modes = ["admin_enter"]
739 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
740 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
741 "password", "chat", "study", "play",
742 "admin_enter", "enter_face", "enter_hat"]
743 this.mode_edit.available_actions = ["move", "flatten", "install",
745 this.inputEl = document.getElementById("input");
746 this.switch_mode('waiting_for_server');
747 this.recalc_input_lines();
748 this.height_header = this.height_turn_line + this.height_mode_line;
751 init_keys: function() {
752 document.getElementById("move_table").hidden = true;
754 for (let key_selector of key_selectors) {
755 this.keys[key_selector.id.slice(4)] = key_selector.value;
757 this.movement_keys = {};
758 let geometry_prefix = 'undefinedMapGeometry_';
759 if (game.map_geometry) {
760 geometry_prefix = game.map_geometry.toLowerCase() + '_';
762 for (const key_name of Object.keys(key_descriptions)) {
763 if (key_name.startsWith(geometry_prefix)) {
764 let direction = key_name.split('_')[2].toUpperCase();
765 let key = this.keys[key_name];
766 this.movement_keys[key] = direction;
769 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
770 if (move_button.id.startsWith('key_')) {
773 move_button.hidden = true;
775 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
776 document.getElementById("move_table").hidden = false;
777 move_button.hidden = false;
779 for (let el of document.getElementsByTagName("button")) {
780 let action_desc = key_descriptions[el.id];
781 let action_key = '[' + this.keys[el.id] + ']';
782 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
785 task_action_on: function(action) {
786 return game.tasks.includes(this.action_tasks[action]);
788 switch_mode: function(mode_name) {
790 function fail(msg, return_mode='play') {
791 tui.log_msg('? ' + msg);
792 terminal.blink_screen();
793 tui.switch_mode(return_mode);
796 if (this.mode && this.mode.name == 'control_tile_draw') {
797 tui.log_msg('@ finished tile protection drawing.')
799 this.draw_face = false;
800 this.tile_draw = false;
801 if (mode_name == 'command_thing' && (!game.player.carrying
802 || !game.player.carrying.commandable)) {
803 return fail('not carrying anything commandable');
804 } else if (mode_name == 'name_thing' && !game.player.carrying) {
805 return fail('not carrying anything to re-name');
806 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
807 return fail('not carrying anything to protect')
808 } else if (mode_name == 'take_thing' && game.player.carrying) {
809 return fail('already carrying something');
810 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
811 return fail('not carrying anything droppable');
812 } else if (mode_name == 'enter_hat' && !game.player.hat) {
813 return fail('not wearing hat to edit', 'edit');
815 if (mode_name == 'admin_enter' && this.is_admin) {
818 this.mode = this['mode_' + mode_name];
819 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
820 this.map_mode = 'protections';
821 } else if (this.mode.name != "edit") {
822 this.map_mode = 'terrain + things';
824 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
825 explorer.position = game.player.position;
827 this.inputEl.value = "";
828 this.restore_input_values();
829 for (let el of document.getElementsByTagName("button")) {
832 document.getElementById("help").disabled = false;
833 for (const action of this.mode.available_actions) {
834 if (["move", "move_explorer"].includes(action)) {
835 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
836 move_key.disabled = false;
838 } else if (Object.keys(this.action_tasks).includes(action)) {
839 if (this.task_action_on(action)) {
840 document.getElementById(action).disabled = false;
843 document.getElementById(action).disabled = false;
846 for (const mode_name of this.mode.available_modes) {
847 document.getElementById('switch_to_' + mode_name).disabled = false;
849 if (this.mode.intro_msg.length > 0) {
850 this.log_msg(this.mode.intro_msg);
852 if (this.mode.name == 'login') {
853 if (this.login_name) {
854 server.send(['LOGIN', this.login_name]);
856 this.log_msg("? need login name");
858 } else if (this.mode.is_single_char_entry) {
859 this.show_help = true;
860 } else if (this.mode.name == 'take_thing') {
861 this.log_msg("Portable things in reach for pick-up:");
862 const y = game.player.position[0]
863 const x = game.player.position[1]
864 let directed_moves = {
865 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
867 if (game.map_geometry == 'Square') {
868 directed_moves['UP'] = [-1, 0];
869 directed_moves['DOWN'] = [1, 0];
870 } else if (game.map_geometry == 'Hex') {
872 directed_moves['UPLEFT'] = [-1, 0];
873 directed_moves['UPRIGHT'] = [-1, 1];
874 directed_moves['DOWNLEFT'] = [1, 0];
875 directed_moves['DOWNRIGHT'] = [1, 1];
877 directed_moves['UPLEFT'] = [-1, -1];
878 directed_moves['UPRIGHT'] = [-1, 0];
879 directed_moves['DOWNLEFT'] = [1, -1];
880 directed_moves['DOWNRIGHT'] = [1, 0];
883 console.log(directed_moves);
884 let select_range = {};
885 for (const direction in directed_moves) {
886 const move = directed_moves[direction];
887 select_range[direction] = [y + move[0], x + move[1]];
889 this.selectables = [];
891 for (const direction in select_range) {
892 for (const t_id in game.things) {
893 const t = game.things[t_id];
894 const position = select_range[direction];
896 && t.position[0] == position[0]
897 && t.position[1] == position[1]) {
898 this.selectables.push(t_id);
899 directions.push(direction);
903 if (this.selectables.length == 0) {
904 this.log_msg('none');
905 terminal.blink_screen();
906 this.switch_mode('play');
909 for (let [i, t_id] of this.selectables.entries()) {
910 const t = game.things[t_id];
911 const direction = directions[i];
912 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
915 } else if (this.mode.name == 'drop_thing') {
916 this.log_msg('Direction to drop thing to:');
917 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
918 for (let [i, direction] of this.selectables.entries()) {
919 this.log_msg(i + ': ' + direction);
921 } else if (this.mode.name == 'enter_hat') {
922 this.log_msg('legal characters: ' + game.players_hat_chars);
923 } else if (this.mode.name == 'command_thing') {
924 server.send(['TASK:COMMAND', 'HELP']);
925 } else if (this.mode.name == 'control_pw_pw') {
926 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
927 } else if (this.mode.name == 'control_tile_draw') {
928 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 + '].')
932 offset_links: function(offset, links) {
933 for (let y in links) {
934 let real_y = offset[0] + parseInt(y);
935 if (!this.links[real_y]) {
936 this.links[real_y] = [];
938 for (let link of links[y]) {
939 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
940 this.links[real_y].push(offset_link);
944 restore_input_values: function() {
945 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
946 let info = explorer.annotations[explorer.position];
947 if (info != "(none)") {
948 this.inputEl.value = info;
950 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
951 let portal = game.portals[explorer.position]
952 this.inputEl.value = portal;
953 } else if (this.mode.name == 'password') {
954 this.inputEl.value = this.password;
955 } else if (this.mode.name == 'name_thing') {
956 if (game.player.carrying && game.player.carrying.name_) {
957 this.inputEl.value = game.player.carrying.name_;
959 } else if (this.mode.name == 'admin_thing_protect') {
960 if (game.player.carrying && game.player.carrying.protection) {
961 this.inputEl.value = game.player.carrying.protection;
963 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
964 const start = this.ascii_draw_stage * 6;
965 const end = (this.ascii_draw_stage + 1) * 6;
966 if (this.mode.name == 'enter_face') {
967 this.inputEl.value = game.player.face.slice(start, end);
968 } else if (this.mode.name == 'enter_hat') {
969 this.inputEl.value = game.player.hat.slice(start, end);
973 recalc_input_lines: function() {
974 if (this.mode.has_input_prompt) {
976 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
978 this.input_lines = [];
980 this.height_input = this.input_lines.length;
982 msg_into_lines_of_width: function(msg, width) {
983 function push_inner_link(y, end_x) {
984 if (!inner_links[y]) {
987 inner_links[y].push([url_start_x, end_x, url]);
991 const regexp = RegExp('https?://[^\\s]+', 'g');
993 while ((match = regexp.exec(msg)) !== null) {
994 const url = match[0];
995 const url_start = match.index;
996 const url_end = match.index + match[0].length;
997 link_data[url_start] = url;
998 url_ends.push(url_end);
1000 let url_start_x = 0;
1002 let inner_links = {};
1003 let in_link = false;
1006 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1007 if (x >= width || msg[i] == "\n") {
1009 push_inner_link(y, chunk.length);
1011 if (url_ends[0] == i) {
1019 if (msg[i] == "\n") {
1024 if (msg[i] != "\n") {
1027 if (i in link_data) {
1031 } else if (url_ends[0] == i) {
1033 push_inner_link(y, x);
1039 push_inner_link(lines.length - 1, chunk.length);
1041 return [lines, inner_links];
1043 log_msg: function(msg) {
1045 while (this.log.length > 100) {
1048 this.full_refresh();
1050 pick_selectable: function(task_name) {
1051 const i = parseInt(this.inputEl.value);
1052 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1053 tui.log_msg('? invalid index, aborted');
1055 server.send(['TASK:' + task_name, tui.selectables[i]]);
1057 this.inputEl.value = "";
1058 this.switch_mode('play');
1060 enter_ascii_art: function(command) {
1061 if (this.inputEl.value.length != 6) {
1062 this.log_msg('? wrong input length, must be 6; try again');
1065 this.log_msg(' ' + this.inputEl.value);
1066 this.full_ascii_draw += this.inputEl.value;
1067 this.ascii_draw_stage += 1;
1068 if (this.ascii_draw_stage < 3) {
1069 this.restore_input_values();
1071 server.send([command, this.full_ascii_draw]);
1072 this.full_ascii_draw = '';
1073 this.ascii_draw_stage = 0;
1074 this.inputEl.value = '';
1075 this.switch_mode('edit');
1078 draw_map: function() {
1079 if (!game.turn_complete && this.map_lines.length == 0) {
1082 if (game.turn_complete) {
1083 let map_lines_split = [];
1085 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1086 if (j == game.map_size[1]) {
1087 map_lines_split.push(line);
1091 if (this.map_mode == 'protections') {
1092 line.push(game.map_control[i] + ' ');
1094 line.push(game.map[i] + ' ');
1097 map_lines_split.push(line);
1098 if (this.map_mode == 'terrain + annotations') {
1099 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1100 const yx = coordinate.split(',')
1101 map_lines_split[yx[0]][yx[1]] = 'A ';
1103 } else if (this.map_mode == 'terrain + things') {
1104 for (const p in game.portals) {
1105 let coordinate = p.split(',')
1106 let original = map_lines_split[coordinate[0]][coordinate[1]];
1107 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1109 let used_positions = [];
1110 function draw_thing(t, used_positions) {
1111 let symbol = game.thing_types[t.type_];
1112 let meta_char = ' ';
1114 meta_char = t.thing_char;
1116 if (used_positions.includes(t.position.toString())) {
1122 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1123 used_positions.push(t.position.toString());
1125 for (const thing_id in game.things) {
1126 let t = game.things[thing_id];
1127 if (t.type_ != 'Player') {
1128 draw_thing(t, used_positions);
1131 for (const thing_id in game.things) {
1132 let t = game.things[thing_id];
1133 if (t.type_ == 'Player') {
1134 draw_thing(t, used_positions);
1138 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1139 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1140 } else if (tui.map_mode != 'terrain + things') {
1141 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1144 if (game.map_geometry == 'Square') {
1145 for (let line_split of map_lines_split) {
1146 this.map_lines.push(line_split.join(''));
1148 } else if (game.map_geometry == 'Hex') {
1150 for (let line_split of map_lines_split) {
1151 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1159 let window_center = [terminal.rows / 2, this.window_width / 2];
1160 let center_position = [game.player.position[0], game.player.position[1]];
1161 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1162 center_position = [explorer.position[0], explorer.position[1]];
1164 center_position[1] = center_position[1] * 2;
1165 this.offset = [center_position[0] - window_center[0],
1166 center_position[1] - window_center[1]]
1167 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1168 this.offset[1] += 1;
1171 let term_y = Math.max(0, -this.offset[0]);
1172 let term_x = Math.max(0, -this.offset[1]);
1173 let map_y = Math.max(0, this.offset[0]);
1174 let map_x = Math.max(0, this.offset[1]);
1175 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1176 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1177 terminal.write(term_y, term_x, to_draw);
1180 draw_face_popup: function() {
1181 const t = game.things[this.draw_face];
1182 if (!t || !t.face) {
1183 this.draw_face = false;
1186 const start_x = tui.window_width - 10;
1189 t_char = t.thing_char;
1191 function draw_body_part(body_part, end_y) {
1192 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1193 terminal.write(end_y - 3, start_x, '| |');
1194 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1195 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1196 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1199 draw_body_part(t.face, terminal.rows - 2);
1202 draw_body_part(t.hat, terminal.rows - 5);
1204 terminal.write(terminal.rows - 1, start_x, '| |');
1206 draw_mode_line: function() {
1207 let help = 'hit [' + this.keys.help + '] for help';
1208 if (this.mode.has_input_prompt) {
1209 help = 'enter /help for help';
1211 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1213 draw_turn_line: function(n) {
1214 if (game.turn_complete) {
1215 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1218 draw_history: function() {
1219 let log_display_lines = [];
1221 let y_offset_in_log = 0;
1222 for (let line of this.log) {
1223 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1225 log_display_lines = log_display_lines.concat(new_lines);
1226 for (const y in link_data) {
1227 const rel_y = y_offset_in_log + parseInt(y);
1228 log_links[rel_y] = [];
1229 for (let link of link_data[y]) {
1230 log_links[rel_y].push(link);
1233 y_offset_in_log += new_lines.length;
1235 let i = log_display_lines.length - 1;
1236 for (let y = terminal.rows - 1 - this.height_input;
1237 y >= this.height_header && i >= 0;
1239 terminal.write(y, this.window_width, log_display_lines[i]);
1241 for (const key of Object.keys(log_links)) {
1242 if (parseInt(key) <= i) {
1243 delete log_links[key];
1246 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1248 this.offset_links(offset, log_links);
1250 draw_info: function() {
1251 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1252 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1253 let offset = [this.height_header, this.window_width];
1254 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1255 terminal.write(y, offset[1], lines[i]);
1257 this.offset_links(offset, link_data);
1259 draw_input: function() {
1260 if (this.mode.has_input_prompt) {
1261 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1262 terminal.write(y, this.window_width, this.input_lines[i]);
1266 draw_help: function() {
1267 let movement_keys_desc = '';
1268 if (!this.mode.is_intro) {
1269 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1271 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1272 if (this.mode.available_actions.length > 0) {
1273 content += "Available actions:\n";
1274 for (let action of this.mode.available_actions) {
1275 if (Object.keys(this.action_tasks).includes(action)) {
1276 if (!this.task_action_on(action)) {
1280 if (action == 'move_explorer') {
1283 if (action == 'move') {
1284 content += "[" + movement_keys_desc + "] – move\n"
1286 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1291 content += this.mode.list_available_modes();
1293 if (!this.mode.has_input_prompt) {
1294 start_x = this.window_width;
1295 this.draw_links = false;
1297 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1298 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1299 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1300 terminal.write(y, start_x, lines[i]);
1303 toggle_tile_draw: function() {
1304 if (tui.tile_draw) {
1305 tui.tile_draw = false;
1307 tui.tile_draw = true;
1310 toggle_map_mode: function() {
1311 if (tui.map_mode == 'terrain only') {
1312 tui.map_mode = 'terrain + annotations';
1313 } else if (tui.map_mode == 'terrain + annotations') {
1314 tui.map_mode = 'terrain + things';
1315 } else if (tui.map_mode == 'terrain + things') {
1316 tui.map_mode = 'protections';
1317 } else if (tui.map_mode == 'protections') {
1318 tui.map_mode = 'terrain only';
1321 full_refresh: function() {
1322 this.draw_links = true;
1324 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1325 this.recalc_input_lines();
1326 if (this.mode.is_intro) {
1327 this.draw_history();
1331 this.draw_turn_line();
1332 this.draw_mode_line();
1333 if (this.mode.shows_info) {
1336 this.draw_history();
1340 if (this.show_help) {
1343 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1344 this.draw_face_popup();
1346 if (!this.draw_links) {
1356 this.player_id = -1;
1359 this.things_new = {};
1364 this.map_control = "";
1365 this.map_control_new = "";
1366 this.map_size = [0,0];
1367 this.map_size_new = [0,0];
1369 this.portals_new = {};
1370 this.players_hat_chars = "";
1372 get_thing_temp: function(id_, create_if_not_found=false) {
1373 if (id_ in game.things_new) {
1374 return game.things_new[id_];
1375 } else if (create_if_not_found) {
1376 let t = new Thing([0,0]);
1377 game.things_new[id_] = t;
1381 get_thing: function(id_, create_if_not_found=false) {
1382 if (id_ in game.things) {
1383 return game.things[id_];
1386 move: function(start_position, direction) {
1387 let target = [start_position[0], start_position[1]];
1388 if (direction == 'LEFT') {
1390 } else if (direction == 'RIGHT') {
1392 } else if (game.map_geometry == 'Square') {
1393 if (direction == 'UP') {
1395 } else if (direction == 'DOWN') {
1398 } else if (game.map_geometry == 'Hex') {
1399 let start_indented = start_position[0] % 2;
1400 if (direction == 'UPLEFT') {
1402 if (!start_indented) {
1405 } else if (direction == 'UPRIGHT') {
1407 if (start_indented) {
1410 } else if (direction == 'DOWNLEFT') {
1412 if (!start_indented) {
1415 } else if (direction == 'DOWNRIGHT') {
1417 if (start_indented) {
1422 if (target[0] < 0 || target[1] < 0 ||
1423 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1428 teleport: function() {
1429 if (game.player.position in this.portals) {
1430 server.reconnect_to(this.portals[game.player.position]);
1432 terminal.blink_screen();
1433 tui.log_msg('? not standing on portal')
1441 server.init(websocket_location);
1446 annotations_new: {},
1448 move: function(direction) {
1449 let target = game.move(this.position, direction);
1451 this.position = target
1452 this.info_cached = false;
1453 if (tui.tile_draw) {
1454 this.send_tile_control_command();
1457 terminal.blink_screen();
1460 get_info: function() {
1461 if (this.info_cached) {
1462 return this.info_cached;
1464 let info_to_cache = '';
1465 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1466 if (game.fov[position_i] != '.') {
1467 info_to_cache += 'outside field of view';
1469 for (let t_id in game.things) {
1470 let t = game.things[t_id];
1471 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1472 info_to_cache += "THING: " + this.get_thing_info(t);
1473 let protection = t.protection;
1474 if (protection == '.') {
1475 protection = 'none';
1477 info_to_cache += " / protection: " + protection + "\n";
1479 info_to_cache += t.hat.slice(0, 6) + '\n';
1480 info_to_cache += t.hat.slice(6, 12) + '\n';
1481 info_to_cache += t.hat.slice(12, 18) + '\n';
1484 info_to_cache += t.face.slice(0, 6) + '\n';
1485 info_to_cache += t.face.slice(6, 12) + '\n';
1486 info_to_cache += t.face.slice(12, 18) + '\n';
1490 let terrain_char = game.map[position_i]
1491 let terrain_desc = '?'
1492 if (game.terrains[terrain_char]) {
1493 terrain_desc = game.terrains[terrain_char];
1495 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1496 let protection = game.map_control[position_i];
1497 if (protection == '.') {
1498 protection = 'unprotected';
1500 info_to_cache += 'PROTECTION: ' + protection + '\n';
1501 if (this.position in game.portals) {
1502 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1504 if (this.position in this.annotations) {
1505 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1508 this.info_cached = info_to_cache;
1509 return this.info_cached;
1511 get_thing_info: function(t) {
1512 const symbol = game.thing_types[t.type_];
1513 let info = t.type_ + " / " + symbol;
1515 info += t.thing_char;
1518 info += " (" + t.name_ + ")";
1521 info += " / installed";
1525 annotate: function(msg) {
1526 if (msg.length == 0) {
1527 msg = " "; // triggers annotation deletion
1529 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1531 set_portal: function(msg) {
1532 if (msg.length == 0) {
1533 msg = " "; // triggers portal deletion
1535 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1537 send_tile_control_command: function() {
1538 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1542 tui.inputEl.addEventListener('input', (event) => {
1543 if (tui.mode.has_input_prompt) {
1544 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1545 if (tui.inputEl.value.length > max_length) {
1546 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1548 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1549 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1550 tui.switch_mode('edit');
1554 document.onclick = function() {
1555 if (!tui.mode.is_single_char_entry) {
1556 tui.show_help = false;
1559 tui.inputEl.addEventListener('keydown', (event) => {
1560 tui.show_help = false;
1561 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1562 event.preventDefault();
1564 if ((!tui.mode.is_intro && event.key == 'Escape')
1565 || (tui.mode.has_input_prompt && event.key == 'Enter'
1566 && tui.inputEl.value.length == 0
1567 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1568 'admin_enter'].includes(tui.mode.name))) {
1569 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1570 tui.log_msg('@ aborted');
1572 tui.switch_mode('play');
1573 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1574 tui.show_help = true;
1575 tui.inputEl.value = "";
1576 tui.restore_input_values();
1577 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1578 && !tui.mode.is_single_char_entry) {
1579 tui.show_help = true;
1580 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1581 tui.login_name = tui.inputEl.value;
1582 server.send(['LOGIN', tui.inputEl.value]);
1583 tui.inputEl.value = "";
1584 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1585 tui.enter_ascii_art('PLAYER_FACE');
1586 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1587 tui.enter_ascii_art('PLAYER_HAT');
1588 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1589 server.send(['TASK:COMMAND', tui.inputEl.value]);
1590 tui.inputEl.value = "";
1591 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1592 tui.pick_selectable('PICK_UP');
1593 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1594 tui.pick_selectable('DROP');
1595 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1596 if (tui.inputEl.value.length == 0) {
1597 tui.log_msg('@ aborted');
1599 server.send(['SET_MAP_CONTROL_PASSWORD',
1600 tui.tile_control_char, tui.inputEl.value]);
1601 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1603 tui.switch_mode('admin');
1604 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1605 explorer.set_portal(tui.inputEl.value);
1606 tui.switch_mode('edit');
1607 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1608 if (tui.inputEl.value.length == 0) {
1609 tui.inputEl.value = " ";
1611 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1612 tui.switch_mode('edit');
1613 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1614 explorer.annotate(tui.inputEl.value);
1615 tui.switch_mode('edit');
1616 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1617 if (tui.inputEl.value.length == 0) {
1618 tui.inputEl.value = " ";
1620 tui.password = tui.inputEl.value
1621 tui.switch_mode('edit');
1622 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1623 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1624 tui.switch_mode('play');
1625 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1626 if (tui.inputEl.value.length != 1) {
1627 tui.log_msg('@ entered non-single-char, therefore aborted');
1628 tui.switch_mode('admin');
1630 tui.tile_control_char = tui.inputEl.value[0];
1631 tui.switch_mode('control_pw_pw');
1633 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1634 if (tui.inputEl.value.length != 1) {
1635 tui.log_msg('@ entered non-single-char, therefore aborted');
1636 tui.switch_mode('admin');
1638 tui.tile_control_char = tui.inputEl.value[0];
1639 tui.switch_mode('control_tile_draw');
1641 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1642 if (tui.inputEl.value.length != 1) {
1643 tui.log_msg('@ entered non-single-char, therefore aborted');
1645 server.send(['THING_PROTECTION', tui.inputEl.value])
1646 tui.log_msg('@ sent new protection character for thing');
1648 tui.switch_mode('admin');
1649 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1650 let tokens = parser.tokenize(tui.inputEl.value);
1651 if (tokens.length > 0 && tokens[0].length > 0) {
1652 if (tui.inputEl.value[0][0] == '/') {
1653 if (tokens[0].slice(1) == 'nick') {
1654 if (tokens.length > 1) {
1655 server.send(['NICK', tokens[1]]);
1657 tui.log_msg('? need new name');
1660 tui.log_msg('? unknown command');
1663 server.send(['ALL', tui.inputEl.value]);
1665 } else if (tui.inputEl.valuelength > 0) {
1666 server.send(['ALL', tui.inputEl.value]);
1668 tui.inputEl.value = "";
1669 } else if (tui.mode.name == 'play') {
1670 if (tui.mode.mode_switch_on_key(event)) {
1672 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1673 server.send(["TASK:INTOXICATE"]);
1674 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1675 server.send(["TASK:DOOR"]);
1676 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1677 server.send(["TASK:WEAR"]);
1678 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1679 server.send(["TASK:SPIN"]);
1680 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1681 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1682 } else if (event.key === tui.keys.teleport) {
1685 } else if (tui.mode.name == 'study') {
1686 if (tui.mode.mode_switch_on_key(event)) {
1688 } else if (event.key in tui.movement_keys) {
1689 explorer.move(tui.movement_keys[event.key]);
1690 } else if (event.key == tui.keys.toggle_map_mode) {
1691 tui.toggle_map_mode();
1693 } else if (tui.mode.name == 'control_tile_draw') {
1694 if (tui.mode.mode_switch_on_key(event)) {
1696 } else if (event.key in tui.movement_keys) {
1697 explorer.move(tui.movement_keys[event.key]);
1698 } else if (event.key === tui.keys.toggle_tile_draw) {
1699 tui.toggle_tile_draw();
1701 } else if (tui.mode.name == 'admin') {
1702 if (tui.mode.mode_switch_on_key(event)) {
1704 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1705 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1707 } else if (tui.mode.name == 'edit') {
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]]);
1712 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1713 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1714 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1715 server.send(["TASK:INSTALL", tui.password]);
1716 } else if (event.key == tui.keys.toggle_map_mode) {
1717 tui.toggle_map_mode();
1723 rows_selector.addEventListener('input', function() {
1724 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1727 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1728 terminal.initialize();
1731 cols_selector.addEventListener('input', function() {
1732 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1735 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1736 terminal.initialize();
1737 tui.window_width = terminal.cols / 2,
1740 for (let key_selector of key_selectors) {
1741 key_selector.addEventListener('input', function() {
1742 window.localStorage.setItem(key_selector.id, key_selector.value);
1746 window.setInterval(function() {
1747 if (server.websocket.readyState == 1) {
1748 server.send(['PING']);
1749 } else if (server.websocket.readyState != 0) {
1750 server.reconnect_to(server.url);
1751 tui.log_msg('@ attempting reconnect …')
1754 window.setInterval(function() {
1755 if (document.activeElement.tagName.toLowerCase() != 'input') {
1756 tui.inputEl.focus();
1759 document.getElementById("help").onclick = function() {
1760 tui.show_help = true;
1763 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1764 const mode = switchEl.id.slice("switch_to_".length);
1765 switchEl.onclick = function() {
1766 tui.switch_mode(mode);
1770 document.getElementById("toggle_tile_draw").onclick = function() {
1771 tui.toggle_tile_draw();
1773 document.getElementById("toggle_map_mode").onclick = function() {
1774 tui.toggle_map_mode();
1777 document.getElementById("flatten").onclick = function() {
1778 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1780 document.getElementById("door").onclick = function() {
1781 server.send(['TASK:DOOR']);
1783 document.getElementById("consume").onclick = function() {
1784 server.send(['TASK:INTOXICATE']);
1786 document.getElementById("install").onclick = function() {
1787 server.send(['TASK:INSTALL', tui.password]);
1789 document.getElementById("wear").onclick = function() {
1790 server.send(['TASK:WEAR']);
1792 document.getElementById("spin").onclick = function() {
1793 server.send(['TASK:SPIN']);
1795 document.getElementById("teleport").onclick = function() {
1798 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1799 if (move_button.id.startsWith('key_')) { // not a move button
1802 let direction = move_button.id.split('_')[2].toUpperCase();
1805 if (tui.mode.available_actions.includes("move")) {
1806 server.send(['TASK:MOVE', direction]);
1807 } else if (tui.mode.available_actions.includes("move_explorer")) {
1808 explorer.move(direction);
1812 move_button.onmousedown = function() {
1814 move_repeat = window.setInterval(move, 100);
1816 move_button.onmouseup = function() {
1817 window.clearInterval(move_repeat);