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="dance"></button>
58 <button id="switch_to_command_thing"></button>
59 <button id="teleport"></button>
60 <button id="wear"></button>
61 <button id="spin"></button>
65 <td><button id="switch_to_edit"></button></td>
67 <button id="switch_to_write"></button>
68 <button id="flatten"></button>
69 <button id="install"></button>
70 <button id="switch_to_annotate"></button>
71 <button id="switch_to_portal"></button>
72 <button id="switch_to_name_thing"></button>
73 <button id="switch_to_password"></button>
74 <button id="switch_to_enter_face"></button>
75 <button id="switch_to_enter_design"></button>
79 <td><button id="switch_to_admin_enter"></button></td>
81 <button id="switch_to_control_pw_type"></button>
82 <button id="switch_to_control_tile_type"></button>
83 <button id="switch_to_admin_thing_protect"></button>
84 <button id="toggle_tile_draw"></button>
89 <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 />
91 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
92 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
93 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
94 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
95 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
96 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
97 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
98 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
99 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
100 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
101 <li>help: <input id="key_help" type="text" value="h" />
102 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
103 <li>teleport: <input id="key_teleport" type="text" value="p" />
104 <li>spin: <input id="key_spin" type="text" value="S" />
105 <li>dance: <input id="key_dance" type="text" value="T" />
106 <li>open/close: <input id="key_door" type="text" value="D" />
107 <li>consume: <input id="key_consume" type="text" value="C" />
108 <li>install: <input id="key_install" type="text" value="I" />
109 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
110 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
111 <li><input id="key_switch_to_enter_face" type="text" value="f" />
112 <li><input id="key_switch_to_enter_design" type="text" value="D" />
113 <li><input id="key_switch_to_take_thing" type="text" value="z" />
114 <li><input id="key_switch_to_chat" type="text" value="t" />
115 <li><input id="key_switch_to_play" type="text" value="p" />
116 <li><input id="key_switch_to_study" type="text" value="?" />
117 <li><input id="key_switch_to_edit" type="text" value="E" />
118 <li><input id="key_switch_to_write" type="text" value="m" />
119 <li><input id="key_switch_to_name_thing" type="text" value="N" />
120 <li><input id="key_switch_to_command_thing" type="text" value="O" />
121 <li><input id="key_switch_to_password" type="text" value="P" />
122 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
123 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
124 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
125 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
126 <li><input id="key_switch_to_annotate" type="text" value="M" />
127 <li><input id="key_switch_to_portal" type="text" value="T" />
128 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
129 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
134 let websocket_location = "wss://plomlompom.com/rogue_chat/";
135 //let websocket_location = "ws://localhost:8000/";
141 'long': 'This mode allows you to interact with the map in various ways.'
146 '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.'},
148 'short': 'world edit',
150 '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.'
153 'short': 'name thing',
155 'long': 'Give name to/change name of carried thing.'
160 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
164 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
165 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
169 'intro': 'Enter number of direction to which you want to drop thing.',
170 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
172 'admin_thing_protect': {
173 'short': 'change thing protection',
174 'intro': '@ enter thing protection character:',
175 'long': 'Change protection character for carried thing.'
178 'short': 'edit face',
179 'intro': '@ enter face line:',
180 '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.'
183 'short': 'edit design',
184 'intro': '@ enter design:',
185 'long': 'Enter design for carried thing as ASCII art.'
188 'short': 'edit tile',
190 '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.'
193 'short': 'change protection character password',
194 'intro': '@ enter protection character for which you want to change the password:',
195 '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.'
198 'short': 'change protection character password',
200 '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.'
202 'control_tile_type': {
203 'short': 'change tiles protection',
204 'intro': '@ enter protection character which you want to draw:',
205 '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.'
207 'control_tile_draw': {
208 'short': 'change tiles protection',
210 '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.'
213 'short': 'annotate tile',
215 '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.'
218 'short': 'edit portal',
220 '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.'
225 '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'
230 'long': 'Enter your player name.'
232 'waiting_for_server': {
233 'short': 'waiting for server response',
234 'intro': '@ waiting for server …',
235 'long': 'Waiting for a server response.'
238 'short': 'waiting for server response',
240 'long': 'Waiting for a server response.'
243 'short': 'set world edit password',
245 '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.'
248 'short': 'become admin',
249 'intro': '@ enter admin password:',
250 'long': 'This mode allows you to become admin if you know an admin password.'
255 'long': 'This mode allows you access to actions limited to administrators.'
258 let key_descriptions = {
260 'flatten': 'flatten surroundings',
261 'teleport': 'teleport',
262 'door': 'open/close',
263 'consume': 'consume',
264 'install': '(un-)install',
268 'toggle_map_mode': 'toggle map view',
269 'toggle_tile_draw': 'toggle protection character drawing',
270 'hex_move_upleft': 'up-left',
271 'hex_move_upright': 'up-right',
272 'hex_move_right': 'right',
273 'hex_move_left': 'left',
274 'hex_move_downleft': 'down-left',
275 'hex_move_downright': 'down-right',
276 'square_move_up': 'up',
277 'square_move_left': 'left',
278 'square_move_down': 'down',
279 'square_move_right': 'right',
281 for (const mode_name of Object.keys(mode_helps)) {
282 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
285 let rows_selector = document.getElementById("n_rows");
286 let cols_selector = document.getElementById("n_cols");
287 let key_selectors = document.querySelectorAll('[id^="key_"]');
289 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
290 const action = key_switch_selector.id.slice("key_switch_to_".length);
291 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
294 function restore_selector_value(selector) {
295 let stored_selection = window.localStorage.getItem(selector.id);
296 if (stored_selection) {
297 selector.value = stored_selection;
300 restore_selector_value(rows_selector);
301 restore_selector_value(cols_selector);
302 for (let key_selector of key_selectors) {
303 restore_selector_value(key_selector);
306 function escapeHTML(str) {
308 replace(/&/g, '&').
309 replace(/</g, '<').
310 replace(/>/g, '>').
311 replace(/'/g, ''').
312 replace(/"/g, '"');
316 initialize: function() {
317 this.rows = rows_selector.value;
318 this.cols = cols_selector.value;
319 this.pre_el = document.getElementById("terminal");
320 this.set_default_colors();
324 for (let y = 0, x = 0; y <= this.rows; x++) {
325 if (x == this.cols) {
328 this.content.push(line);
330 if (y == this.rows) {
337 apply_colors: function() {
338 this.pre_el.style.color = this.foreground;
339 this.pre_el.style.backgroundColor = this.background;
341 set_default_colors: function() {
342 this.foreground = 'white';
343 this.background = 'black';
346 set_random_colors: function() {
347 function rand(offset) {
348 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
350 this.foreground = '#' + rand(159) + rand(159) + rand(159);
351 this.background = '#' + rand(0) + rand(0) + rand(0);
354 blink_screen: function() {
355 this.pre_el.style.color = this.background;
356 this.pre_el.style.backgroundColor = this.foreground;
358 this.pre_el.style.color = this.foreground;
359 this.pre_el.style.backgroundColor = this.background;
362 refresh: function() {
363 let pre_content = '';
364 for (let y = 0; y < this.rows; y++) {
365 let line = this.content[y].join('');
367 if (y in tui.links) {
369 for (let span of tui.links[y]) {
370 chunks.push(escapeHTML(line.slice(start_x, span[0])));
371 chunks.push('<a target="_blank" href="');
372 chunks.push(escapeHTML(span[2]));
374 chunks.push(escapeHTML(line.slice(span[0], span[1])));
378 chunks.push(escapeHTML(line.slice(start_x)));
380 chunks = [escapeHTML(line)];
382 for (const chunk of chunks) {
383 pre_content += chunk;
387 this.pre_el.innerHTML = pre_content;
389 write: function(start_y, start_x, msg) {
390 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
391 this.content[start_y][x] = msg[i];
394 drawBox: function(start_y, start_x, height, width) {
395 let end_y = start_y + height;
396 let end_x = start_x + width;
397 for (let y = start_y, x = start_x; y < this.rows; x++) {
405 this.content[y][x] = ' ';
409 terminal.initialize();
412 tokenize: function(str) {
417 for (let i = 0; i < str.length; i++) {
423 } else if (c == '\\') {
425 } else if (c == '"') {
430 } else if (c == '"') {
432 } else if (c === ' ') {
433 if (token.length > 0) {
441 if (token.length > 0) {
446 parse_yx: function(position_string) {
447 let coordinate_strings = position_string.split(',')
448 let position = [0, 0];
449 position[0] = parseInt(coordinate_strings[0].slice(2));
450 position[1] = parseInt(coordinate_strings[1].slice(2));
462 init: function(url) {
464 this.websocket = new WebSocket(this.url);
465 this.websocket.onopen = function(event) {
466 game.thing_types = {};
468 tui.is_admin = false;
469 server.send(['TASKS']);
470 server.send(['TERRAINS']);
471 server.send(['THING_TYPES']);
472 tui.log_msg("@ server connected! :)");
473 tui.switch_mode('login');
475 this.websocket.onclose = function(event) {
476 tui.switch_mode('waiting_for_server');
477 tui.log_msg("@ server disconnected :(");
479 this.websocket.onmessage = this.handle_event;
481 reconnect_to: function(url) {
482 this.websocket.close();
485 send: function(tokens) {
486 this.websocket.send(unparser.untokenize(tokens));
488 handle_event: function(event) {
489 let tokens = parser.tokenize(event.data);
490 if (tokens[0] === 'TURN') {
491 game.turn_complete = false;
492 } else if (tokens[0] === 'OTHER_WIPE') {
493 game.portals_new = {};
494 explorer.annotations_new = {};
495 game.things_new = [];
496 } else if (tokens[0] === 'STATS') {
497 game.bladder_pressure_new = parseInt(tokens[1])
498 game.energy_new = parseInt(tokens[2])
499 } else if (tokens[0] === 'THING') {
500 let t = game.get_thing_temp(tokens[4], true);
501 t.position = parser.parse_yx(tokens[1]);
503 t.protection = tokens[3];
504 t.portable = parseInt(tokens[5]);
505 t.commandable = parseInt(tokens[6]);
506 } else if (tokens[0] === 'THING_NAME') {
507 let t = game.get_thing_temp(tokens[1]);
509 } else if (tokens[0] === 'THING_FACE') {
510 let t = game.get_thing_temp(tokens[1]);
512 } else if (tokens[0] === 'THING_HAT') {
513 let t = game.get_thing_temp(tokens[1]);
515 } else if (tokens[0] === 'THING_DESIGN') {
516 let t = game.get_thing_temp(tokens[1]);
517 t.design = [parser.parse_yx(tokens[2]), tokens[3]];
518 } else if (tokens[0] === 'THING_CHAR') {
519 let t = game.get_thing_temp(tokens[1]);
520 t.thing_char = tokens[2];
521 } else if (tokens[0] === 'TASKS') {
522 game.tasks = tokens[1].split(',');
523 tui.mode_write.legal = game.tasks.includes('WRITE');
524 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
525 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
526 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
527 } else if (tokens[0] === 'THING_TYPE') {
528 game.thing_types[tokens[1]] = tokens[2]
529 } else if (tokens[0] === 'THING_CARRYING') {
530 let t = game.get_thing_temp(tokens[1]);
531 t.carrying = game.get_thing_temp(tokens[2], false);
532 } else if (tokens[0] === 'THING_INSTALLED') {
533 let t = game.get_thing_temp(tokens[1]);
535 } else if (tokens[0] === 'TERRAIN') {
536 game.terrains[tokens[1]] = tokens[2]
537 } else if (tokens[0] === 'MAP') {
538 game.map_geometry_new = tokens[1];
539 game.map_size_new = parser.parse_yx(tokens[2]);
540 game.map_new = tokens[3]
541 } else if (tokens[0] === 'FOV') {
542 game.fov_new = tokens[1]
543 } else if (tokens[0] === 'MAP_CONTROL') {
544 game.map_control_new = tokens[1]
545 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
546 game.portals = game.portals_new;
547 game.map_geometry = game.map_geometry_new;
548 game.map_size = game.map_size_new;
549 game.map = game.map_new;
550 game.fov = game.fov_new;
552 game.map_control = game.map_control_new;
553 explorer.annotations = explorer.annotations_new;
554 explorer.info_cached = false;
555 game.things = game.things_new;
556 game.player = game.things[game.player_id];
557 game.players_hat_chars = game.players_hat_chars_new;
558 game.bladder_pressure = game.bladder_pressure_new
559 game.energy = game.energy_new
560 game.turn_complete = true;
561 if (tui.mode.name == 'post_login_wait') {
562 tui.switch_mode('play');
566 } else if (tokens[0] === 'CHAT') {
567 tui.log_msg('# ' + tokens[1], 1);
568 } else if (tokens[0] === 'CHATFACE') {
569 tui.draw_face = tokens[1];
571 } else if (tokens[0] === 'REPLY') {
572 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
573 } else if (tokens[0] === 'PLAYER_ID') {
574 game.player_id = parseInt(tokens[1]);
575 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
576 game.players_hat_chars_new = tokens[1];
577 } else if (tokens[0] === 'LOGIN_OK') {
578 this.send(['GET_GAMESTATE']);
579 tui.switch_mode('post_login_wait');
580 tui.log_msg('@ welcome!')
581 } else if (tokens[0] === 'DEFAULT_COLORS') {
582 terminal.set_default_colors();
583 } else if (tokens[0] === 'RANDOM_COLORS') {
584 terminal.set_random_colors();
585 } else if (tokens[0] === 'ADMIN_OK') {
587 tui.log_msg('@ you now have admin rights');
588 tui.switch_mode('admin');
589 } else if (tokens[0] === 'PORTAL') {
590 let position = parser.parse_yx(tokens[1]);
591 game.portals_new[position] = tokens[2];
592 } else if (tokens[0] === 'ANNOTATION') {
593 let position = parser.parse_yx(tokens[1]);
594 explorer.annotations_new[position] = tokens[2];
595 } else if (tokens[0] === 'UNHANDLED_INPUT') {
596 tui.log_msg('? unknown command');
597 } else if (tokens[0] === 'PLAY_ERROR') {
598 tui.log_msg('? ' + tokens[1]);
599 terminal.blink_screen();
600 } else if (tokens[0] === 'ARGUMENT_ERROR') {
601 tui.log_msg('? syntax error: ' + tokens[1]);
602 } else if (tokens[0] === 'GAME_ERROR') {
603 tui.log_msg('? game error: ' + tokens[1]);
604 } else if (tokens[0] === 'PONG') {
607 tui.log_msg('? unhandled input: ' + event.data);
613 quote: function(str) {
615 for (let i = 0; i < str.length; i++) {
617 if (['"', '\\'].includes(c)) {
623 return quoted.join('');
625 to_yx: function(yx_coordinate) {
626 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
628 untokenize: function(tokens) {
629 let quoted_tokens = [];
630 for (let token of tokens) {
631 quoted_tokens.push(this.quote(token));
633 return quoted_tokens.join(" ");
638 constructor(name, has_input_prompt=false, shows_info=false,
639 is_intro=false, is_single_char_entry=false) {
641 this.short_desc = mode_helps[name].short;
642 this.available_modes = [];
643 this.available_actions = [];
644 this.has_input_prompt = has_input_prompt;
645 this.shows_info= shows_info;
646 this.is_intro = is_intro;
647 this.help_intro = mode_helps[name].long;
648 this.intro_msg = mode_helps[name].intro;
649 this.is_single_char_entry = is_single_char_entry;
652 *iter_available_modes() {
653 for (let mode_name of this.available_modes) {
654 let mode = tui['mode_' + mode_name];
658 let key = tui.keys['switch_to_' + mode.name];
662 list_available_modes() {
664 if (this.available_modes.length > 0) {
665 msg += 'Other modes available from here:\n';
666 for (let [mode, key] of this.iter_available_modes()) {
667 msg += '[' + key + '] – ' + mode.short_desc + '\n';
672 mode_switch_on_key(key_event) {
673 for (let [mode, key] of this.iter_available_modes()) {
674 if (key_event.key == key) {
675 event.preventDefault();
676 tui.switch_mode(mode.name);
695 mode_waiting_for_server: new Mode('waiting_for_server',
697 mode_login: new Mode('login', true, false, true),
698 mode_post_login_wait: new Mode('post_login_wait'),
699 mode_chat: new Mode('chat', true),
700 mode_annotate: new Mode('annotate', true, true),
701 mode_play: new Mode('play'),
702 mode_study: new Mode('study', false, true),
703 mode_write: new Mode('write', false, false, false, true),
704 mode_edit: new Mode('edit'),
705 mode_control_pw_type: new Mode('control_pw_type', true),
706 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
707 mode_portal: new Mode('portal', true, true),
708 mode_password: new Mode('password', true),
709 mode_name_thing: new Mode('name_thing', true, true),
710 mode_command_thing: new Mode('command_thing', true),
711 mode_take_thing: new Mode('take_thing', true),
712 mode_drop_thing: new Mode('drop_thing', true),
713 mode_enter_face: new Mode('enter_face', true),
714 mode_enter_design: new Mode('enter_design', true),
715 mode_admin_enter: new Mode('admin_enter', true),
716 mode_admin: new Mode('admin'),
717 mode_control_pw_pw: new Mode('control_pw_pw', true),
718 mode_control_tile_type: new Mode('control_tile_type', true),
719 mode_control_tile_draw: new Mode('control_tile_draw'),
721 'flatten': 'FLATTEN_SURROUNDINGS',
722 'take_thing': 'PICK_UP',
723 'drop_thing': 'DROP',
726 'install': 'INSTALL',
728 'command': 'COMMAND',
729 'consume': 'INTOXICATE',
740 this.reset_screen_size();
741 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
742 "command_thing", "take_thing", "drop_thing"]
743 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
744 "wear", "spin", "dance"];
745 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
746 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
747 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
748 "control_tile_type", "chat",
749 "study", "play", "edit"]
750 this.mode_admin.available_actions = ["move", "toggle_map_mode"];
751 this.mode_control_tile_draw.available_modes = ["admin_enter"]
752 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
753 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
754 "enter_design", "password", "chat", "study",
755 "play", "admin_enter", "enter_face"]
756 this.mode_edit.available_actions = ["move", "flatten", "install",
758 this.inputEl = document.getElementById("input");
759 this.switch_mode('waiting_for_server');
760 this.recalc_input_lines();
761 this.height_header = this.height_turn_line + this.height_mode_line;
764 reset_screen_size: function() {
765 this.left_window_width = Math.min(52, terminal.cols / 2);
766 this.right_window_width = terminal.cols - tui.left_window_width;
768 init_keys: function() {
769 document.getElementById("move_table").hidden = true;
771 for (let key_selector of key_selectors) {
772 this.keys[key_selector.id.slice(4)] = key_selector.value;
774 this.movement_keys = {};
775 let geometry_prefix = 'undefinedMapGeometry_';
776 if (game.map_geometry) {
777 geometry_prefix = game.map_geometry.toLowerCase() + '_';
779 for (const key_name of Object.keys(key_descriptions)) {
780 if (key_name.startsWith(geometry_prefix)) {
781 let direction = key_name.split('_')[2].toUpperCase();
782 let key = this.keys[key_name];
783 this.movement_keys[key] = direction;
786 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
787 if (move_button.id.startsWith('key_')) {
790 move_button.hidden = true;
792 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
793 document.getElementById("move_table").hidden = false;
794 move_button.hidden = false;
796 for (let el of document.getElementsByTagName("button")) {
797 let action_desc = key_descriptions[el.id];
798 let action_key = '[' + this.keys[el.id] + ']';
799 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
802 task_action_on: function(action) {
803 return game.tasks.includes(this.action_tasks[action]);
805 switch_mode: function(mode_name) {
807 function fail(msg, return_mode='play') {
808 tui.log_msg('? ' + msg);
809 terminal.blink_screen();
810 tui.switch_mode(return_mode);
813 if (this.mode && this.mode.name == 'control_tile_draw') {
814 tui.log_msg('@ finished tile protection drawing.')
816 this.draw_face = false;
817 this.tile_draw = false;
818 this.ascii_draw_stage = 0;
819 this.full_ascii_draw = '';
820 if (mode_name == 'command_thing' && (!game.player.carrying
821 || !game.player.carrying.commandable)) {
822 return fail('not carrying anything commandable');
823 } else if (mode_name == 'name_thing' && !game.player.carrying) {
824 return fail('not carrying anything to re-name', 'edit');
825 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
826 return fail('not carrying anything to protect')
827 } else if (mode_name == 'take_thing' && game.player.carrying) {
828 return fail('already carrying something');
829 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
830 return fail('not carrying anything droppable');
831 } else if (mode_name == 'enter_design' && (!game.player.carrying
832 || !game.player.carrying.design)) {
833 return fail('not carrying designable to edit', 'edit');
835 if (mode_name == 'admin_enter' && this.is_admin) {
838 this.mode = this['mode_' + mode_name];
839 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
840 this.map_mode = 'protections';
841 } else if (this.mode.name != "edit") {
842 this.map_mode = 'terrain + things';
844 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
845 explorer.position = game.player.position;
847 this.inputEl.value = "";
848 this.restore_input_values();
849 for (let el of document.getElementsByTagName("button")) {
852 document.getElementById("help").disabled = false;
853 for (const action of this.mode.available_actions) {
854 if (["move", "move_explorer"].includes(action)) {
855 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
856 move_key.disabled = false;
858 } else if (Object.keys(this.action_tasks).includes(action)) {
859 if (this.task_action_on(action)) {
860 document.getElementById(action).disabled = false;
863 document.getElementById(action).disabled = false;
866 for (const mode_name of this.mode.available_modes) {
867 document.getElementById('switch_to_' + mode_name).disabled = false;
869 if (this.mode.intro_msg.length > 0) {
870 this.log_msg(this.mode.intro_msg);
872 if (this.mode.name == 'login') {
873 if (this.login_name) {
874 server.send(['LOGIN', this.login_name]);
876 this.log_msg("? need login name");
878 } else if (this.mode.is_single_char_entry) {
879 this.show_help = true;
880 } else if (this.mode.name == 'take_thing') {
881 this.log_msg("Portable things in reach for pick-up:");
882 const y = game.player.position[0]
883 const x = game.player.position[1]
884 let directed_moves = {
885 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
887 if (game.map_geometry == 'Square') {
888 directed_moves['UP'] = [-1, 0];
889 directed_moves['DOWN'] = [1, 0];
890 } else if (game.map_geometry == 'Hex') {
892 directed_moves['UPLEFT'] = [-1, 0];
893 directed_moves['UPRIGHT'] = [-1, 1];
894 directed_moves['DOWNLEFT'] = [1, 0];
895 directed_moves['DOWNRIGHT'] = [1, 1];
897 directed_moves['UPLEFT'] = [-1, -1];
898 directed_moves['UPRIGHT'] = [-1, 0];
899 directed_moves['DOWNLEFT'] = [1, -1];
900 directed_moves['DOWNRIGHT'] = [1, 0];
903 let select_range = {};
904 for (const direction in directed_moves) {
905 const move = directed_moves[direction];
906 select_range[direction] = [y + move[0], x + move[1]];
908 this.selectables = [];
910 for (const direction in select_range) {
911 for (const t_id in game.things) {
912 const t = game.things[t_id];
913 const position = select_range[direction];
915 && t.position[0] == position[0]
916 && t.position[1] == position[1]) {
917 this.selectables.push(t_id);
918 directions.push(direction);
922 if (this.selectables.length == 0) {
923 this.log_msg('none');
924 terminal.blink_screen();
925 this.switch_mode('play');
928 for (let [i, t_id] of this.selectables.entries()) {
929 const t = game.things[t_id];
930 const direction = directions[i];
931 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
934 } else if (this.mode.name == 'drop_thing') {
935 this.log_msg('Direction to drop thing to:');
936 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
937 for (let [i, direction] of this.selectables.entries()) {
938 this.log_msg(i + ': ' + direction);
940 } else if (this.mode.name == 'enter_design') {
941 if (game.player.carrying.type_ == 'Hat') {
942 this.log_msg('@ The design you enter must be '
943 + game.player.carrying.design[0][0] + ' lines of max '
944 + game.player.carrying.design[0][1] + ' characters width each');
945 this.log_msg('@ Legal characters: ' + game.players_hat_chars);
946 this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
948 this.log_msg('@ Width of first line determines maximum width for remaining design')
949 this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
951 } else if (this.mode.name == 'command_thing') {
952 server.send(['TASK:COMMAND', 'HELP']);
953 } else if (this.mode.name == 'control_pw_pw') {
954 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
955 } else if (this.mode.name == 'control_tile_draw') {
956 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 + '].')
960 offset_links: function(offset, links) {
961 for (let y in links) {
962 let real_y = offset[0] + parseInt(y);
963 if (!this.links[real_y]) {
964 this.links[real_y] = [];
966 for (let link of links[y]) {
967 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
968 this.links[real_y].push(offset_link);
972 restore_input_values: function() {
973 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
974 let info = explorer.annotations[explorer.position];
975 if (info != "(none)") {
976 this.inputEl.value = info;
978 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
979 let portal = game.portals[explorer.position]
980 this.inputEl.value = portal;
981 } else if (this.mode.name == 'password') {
982 this.inputEl.value = this.password;
983 } else if (this.mode.name == 'name_thing') {
984 if (game.player.carrying && game.player.carrying.name_) {
985 this.inputEl.value = game.player.carrying.name_;
987 } else if (this.mode.name == 'admin_thing_protect') {
988 if (game.player.carrying && game.player.carrying.protection) {
989 this.inputEl.value = game.player.carrying.protection;
991 } else if (this.mode.name == 'enter_face') {
992 const start = this.ascii_draw_stage * 6;
993 const end = (this.ascii_draw_stage + 1) * 6;
994 this.inputEl.value = game.player.face.slice(start, end);
995 } else if (this.mode.name == 'enter_design') {
996 const width = game.player.carrying.design[0][1];
997 const start = this.ascii_draw_stage * width;
998 const end = (this.ascii_draw_stage + 1) * width;
999 this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1002 recalc_input_lines: function() {
1003 if (this.mode.has_input_prompt) {
1005 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1007 this.input_lines = [];
1009 this.height_input = this.input_lines.length;
1011 msg_into_lines_of_width: function(msg, width) {
1012 function push_inner_link(y, end_x) {
1013 if (!inner_links[y]) {
1014 inner_links[y] = [];
1016 inner_links[y].push([url_start_x, end_x, url]);
1020 const regexp = RegExp('https?://[^\\s]+', 'g');
1022 while ((match = regexp.exec(msg)) !== null) {
1023 const url = match[0];
1024 const url_start = match.index;
1025 const url_end = match.index + match[0].length;
1026 link_data[url_start] = url;
1027 url_ends.push(url_end);
1029 let url_start_x = 0;
1031 let inner_links = {};
1032 let in_link = false;
1035 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1036 if (x >= width || msg[i] == "\n") {
1038 push_inner_link(y, chunk.length);
1040 if (url_ends[0] == i) {
1048 if (msg[i] == "\n") {
1053 if (msg[i] != "\n") {
1056 if (i in link_data) {
1060 } else if (url_ends[0] == i) {
1062 push_inner_link(y, x);
1068 push_inner_link(lines.length - 1, chunk.length);
1070 return [lines, inner_links];
1072 log_msg: function(msg) {
1074 while (this.log.length > 100) {
1077 this.full_refresh();
1079 pick_selectable: function(task_name) {
1080 const i = parseInt(this.inputEl.value);
1081 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1082 tui.log_msg('? invalid index, aborted');
1084 server.send(['TASK:' + task_name, tui.selectables[i]]);
1086 this.inputEl.value = "";
1087 this.switch_mode('play');
1089 enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1090 if (with_size && this.ascii_draw_stage == 0) {
1091 width = this.inputEl.value.length;
1093 this.log_msg('? wrong input length, must be max 36; try again');
1096 if (width != game.player.carrying.design[0][1]) {
1097 game.player.carrying.design[1] = '';
1098 game.player.carrying.design[0][1] = width;
1100 } else if (this.inputEl.value.length > width) {
1101 this.log_msg('? wrong input length, must be max ' + width + '; try again');
1104 this.log_msg(' ' + this.inputEl.value);
1105 if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1106 height = this.ascii_draw_stage;
1109 height = this.ascii_draw_stage + 2;
1111 while (this.inputEl.value.length < width) {
1112 this.inputEl.value += ' ';
1114 this.full_ascii_draw += this.inputEl.value;
1117 game.player.carrying.design[0][0] = height;
1119 this.ascii_draw_stage += 1;
1120 if (this.ascii_draw_stage < height) {
1121 this.restore_input_values();
1123 if (with_pw && with_size) {
1124 server.send([command + '_SIZE',
1125 unparser.to_yx(game.player.carrying.design[0]),
1129 server.send([command, this.full_ascii_draw, this.password]);
1131 server.send([command, this.full_ascii_draw]);
1133 this.full_ascii_draw = '';
1134 this.ascii_draw_stage = 0;
1135 this.inputEl.value = '';
1136 this.switch_mode('edit');
1139 draw_map: function() {
1140 if (!game.turn_complete && this.map_lines.length == 0) {
1143 if (game.turn_complete) {
1144 let map_lines_split = [];
1146 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1147 if (j == game.map_size[1]) {
1148 map_lines_split.push(line);
1152 if (this.map_mode == 'protections') {
1153 line.push(game.map_control[i] + ' ');
1155 line.push(game.map[i] + ' ');
1158 map_lines_split.push(line);
1159 if (this.map_mode == 'terrain + annotations') {
1160 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1161 const yx = coordinate.split(',')
1162 map_lines_split[yx[0]][yx[1]] = 'A ';
1164 } else if (this.map_mode == 'terrain + things') {
1165 for (const p in game.portals) {
1166 let coordinate = p.split(',')
1167 let original = map_lines_split[coordinate[0]][coordinate[1]];
1168 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1170 let used_positions = [];
1171 function draw_thing(t, used_positions) {
1172 let symbol = game.thing_types[t.type_];
1173 let meta_char = ' ';
1175 meta_char = t.thing_char;
1177 if (used_positions.includes(t.position.toString())) {
1183 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1184 used_positions.push(t.position.toString());
1186 for (const thing_id in game.things) {
1187 let t = game.things[thing_id];
1188 if (t.type_ != 'Player') {
1189 draw_thing(t, used_positions);
1192 for (const thing_id in game.things) {
1193 let t = game.things[thing_id];
1194 if (t.type_ == 'Player') {
1195 draw_thing(t, used_positions);
1199 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1200 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1201 } else if (tui.map_mode != 'terrain + things') {
1202 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1205 if (game.map_geometry == 'Square') {
1206 for (let line_split of map_lines_split) {
1207 this.map_lines.push(line_split.join(''));
1209 } else if (game.map_geometry == 'Hex') {
1211 for (let line_split of map_lines_split) {
1212 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1220 let window_center = [terminal.rows / 2, this.left_window_width / 2];
1221 let center_position = [game.player.position[0], game.player.position[1]];
1222 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1223 center_position = [explorer.position[0], explorer.position[1]];
1225 center_position[1] = center_position[1] * 2;
1226 this.offset = [center_position[0] - window_center[0],
1227 center_position[1] - window_center[1]]
1228 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1229 this.offset[1] += 1;
1232 let term_y = Math.max(0, -this.offset[0]);
1233 let term_x = Math.max(0, -this.offset[1]);
1234 let map_y = Math.max(0, this.offset[0]);
1235 let map_x = Math.max(0, this.offset[1]);
1236 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1237 let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1238 terminal.write(term_y, term_x, to_draw);
1241 draw_face_popup: function() {
1242 const t = game.things[this.draw_face];
1243 if (!t || !t.face) {
1244 this.draw_face = false;
1247 const start_x = tui.left_window_width - 10;
1248 function draw_body_part(body_part, end_y) {
1249 terminal.write(end_y - 3, start_x, '----------');
1250 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1251 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1252 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1255 draw_body_part(t.face, terminal.rows - 3);
1258 draw_body_part(t.hat, terminal.rows - 6);
1260 terminal.write(terminal.rows - 2, start_x, '----------');
1262 if (name.length > 6) {
1263 name = name.slice(0, 6) + '…';
1265 terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1267 draw_mode_line: function() {
1268 let help = 'hit [' + this.keys.help + '] for help';
1269 if (this.mode.has_input_prompt) {
1270 help = 'enter /help for help';
1272 terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1274 draw_stats_line: function(n) {
1275 terminal.write(0, this.left_window_width,
1276 'ENERGY: ' + game.energy +
1277 ' BLADDER: ' + game.bladder_pressure);
1279 draw_history: function() {
1280 let log_display_lines = [];
1282 let y_offset_in_log = 0;
1283 for (let line of this.log) {
1284 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1285 this.right_window_width)
1286 log_display_lines = log_display_lines.concat(new_lines);
1287 for (const y in link_data) {
1288 const rel_y = y_offset_in_log + parseInt(y);
1289 log_links[rel_y] = [];
1290 for (let link of link_data[y]) {
1291 log_links[rel_y].push(link);
1294 y_offset_in_log += new_lines.length;
1296 let i = log_display_lines.length - 1;
1297 for (let y = terminal.rows - 1 - this.height_input;
1298 y >= this.height_header && i >= 0;
1300 terminal.write(y, this.left_window_width, log_display_lines[i]);
1302 for (const key of Object.keys(log_links)) {
1303 if (parseInt(key) <= i) {
1304 delete log_links[key];
1307 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1308 this.left_window_width];
1309 this.offset_links(offset, log_links);
1311 draw_info: function() {
1312 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1313 let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1314 let offset = [this.height_header, this.left_window_width];
1315 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1316 terminal.write(y, offset[1], lines[i]);
1318 this.offset_links(offset, link_data);
1320 draw_input: function() {
1321 if (this.mode.has_input_prompt) {
1322 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1323 terminal.write(y, this.left_window_width, this.input_lines[i]);
1327 draw_help: function() {
1328 let movement_keys_desc = '';
1329 if (!this.mode.is_intro) {
1330 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1332 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1333 if (this.mode.available_actions.length > 0) {
1334 content += "Available actions:\n";
1335 for (let action of this.mode.available_actions) {
1336 if (Object.keys(this.action_tasks).includes(action)) {
1337 if (!this.task_action_on(action)) {
1341 if (action == 'move_explorer') {
1344 if (action == 'move') {
1345 content += "[" + movement_keys_desc + "] – move\n"
1347 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1352 content += this.mode.list_available_modes();
1356 if (!this.mode.has_input_prompt) {
1357 start_x = this.left_window_width;
1358 this.draw_links = false;
1359 terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1360 [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1363 terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1364 [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1366 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1367 terminal.write(y, start_x, lines[i]);
1370 toggle_tile_draw: function() {
1371 if (tui.tile_draw) {
1372 tui.tile_draw = false;
1374 tui.tile_draw = true;
1377 toggle_map_mode: function() {
1378 if (tui.map_mode == 'terrain only') {
1379 tui.map_mode = 'terrain + annotations';
1380 } else if (tui.map_mode == 'terrain + annotations') {
1381 tui.map_mode = 'terrain + things';
1382 } else if (tui.map_mode == 'terrain + things') {
1383 tui.map_mode = 'protections';
1384 } else if (tui.map_mode == 'protections') {
1385 tui.map_mode = 'terrain only';
1388 full_refresh: function() {
1389 this.draw_links = true;
1391 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1392 this.recalc_input_lines();
1393 if (this.mode.is_intro) {
1394 this.draw_history();
1398 this.draw_stats_line();
1399 this.draw_mode_line();
1400 if (this.mode.shows_info) {
1403 this.draw_history();
1407 if (this.show_help) {
1410 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1411 this.draw_face_popup();
1413 if (!this.draw_links) {
1423 this.player_id = -1;
1426 this.things_new = {};
1431 this.map_control = "";
1432 this.map_control_new = "";
1433 this.map_size = [0,0];
1434 this.map_size_new = [0,0];
1436 this.portals_new = {};
1437 this.players_hat_chars = "";
1438 this.bladder_pressure = 0;
1439 this.bladder_pressure_new = 0;
1441 get_thing_temp: function(id_, create_if_not_found=false) {
1442 if (id_ in game.things_new) {
1443 return game.things_new[id_];
1444 } else if (create_if_not_found) {
1445 let t = new Thing([0,0]);
1446 game.things_new[id_] = t;
1450 get_thing: function(id_, create_if_not_found=false) {
1451 if (id_ in game.things) {
1452 return game.things[id_];
1455 move: function(start_position, direction) {
1456 let target = [start_position[0], start_position[1]];
1457 if (direction == 'LEFT') {
1459 } else if (direction == 'RIGHT') {
1461 } else if (game.map_geometry == 'Square') {
1462 if (direction == 'UP') {
1464 } else if (direction == 'DOWN') {
1467 } else if (game.map_geometry == 'Hex') {
1468 let start_indented = start_position[0] % 2;
1469 if (direction == 'UPLEFT') {
1471 if (!start_indented) {
1474 } else if (direction == 'UPRIGHT') {
1476 if (start_indented) {
1479 } else if (direction == 'DOWNLEFT') {
1481 if (!start_indented) {
1484 } else if (direction == 'DOWNRIGHT') {
1486 if (start_indented) {
1491 if (target[0] < 0 || target[1] < 0 ||
1492 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1497 teleport: function() {
1498 if (game.player.position in this.portals) {
1499 server.reconnect_to(this.portals[game.player.position]);
1501 terminal.blink_screen();
1502 tui.log_msg('? not standing on portal')
1510 server.init(websocket_location);
1515 annotations_new: {},
1517 move: function(direction) {
1518 let target = game.move(this.position, direction);
1520 this.position = target
1521 this.info_cached = false;
1522 if (tui.tile_draw) {
1523 this.send_tile_control_command();
1526 terminal.blink_screen();
1529 get_info: function() {
1530 if (this.info_cached) {
1531 return this.info_cached;
1533 let info_to_cache = '';
1534 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1535 if (game.fov[position_i] != '.') {
1536 info_to_cache += 'outside field of view';
1538 for (let t_id in game.things) {
1539 let t = game.things[t_id];
1540 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1541 info_to_cache += this.get_thing_info(t, true);
1544 let terrain_char = game.map[position_i]
1545 let terrain_desc = '?'
1546 if (game.terrains[terrain_char]) {
1547 terrain_desc = game.terrains[terrain_char];
1549 info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1550 let protection = game.map_control[position_i];
1551 if (protection != '.') {
1552 info_to_cache += '/protection:' + protection;
1554 info_to_cache += ')\n';
1555 if (this.position in game.portals) {
1556 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1558 if (this.position in this.annotations) {
1559 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1562 this.info_cached = info_to_cache;
1563 return this.info_cached;
1565 get_thing_info: function(t, detailed=false) {
1570 info += game.thing_types[t.type_];
1572 info += t.thing_char;
1575 info += ": " + t.name_;
1577 info += ' (' + t.type_;
1579 info += "/installed";
1581 if (t.type_ == 'Bottle') {
1582 if (t.thing_char == '_') {
1584 } else if (t.thing_char == '~') {
1589 const protection = t.protection;
1590 if (protection != '.') {
1591 info += '/protection:' + protection;
1594 if (t.hat || t.face) {
1595 info += '----------\n';
1598 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1599 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1600 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1603 info += '| ' + t.face.slice(0, 6) + ' |\n';
1604 info += '| ' + t.face.slice(6, 12) + ' |\n';
1605 info += '| ' + t.face.slice(12, 18) + ' |\n';
1606 info += '----------\n';
1609 const line_length = t.design[0][1];
1610 info += '-'.repeat(line_length + 4) + '\n';
1612 if (line_length > 0) {
1613 const regexp = RegExp('.{1,' + line_length + '}', 'g');
1614 lines = t.design[1].match(regexp);
1616 for (const line of lines) {
1617 info += '| ' + line + ' |\n';
1619 info += '-'.repeat(line_length + 4) + '\n';
1626 annotate: function(msg) {
1627 if (msg.length == 0) {
1628 msg = " "; // triggers annotation deletion
1630 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1632 set_portal: function(msg) {
1633 if (msg.length == 0) {
1634 msg = " "; // triggers portal deletion
1636 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1638 send_tile_control_command: function() {
1639 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1643 tui.inputEl.addEventListener('input', (event) => {
1644 if (tui.mode.has_input_prompt) {
1645 let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1646 if (tui.inputEl.value.length > max_length) {
1647 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1649 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1650 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1651 tui.switch_mode('edit');
1655 document.onclick = function() {
1656 if (!tui.mode.is_single_char_entry) {
1657 tui.show_help = false;
1660 tui.inputEl.addEventListener('keydown', (event) => {
1661 tui.show_help = false;
1662 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1663 event.preventDefault();
1665 if ((!tui.mode.is_intro && event.key == 'Escape')
1666 || (tui.mode.has_input_prompt && event.key == 'Enter'
1667 && tui.inputEl.value.length == 0
1668 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1669 'admin_enter'].includes(tui.mode.name))) {
1670 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1671 tui.log_msg('@ aborted');
1673 tui.switch_mode('play');
1674 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1675 tui.show_help = true;
1676 tui.inputEl.value = "";
1677 tui.restore_input_values();
1678 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1679 && !tui.mode.is_single_char_entry) {
1680 tui.show_help = true;
1681 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1682 tui.login_name = tui.inputEl.value;
1683 server.send(['LOGIN', tui.inputEl.value]);
1684 tui.inputEl.value = "";
1685 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1686 tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1687 } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1688 if (game.player.carrying.type_ == 'Hat') {
1689 tui.enter_ascii_art('THING_DESIGN',
1690 game.player.carrying.design[0][0],
1691 game.player.carrying.design[0][1], true);
1693 tui.enter_ascii_art('THING_DESIGN',
1694 game.player.carrying.design[0][0],
1695 game.player.carrying.design[0][1], true, true);
1697 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1698 server.send(['TASK:COMMAND', tui.inputEl.value]);
1699 tui.inputEl.value = "";
1700 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1701 tui.pick_selectable('PICK_UP');
1702 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1703 tui.pick_selectable('DROP');
1704 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1705 if (tui.inputEl.value.length == 0) {
1706 tui.log_msg('@ aborted');
1708 server.send(['SET_MAP_CONTROL_PASSWORD',
1709 tui.tile_control_char, tui.inputEl.value]);
1710 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1712 tui.switch_mode('admin');
1713 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1714 explorer.set_portal(tui.inputEl.value);
1715 tui.switch_mode('edit');
1716 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1717 if (tui.inputEl.value.length == 0) {
1718 tui.inputEl.value = " ";
1720 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1721 tui.switch_mode('edit');
1722 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1723 explorer.annotate(tui.inputEl.value);
1724 tui.switch_mode('edit');
1725 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1726 if (tui.inputEl.value.length == 0) {
1727 tui.inputEl.value = " ";
1729 tui.password = tui.inputEl.value
1730 tui.switch_mode('edit');
1731 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1732 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1733 tui.switch_mode('play');
1734 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1735 if (tui.inputEl.value.length != 1) {
1736 tui.log_msg('@ entered non-single-char, therefore aborted');
1737 tui.switch_mode('admin');
1739 tui.tile_control_char = tui.inputEl.value[0];
1740 tui.switch_mode('control_pw_pw');
1742 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1743 if (tui.inputEl.value.length != 1) {
1744 tui.log_msg('@ entered non-single-char, therefore aborted');
1745 tui.switch_mode('admin');
1747 tui.tile_control_char = tui.inputEl.value[0];
1748 tui.switch_mode('control_tile_draw');
1750 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1751 if (tui.inputEl.value.length != 1) {
1752 tui.log_msg('@ entered non-single-char, therefore aborted');
1754 server.send(['THING_PROTECTION', tui.inputEl.value])
1755 tui.log_msg('@ sent new protection character for thing');
1757 tui.switch_mode('admin');
1758 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1759 let tokens = parser.tokenize(tui.inputEl.value);
1760 if (tokens.length > 0 && tokens[0].length > 0) {
1761 if (tui.inputEl.value[0][0] == '/') {
1762 if (tokens[0].slice(1) == 'nick') {
1763 if (tokens.length > 1) {
1764 server.send(['NICK', tokens[1]]);
1766 tui.log_msg('? need new name');
1769 tui.log_msg('? unknown command');
1772 server.send(['ALL', tui.inputEl.value]);
1774 } else if (tui.inputEl.valuelength > 0) {
1775 server.send(['ALL', tui.inputEl.value]);
1777 tui.inputEl.value = "";
1778 } else if (tui.mode.name == 'play') {
1779 if (tui.mode.mode_switch_on_key(event)) {
1781 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1782 server.send(["TASK:INTOXICATE"]);
1783 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1784 server.send(["TASK:DOOR"]);
1785 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1786 server.send(["TASK:WEAR"]);
1787 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1788 server.send(["TASK:SPIN"]);
1789 } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1790 server.send(["TASK:DANCE"]);
1791 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1792 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1793 } else if (event.key === tui.keys.teleport) {
1796 } else if (tui.mode.name == 'study') {
1797 if (tui.mode.mode_switch_on_key(event)) {
1799 } else if (event.key in tui.movement_keys) {
1800 explorer.move(tui.movement_keys[event.key]);
1801 } else if (event.key == tui.keys.toggle_map_mode) {
1802 tui.toggle_map_mode();
1804 } else if (tui.mode.name == 'control_tile_draw') {
1805 if (tui.mode.mode_switch_on_key(event)) {
1807 } else if (event.key in tui.movement_keys) {
1808 explorer.move(tui.movement_keys[event.key]);
1809 } else if (event.key === tui.keys.toggle_tile_draw) {
1810 tui.toggle_tile_draw();
1812 } else if (tui.mode.name == 'admin') {
1813 if (tui.mode.mode_switch_on_key(event)) {
1815 } else if (event.key == tui.keys.toggle_map_mode) {
1816 tui.toggle_map_mode();
1817 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1818 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1820 } else if (tui.mode.name == 'edit') {
1821 if (tui.mode.mode_switch_on_key(event)) {
1823 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1824 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1825 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1826 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1827 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1828 server.send(["TASK:INSTALL", tui.password]);
1829 } else if (event.key == tui.keys.toggle_map_mode) {
1830 tui.toggle_map_mode();
1836 rows_selector.addEventListener('input', function() {
1837 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1840 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1841 terminal.initialize();
1844 cols_selector.addEventListener('input', function() {
1845 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1848 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1849 terminal.initialize();
1850 tui.reset_screen_size();
1853 for (let key_selector of key_selectors) {
1854 key_selector.addEventListener('input', function() {
1855 window.localStorage.setItem(key_selector.id, key_selector.value);
1859 window.setInterval(function() {
1860 if (server.websocket.readyState == 1) {
1861 server.send(['PING']);
1862 } else if (server.websocket.readyState != 0) {
1863 server.reconnect_to(server.url);
1864 tui.log_msg('@ attempting reconnect …')
1867 window.setInterval(function() {
1868 if (document.activeElement.tagName.toLowerCase() != 'input') {
1869 const scroll_x = window.scrollX;
1870 const scroll_y = window.scrollY;
1871 tui.inputEl.focus();
1872 window.scrollTo(scroll_x, scroll_y);
1875 document.getElementById("help").onclick = function() {
1876 tui.show_help = true;
1879 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1880 const mode = switchEl.id.slice("switch_to_".length);
1881 switchEl.onclick = function() {
1882 tui.switch_mode(mode);
1886 document.getElementById("toggle_tile_draw").onclick = function() {
1887 tui.toggle_tile_draw();
1889 document.getElementById("toggle_map_mode").onclick = function() {
1890 tui.toggle_map_mode();
1893 document.getElementById("flatten").onclick = function() {
1894 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1896 document.getElementById("door").onclick = function() {
1897 server.send(['TASK:DOOR']);
1899 document.getElementById("consume").onclick = function() {
1900 server.send(['TASK:INTOXICATE']);
1902 document.getElementById("install").onclick = function() {
1903 server.send(['TASK:INSTALL', tui.password]);
1905 document.getElementById("wear").onclick = function() {
1906 server.send(['TASK:WEAR']);
1908 document.getElementById("spin").onclick = function() {
1909 server.send(['TASK:SPIN']);
1911 document.getElementById("dance").onclick = function() {
1912 server.send(['TASK:DANCE']);
1914 document.getElementById("teleport").onclick = function() {
1917 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1918 if (move_button.id.startsWith('key_')) { // not a move button
1921 let direction = move_button.id.split('_')[2].toUpperCase();
1924 if (tui.mode.available_actions.includes("move")) {
1925 server.send(['TASK:MOVE', direction]);
1926 } else if (tui.mode.available_actions.includes("move_explorer")) {
1927 explorer.move(direction);
1931 move_button.onmousedown = function() {
1933 move_repeat = window.setInterval(move, 100);
1935 move_button.onmouseup = function() {
1936 window.clearInterval(move_repeat);