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 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]);
989 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
992 for (const match of matches) {
993 const url = match[0];
994 const url_start = match.index;
995 const url_end = match.index + match[0].length;
996 link_data[url_start] = url;
997 url_ends.push(url_end);
1001 let inner_links = {};
1002 let in_link = false;
1005 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1006 if (x >= width || msg[i] == "\n") {
1008 push_inner_link(y, chunk.length);
1010 if (url_ends[0] == i) {
1018 if (msg[i] == "\n") {
1023 if (msg[i] != "\n") {
1026 if (i in link_data) {
1030 } else if (url_ends[0] == i) {
1032 push_inner_link(y, x);
1038 push_inner_link(lines.length - 1, chunk.length);
1040 return [lines, inner_links];
1042 log_msg: function(msg) {
1044 while (this.log.length > 100) {
1047 this.full_refresh();
1049 pick_selectable: function(task_name) {
1050 const i = parseInt(this.inputEl.value);
1051 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1052 tui.log_msg('? invalid index, aborted');
1054 server.send(['TASK:' + task_name, tui.selectables[i]]);
1056 this.inputEl.value = "";
1057 this.switch_mode('play');
1059 enter_ascii_art: function(command) {
1060 if (this.inputEl.value.length != 6) {
1061 this.log_msg('? wrong input length, must be 6; try again');
1064 this.log_msg(' ' + this.inputEl.value);
1065 this.full_ascii_draw += this.inputEl.value;
1066 this.ascii_draw_stage += 1;
1067 if (this.ascii_draw_stage < 3) {
1068 this.restore_input_values();
1070 server.send([command, this.full_ascii_draw]);
1071 this.full_ascii_draw = '';
1072 this.ascii_draw_stage = 0;
1073 this.inputEl.value = '';
1074 this.switch_mode('edit');
1077 draw_map: function() {
1078 if (!game.turn_complete && this.map_lines.length == 0) {
1081 if (game.turn_complete) {
1082 let map_lines_split = [];
1084 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1085 if (j == game.map_size[1]) {
1086 map_lines_split.push(line);
1090 if (this.map_mode == 'protections') {
1091 line.push(game.map_control[i] + ' ');
1093 line.push(game.map[i] + ' ');
1096 map_lines_split.push(line);
1097 if (this.map_mode == 'terrain + annotations') {
1098 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1099 const yx = coordinate.split(',')
1100 map_lines_split[yx[0]][yx[1]] = 'A ';
1102 } else if (this.map_mode == 'terrain + things') {
1103 for (const p in game.portals) {
1104 let coordinate = p.split(',')
1105 let original = map_lines_split[coordinate[0]][coordinate[1]];
1106 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1108 let used_positions = [];
1109 function draw_thing(t, used_positions) {
1110 let symbol = game.thing_types[t.type_];
1111 let meta_char = ' ';
1113 meta_char = t.thing_char;
1115 if (used_positions.includes(t.position.toString())) {
1121 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1122 used_positions.push(t.position.toString());
1124 for (const thing_id in game.things) {
1125 let t = game.things[thing_id];
1126 if (t.type_ != 'Player') {
1127 draw_thing(t, used_positions);
1130 for (const thing_id in game.things) {
1131 let t = game.things[thing_id];
1132 if (t.type_ == 'Player') {
1133 draw_thing(t, used_positions);
1137 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1138 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1139 } else if (tui.map_mode != 'terrain + things') {
1140 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1143 if (game.map_geometry == 'Square') {
1144 for (let line_split of map_lines_split) {
1145 this.map_lines.push(line_split.join(''));
1147 } else if (game.map_geometry == 'Hex') {
1149 for (let line_split of map_lines_split) {
1150 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1158 let window_center = [terminal.rows / 2, this.window_width / 2];
1159 let center_position = [game.player.position[0], game.player.position[1]];
1160 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1161 center_position = [explorer.position[0], explorer.position[1]];
1163 center_position[1] = center_position[1] * 2;
1164 this.offset = [center_position[0] - window_center[0],
1165 center_position[1] - window_center[1]]
1166 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1167 this.offset[1] += 1;
1170 let term_y = Math.max(0, -this.offset[0]);
1171 let term_x = Math.max(0, -this.offset[1]);
1172 let map_y = Math.max(0, this.offset[0]);
1173 let map_x = Math.max(0, this.offset[1]);
1174 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1175 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1176 terminal.write(term_y, term_x, to_draw);
1179 draw_face_popup: function() {
1180 const t = game.things[this.draw_face];
1181 if (!t || !t.face) {
1182 this.draw_face = false;
1185 const start_x = tui.window_width - 10;
1188 t_char = t.thing_char;
1190 function draw_body_part(body_part, end_y) {
1191 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1192 terminal.write(end_y - 3, start_x, '| |');
1193 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1194 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1195 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1198 draw_body_part(t.face, terminal.rows - 2);
1201 draw_body_part(t.hat, terminal.rows - 5);
1203 terminal.write(terminal.rows - 1, start_x, '| |');
1205 draw_mode_line: function() {
1206 let help = 'hit [' + this.keys.help + '] for help';
1207 if (this.mode.has_input_prompt) {
1208 help = 'enter /help for help';
1210 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1212 draw_turn_line: function(n) {
1213 if (game.turn_complete) {
1214 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1217 draw_history: function() {
1218 let log_display_lines = [];
1220 let y_offset_in_log = 0;
1221 for (let line of this.log) {
1222 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1224 log_display_lines = log_display_lines.concat(new_lines);
1225 for (const y in link_data) {
1226 const rel_y = y_offset_in_log + parseInt(y);
1227 log_links[rel_y] = [];
1228 for (let link of link_data[y]) {
1229 log_links[rel_y].push(link);
1232 y_offset_in_log += new_lines.length;
1234 let i = log_display_lines.length - 1;
1235 for (let y = terminal.rows - 1 - this.height_input;
1236 y >= this.height_header && i >= 0;
1238 terminal.write(y, this.window_width, log_display_lines[i]);
1240 for (const key of Object.keys(log_links)) {
1241 if (parseInt(key) <= i) {
1242 delete log_links[key];
1245 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1247 this.offset_links(offset, log_links);
1249 draw_info: function() {
1250 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1251 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1252 let offset = [this.height_header, this.window_width];
1253 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1254 terminal.write(y, offset[1], lines[i]);
1256 this.offset_links(offset, link_data);
1258 draw_input: function() {
1259 if (this.mode.has_input_prompt) {
1260 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1261 terminal.write(y, this.window_width, this.input_lines[i]);
1265 draw_help: function() {
1266 let movement_keys_desc = '';
1267 if (!this.mode.is_intro) {
1268 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1270 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1271 if (this.mode.available_actions.length > 0) {
1272 content += "Available actions:\n";
1273 for (let action of this.mode.available_actions) {
1274 if (Object.keys(this.action_tasks).includes(action)) {
1275 if (!this.task_action_on(action)) {
1279 if (action == 'move_explorer') {
1282 if (action == 'move') {
1283 content += "[" + movement_keys_desc + "] – move\n"
1285 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1290 content += this.mode.list_available_modes();
1292 if (!this.mode.has_input_prompt) {
1293 start_x = this.window_width;
1294 this.draw_links = false;
1296 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1297 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1298 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1299 terminal.write(y, start_x, lines[i]);
1302 toggle_tile_draw: function() {
1303 if (tui.tile_draw) {
1304 tui.tile_draw = false;
1306 tui.tile_draw = true;
1309 toggle_map_mode: function() {
1310 if (tui.map_mode == 'terrain only') {
1311 tui.map_mode = 'terrain + annotations';
1312 } else if (tui.map_mode == 'terrain + annotations') {
1313 tui.map_mode = 'terrain + things';
1314 } else if (tui.map_mode == 'terrain + things') {
1315 tui.map_mode = 'protections';
1316 } else if (tui.map_mode == 'protections') {
1317 tui.map_mode = 'terrain only';
1320 full_refresh: function() {
1321 this.draw_links = true;
1323 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1324 this.recalc_input_lines();
1325 if (this.mode.is_intro) {
1326 this.draw_history();
1330 this.draw_turn_line();
1331 this.draw_mode_line();
1332 if (this.mode.shows_info) {
1335 this.draw_history();
1339 if (this.show_help) {
1342 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1343 this.draw_face_popup();
1345 if (!this.draw_links) {
1355 this.player_id = -1;
1358 this.things_new = {};
1363 this.map_control = "";
1364 this.map_control_new = "";
1365 this.map_size = [0,0];
1366 this.map_size_new = [0,0];
1368 this.portals_new = {};
1369 this.players_hat_chars = "";
1371 get_thing_temp: function(id_, create_if_not_found=false) {
1372 if (id_ in game.things_new) {
1373 return game.things_new[id_];
1374 } else if (create_if_not_found) {
1375 let t = new Thing([0,0]);
1376 game.things_new[id_] = t;
1380 get_thing: function(id_, create_if_not_found=false) {
1381 if (id_ in game.things) {
1382 return game.things[id_];
1385 move: function(start_position, direction) {
1386 let target = [start_position[0], start_position[1]];
1387 if (direction == 'LEFT') {
1389 } else if (direction == 'RIGHT') {
1391 } else if (game.map_geometry == 'Square') {
1392 if (direction == 'UP') {
1394 } else if (direction == 'DOWN') {
1397 } else if (game.map_geometry == 'Hex') {
1398 let start_indented = start_position[0] % 2;
1399 if (direction == 'UPLEFT') {
1401 if (!start_indented) {
1404 } else if (direction == 'UPRIGHT') {
1406 if (start_indented) {
1409 } else if (direction == 'DOWNLEFT') {
1411 if (!start_indented) {
1414 } else if (direction == 'DOWNRIGHT') {
1416 if (start_indented) {
1421 if (target[0] < 0 || target[1] < 0 ||
1422 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1427 teleport: function() {
1428 if (game.player.position in this.portals) {
1429 server.reconnect_to(this.portals[game.player.position]);
1431 terminal.blink_screen();
1432 tui.log_msg('? not standing on portal')
1440 server.init(websocket_location);
1445 annotations_new: {},
1447 move: function(direction) {
1448 let target = game.move(this.position, direction);
1450 this.position = target
1451 this.info_cached = false;
1452 if (tui.tile_draw) {
1453 this.send_tile_control_command();
1456 terminal.blink_screen();
1459 get_info: function() {
1460 if (this.info_cached) {
1461 return this.info_cached;
1463 let info_to_cache = '';
1464 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1465 if (game.fov[position_i] != '.') {
1466 info_to_cache += 'outside field of view';
1468 for (let t_id in game.things) {
1469 let t = game.things[t_id];
1470 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1471 info_to_cache += "THING: " + this.get_thing_info(t);
1472 let protection = t.protection;
1473 if (protection == '.') {
1474 protection = 'none';
1476 info_to_cache += " / protection: " + protection + "\n";
1478 info_to_cache += t.hat.slice(0, 6) + '\n';
1479 info_to_cache += t.hat.slice(6, 12) + '\n';
1480 info_to_cache += t.hat.slice(12, 18) + '\n';
1483 info_to_cache += t.face.slice(0, 6) + '\n';
1484 info_to_cache += t.face.slice(6, 12) + '\n';
1485 info_to_cache += t.face.slice(12, 18) + '\n';
1489 let terrain_char = game.map[position_i]
1490 let terrain_desc = '?'
1491 if (game.terrains[terrain_char]) {
1492 terrain_desc = game.terrains[terrain_char];
1494 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1495 let protection = game.map_control[position_i];
1496 if (protection == '.') {
1497 protection = 'unprotected';
1499 info_to_cache += 'PROTECTION: ' + protection + '\n';
1500 if (this.position in game.portals) {
1501 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1503 if (this.position in this.annotations) {
1504 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1507 this.info_cached = info_to_cache;
1508 return this.info_cached;
1510 get_thing_info: function(t) {
1511 const symbol = game.thing_types[t.type_];
1512 let info = t.type_ + " / " + symbol;
1514 info += t.thing_char;
1517 info += " (" + t.name_ + ")";
1520 info += " / installed";
1524 annotate: function(msg) {
1525 if (msg.length == 0) {
1526 msg = " "; // triggers annotation deletion
1528 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1530 set_portal: function(msg) {
1531 if (msg.length == 0) {
1532 msg = " "; // triggers portal deletion
1534 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1536 send_tile_control_command: function() {
1537 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1541 tui.inputEl.addEventListener('input', (event) => {
1542 if (tui.mode.has_input_prompt) {
1543 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1544 if (tui.inputEl.value.length > max_length) {
1545 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1547 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1548 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1549 tui.switch_mode('edit');
1553 document.onclick = function() {
1554 if (!tui.mode.is_single_char_entry) {
1555 tui.show_help = false;
1558 tui.inputEl.addEventListener('keydown', (event) => {
1559 tui.show_help = false;
1560 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1561 event.preventDefault();
1563 if ((!tui.mode.is_intro && event.key == 'Escape')
1564 || (tui.mode.has_input_prompt && event.key == 'Enter'
1565 && tui.inputEl.value.length == 0
1566 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1567 'admin_enter'].includes(tui.mode.name))) {
1568 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1569 tui.log_msg('@ aborted');
1571 tui.switch_mode('play');
1572 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1573 tui.show_help = true;
1574 tui.inputEl.value = "";
1575 tui.restore_input_values();
1576 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1577 && !tui.mode.is_single_char_entry) {
1578 tui.show_help = true;
1579 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1580 tui.login_name = tui.inputEl.value;
1581 server.send(['LOGIN', tui.inputEl.value]);
1582 tui.inputEl.value = "";
1583 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1584 tui.enter_ascii_art('PLAYER_FACE');
1585 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1586 tui.enter_ascii_art('PLAYER_HAT');
1587 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1588 server.send(['TASK:COMMAND', tui.inputEl.value]);
1589 tui.inputEl.value = "";
1590 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1591 tui.pick_selectable('PICK_UP');
1592 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1593 tui.pick_selectable('DROP');
1594 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1595 if (tui.inputEl.value.length == 0) {
1596 tui.log_msg('@ aborted');
1598 server.send(['SET_MAP_CONTROL_PASSWORD',
1599 tui.tile_control_char, tui.inputEl.value]);
1600 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1602 tui.switch_mode('admin');
1603 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1604 explorer.set_portal(tui.inputEl.value);
1605 tui.switch_mode('edit');
1606 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1607 if (tui.inputEl.value.length == 0) {
1608 tui.inputEl.value = " ";
1610 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
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.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 tui.inputEl.focus();
1758 document.getElementById("help").onclick = function() {
1759 tui.show_help = true;
1762 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1763 const mode = switchEl.id.slice("switch_to_".length);
1764 switchEl.onclick = function() {
1765 tui.switch_mode(mode);
1769 document.getElementById("toggle_tile_draw").onclick = function() {
1770 tui.toggle_tile_draw();
1772 document.getElementById("toggle_map_mode").onclick = function() {
1773 tui.toggle_map_mode();
1776 document.getElementById("flatten").onclick = function() {
1777 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1779 document.getElementById("door").onclick = function() {
1780 server.send(['TASK:DOOR']);
1782 document.getElementById("consume").onclick = function() {
1783 server.send(['TASK:INTOXICATE']);
1785 document.getElementById("install").onclick = function() {
1786 server.send(['TASK:INSTALL', tui.password]);
1788 document.getElementById("wear").onclick = function() {
1789 server.send(['TASK:WEAR']);
1791 document.getElementById("spin").onclick = function() {
1792 server.send(['TASK:SPIN']);
1794 document.getElementById("teleport").onclick = function() {
1797 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1798 if (move_button.id.startsWith('key_')) { // not a move button
1801 let direction = move_button.id.split('_')[2].toUpperCase();
1804 if (tui.mode.available_actions.includes("move")) {
1805 server.send(['TASK:MOVE', direction]);
1806 } else if (tui.mode.available_actions.includes("move_explorer")) {
1807 explorer.move(direction);
1811 move_button.onmousedown = function() {
1813 move_repeat = window.setInterval(move, 100);
1815 move_button.onmouseup = function() {
1816 window.clearInterval(move_repeat);