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 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table id="move_table" style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
26 <td style="text-align: center"><button id="square_move_up"></button></td>
27 <td><button id="hex_move_upright"></button></td>
30 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
35 <td><button id="hex_move_downleft"></button></td>
36 <td style="text-align: center"><button id="square_move_down"></button></td>
37 <td><button id="hex_move_downright"></button></td>
42 <td><button id="help"></button></td>
45 <td><button id="switch_to_chat"></button><br /></td>
48 <td><button id="switch_to_study"></button></td>
49 <td><button id="toggle_map_mode"></button>
52 <td><button id="switch_to_play"></button></td>
54 <button id="switch_to_take_thing"></button>
55 <button id="switch_to_drop_thing"></button>
56 <button id="door"></button>
57 <button id="consume"></button>
58 <button id="switch_to_command_thing"></button>
59 <button id="teleport"></button>
60 <button id="install"></button>
61 <button id="wear"></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="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>
77 <td><button id="switch_to_admin_enter"></button></td>
79 <button id="switch_to_control_pw_type"></button>
80 <button id="switch_to_control_tile_type"></button>
81 <button id="switch_to_admin_thing_protect"></button>
82 <button id="toggle_tile_draw"></button>
87 <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 />
89 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
90 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
91 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
92 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
93 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
94 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
95 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
96 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
97 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
98 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
99 <li>help: <input id="key_help" type="text" value="h" />
100 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
101 <li>teleport: <input id="key_teleport" type="text" value="p" />
102 <li>open/close: <input id="key_door" type="text" value="D" />
103 <li>consume: <input id="key_consume" type="text" value="C" />
104 <li>install: <input id="key_install" type="text" value="I" />
105 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
106 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
107 <li><input id="key_switch_to_enter_face" type="text" value="f" />
108 <li><input id="key_switch_to_take_thing" type="text" value="z" />
109 <li><input id="key_switch_to_chat" type="text" value="t" />
110 <li><input id="key_switch_to_play" type="text" value="p" />
111 <li><input id="key_switch_to_study" type="text" value="?" />
112 <li><input id="key_switch_to_edit" type="text" value="E" />
113 <li><input id="key_switch_to_write" type="text" value="m" />
114 <li><input id="key_switch_to_name_thing" type="text" value="N" />
115 <li><input id="key_switch_to_command_thing" type="text" value="O" />
116 <li><input id="key_switch_to_password" type="text" value="P" />
117 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
118 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
119 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
120 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
121 <li><input id="key_switch_to_annotate" type="text" value="M" />
122 <li><input id="key_switch_to_portal" type="text" value="T" />
123 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
124 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
129 let websocket_location = "wss://plomlompom.com/rogue_chat/";
130 //let websocket_location = "ws://localhost:8000/";
136 'long': 'This mode allows you to interact with the map in various ways.'
141 '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.'},
143 'short': 'world edit',
145 '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.'
148 'short': 'name thing',
150 'long': 'Give name to/change name of thing here.'
153 'short': 'command thing',
155 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
158 'short': 'take thing',
159 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
160 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
163 'short': 'drop thing',
164 'intro': 'Enter number of direction to which you want to drop thing.',
165 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
167 'admin_thing_protect': {
168 'short': 'change thing protection',
169 'intro': '@ enter thing protection character:',
170 'long': 'Change protection character for thing here.'
173 'short': 'enter your face',
174 'intro': '@ enter face line (enter nothing to abort):',
175 '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..'
178 'short': 'change terrain',
180 '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.'
183 'short': 'change protection character password',
184 'intro': '@ enter protection character for which you want to change the password:',
185 '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.'
188 'short': 'change protection character password',
190 '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.'
192 'control_tile_type': {
193 'short': 'change tiles protection',
194 'intro': '@ enter protection character which you want to draw:',
195 '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.'
197 'control_tile_draw': {
198 'short': 'change tiles protection',
200 '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.'
203 'short': 'annotate tile',
205 '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.'
208 'short': 'edit portal',
210 '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.'
215 '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'
220 'long': 'Enter your player name.'
222 'waiting_for_server': {
223 'short': 'waiting for server response',
224 'intro': '@ waiting for server …',
225 'long': 'Waiting for a server response.'
228 'short': 'waiting for server response',
230 'long': 'Waiting for a server response.'
233 'short': 'set world edit password',
235 '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.'
238 'short': 'become admin',
239 'intro': '@ enter admin password:',
240 'long': 'This mode allows you to become admin if you know an admin password.'
245 'long': 'This mode allows you access to actions limited to administrators.'
248 let key_descriptions = {
250 'flatten': 'flatten surroundings',
251 'teleport': 'teleport',
252 'door': 'open/close',
253 'consume': 'consume',
254 'install': '(un-)install',
256 'toggle_map_mode': 'toggle map view',
257 'toggle_tile_draw': 'toggle protection character drawing',
258 'hex_move_upleft': 'up-left',
259 'hex_move_upright': 'up-right',
260 'hex_move_right': 'right',
261 'hex_move_left': 'left',
262 'hex_move_downleft': 'down-left',
263 'hex_move_downright': 'down-right',
264 'square_move_up': 'up',
265 'square_move_left': 'left',
266 'square_move_down': 'down',
267 'square_move_right': 'right',
269 for (const mode_name of Object.keys(mode_helps)) {
270 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
273 let rows_selector = document.getElementById("n_rows");
274 let cols_selector = document.getElementById("n_cols");
275 let key_selectors = document.querySelectorAll('[id^="key_"]');
277 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
278 const action = key_switch_selector.id.slice("key_switch_to_".length);
279 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
282 function restore_selector_value(selector) {
283 let stored_selection = window.localStorage.getItem(selector.id);
284 if (stored_selection) {
285 selector.value = stored_selection;
288 restore_selector_value(rows_selector);
289 restore_selector_value(cols_selector);
290 for (let key_selector of key_selectors) {
291 restore_selector_value(key_selector);
294 function escapeHTML(str) {
296 replace(/&/g, '&').
297 replace(/</g, '<').
298 replace(/>/g, '>').
299 replace(/'/g, ''').
300 replace(/"/g, '"');
304 initialize: function() {
305 this.rows = rows_selector.value;
306 this.cols = cols_selector.value;
307 this.pre_el = document.getElementById("terminal");
308 this.set_default_colors();
312 for (let y = 0, x = 0; y <= this.rows; x++) {
313 if (x == this.cols) {
316 this.content.push(line);
318 if (y == this.rows) {
325 apply_colors: function() {
326 this.pre_el.style.color = this.foreground;
327 this.pre_el.style.backgroundColor = this.background;
329 set_default_colors: function() {
330 this.foreground = 'white';
331 this.background = 'black';
334 set_random_colors: function() {
335 function rand(offset) {
336 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
338 this.foreground = '#' + rand(159) + rand(159) + rand(159);
339 this.background = '#' + rand(0) + rand(0) + rand(0);
342 blink_screen: function() {
343 this.pre_el.style.color = this.background;
344 this.pre_el.style.backgroundColor = this.foreground;
346 this.pre_el.style.color = this.foreground;
347 this.pre_el.style.backgroundColor = this.background;
350 refresh: function() {
351 let pre_content = '';
352 for (let y = 0; y < this.rows; y++) {
353 let line = this.content[y].join('');
355 if (y in tui.links) {
357 for (let span of tui.links[y]) {
358 chunks.push(escapeHTML(line.slice(start_x, span[0])));
359 chunks.push('<a target="_blank" href="');
360 chunks.push(escapeHTML(span[2]));
362 chunks.push(escapeHTML(line.slice(span[0], span[1])));
366 chunks.push(escapeHTML(line.slice(start_x)));
368 chunks = [escapeHTML(line)];
370 for (const chunk of chunks) {
371 pre_content += chunk;
375 this.pre_el.innerHTML = pre_content;
377 write: function(start_y, start_x, msg) {
378 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
379 this.content[start_y][x] = msg[i];
382 drawBox: function(start_y, start_x, height, width) {
383 let end_y = start_y + height;
384 let end_x = start_x + width;
385 for (let y = start_y, x = start_x; y < this.rows; x++) {
393 this.content[y][x] = ' ';
397 terminal.initialize();
400 tokenize: function(str) {
405 for (let i = 0; i < str.length; i++) {
411 } else if (c == '\\') {
413 } else if (c == '"') {
418 } else if (c == '"') {
420 } else if (c === ' ') {
421 if (token.length > 0) {
429 if (token.length > 0) {
434 parse_yx: function(position_string) {
435 let coordinate_strings = position_string.split(',')
436 let position = [0, 0];
437 position[0] = parseInt(coordinate_strings[0].slice(2));
438 position[1] = parseInt(coordinate_strings[1].slice(2));
450 init: function(url) {
452 this.websocket = new WebSocket(this.url);
453 this.websocket.onopen = function(event) {
454 server.connected = true;
455 game.thing_types = {};
457 server.send(['TASKS']);
458 server.send(['TERRAINS']);
459 server.send(['THING_TYPES']);
460 tui.log_msg("@ server connected! :)");
461 tui.switch_mode('login');
463 this.websocket.onclose = function(event) {
464 server.connected = false;
465 tui.switch_mode('waiting_for_server');
466 tui.log_msg("@ server disconnected :(");
468 this.websocket.onmessage = this.handle_event;
470 reconnect_to: function(url) {
471 this.websocket.close();
474 send: function(tokens) {
475 this.websocket.send(unparser.untokenize(tokens));
477 handle_event: function(event) {
478 let tokens = parser.tokenize(event.data);
479 if (tokens[0] === 'TURN') {
480 game.turn_complete = false;
481 explorer.empty_annotations();
485 game.turn = parseInt(tokens[1]);
486 } else if (tokens[0] === 'THING') {
487 let t = game.get_thing(tokens[4], true);
488 t.position = parser.parse_yx(tokens[1]);
490 t.protection = tokens[3];
491 t.portable = parseInt(tokens[5]);
492 t.commandable = parseInt(tokens[6]);
493 } else if (tokens[0] === 'THING_NAME') {
494 let t = game.get_thing(tokens[1], false);
496 } else if (tokens[0] === 'THING_FACE') {
497 let t = game.get_thing(tokens[1], false);
499 } else if (tokens[0] === 'THING_HAT') {
500 let t = game.get_thing(tokens[1], false);
502 } else if (tokens[0] === 'THING_CHAR') {
503 let t = game.get_thing(tokens[1], false);
504 t.thing_char = tokens[2];
505 } else if (tokens[0] === 'TASKS') {
506 game.tasks = tokens[1].split(',');
507 tui.mode_write.legal = game.tasks.includes('WRITE');
508 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
509 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
510 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
511 } else if (tokens[0] === 'THING_TYPE') {
512 game.thing_types[tokens[1]] = tokens[2]
513 } else if (tokens[0] === 'THING_CARRYING') {
514 let t = game.get_thing(tokens[1], false);
515 t.carrying = t = game.get_thing(tokens[2], false);
516 } else if (tokens[0] === 'THING_INSTALLED') {
517 let t = game.get_thing(tokens[1], false);
519 } else if (tokens[0] === 'TERRAIN') {
520 game.terrains[tokens[1]] = tokens[2]
521 } else if (tokens[0] === 'MAP') {
522 game.map_geometry = tokens[1];
524 game.map_size = parser.parse_yx(tokens[2]);
526 } else if (tokens[0] === 'FOV') {
528 } else if (tokens[0] === 'MAP_CONTROL') {
529 game.map_control = tokens[1]
530 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
531 game.turn_complete = true;
532 if (tui.mode.name == 'post_login_wait') {
533 tui.switch_mode('play');
535 explorer.info_cached = false;
537 } else if (tokens[0] === 'CHAT') {
538 tui.log_msg('# ' + tokens[1], 1);
539 } else if (tokens[0] === 'REPLY') {
540 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
541 } else if (tokens[0] === 'PLAYER_ID') {
542 game.player_id = parseInt(tokens[1]);
543 } else if (tokens[0] === 'LOGIN_OK') {
544 this.send(['GET_GAMESTATE']);
545 tui.switch_mode('post_login_wait');
546 } else if (tokens[0] === 'DEFAULT_COLORS') {
547 terminal.set_default_colors();
548 } else if (tokens[0] === 'RANDOM_COLORS') {
549 terminal.set_random_colors();
550 } else if (tokens[0] === 'ADMIN_OK') {
552 tui.log_msg('@ you now have admin rights');
553 tui.switch_mode('admin');
554 } else if (tokens[0] === 'PORTAL') {
555 let position = parser.parse_yx(tokens[1]);
556 game.portals[position] = tokens[2];
557 } else if (tokens[0] === 'ANNOTATION') {
558 let position = parser.parse_yx(tokens[1]);
559 explorer.update_annotations(position, tokens[2]);
561 } else if (tokens[0] === 'UNHANDLED_INPUT') {
562 tui.log_msg('? unknown command');
563 } else if (tokens[0] === 'PLAY_ERROR') {
564 tui.log_msg('? ' + tokens[1]);
565 terminal.blink_screen();
566 } else if (tokens[0] === 'ARGUMENT_ERROR') {
567 tui.log_msg('? syntax error: ' + tokens[1]);
568 } else if (tokens[0] === 'GAME_ERROR') {
569 tui.log_msg('? game error: ' + tokens[1]);
570 } else if (tokens[0] === 'PONG') {
573 tui.log_msg('? unhandled input: ' + event.data);
579 quote: function(str) {
581 for (let i = 0; i < str.length; i++) {
583 if (['"', '\\'].includes(c)) {
589 return quoted.join('');
591 to_yx: function(yx_coordinate) {
592 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
594 untokenize: function(tokens) {
595 let quoted_tokens = [];
596 for (let token of tokens) {
597 quoted_tokens.push(this.quote(token));
599 return quoted_tokens.join(" ");
604 constructor(name, has_input_prompt=false, shows_info=false,
605 is_intro=false, is_single_char_entry=false) {
607 this.short_desc = mode_helps[name].short;
608 this.available_modes = [];
609 this.available_actions = [];
610 this.has_input_prompt = has_input_prompt;
611 this.shows_info= shows_info;
612 this.is_intro = is_intro;
613 this.help_intro = mode_helps[name].long;
614 this.intro_msg = mode_helps[name].intro;
615 this.is_single_char_entry = is_single_char_entry;
618 *iter_available_modes() {
619 for (let mode_name of this.available_modes) {
620 let mode = tui['mode_' + mode_name];
624 let key = tui.keys['switch_to_' + mode.name];
628 list_available_modes() {
630 if (this.available_modes.length > 0) {
631 msg += 'Other modes available from here:\n';
632 for (let [mode, key] of this.iter_available_modes()) {
633 msg += '[' + key + '] – ' + mode.short_desc + '\n';
638 mode_switch_on_key(key_event) {
639 for (let [mode, key] of this.iter_available_modes()) {
640 if (key_event.key == key) {
641 event.preventDefault();
642 tui.switch_mode(mode.name);
654 window_width: terminal.cols / 2,
662 mode_waiting_for_server: new Mode('waiting_for_server',
664 mode_login: new Mode('login', true, false, true),
665 mode_post_login_wait: new Mode('post_login_wait'),
666 mode_chat: new Mode('chat', true),
667 mode_annotate: new Mode('annotate', true, true),
668 mode_play: new Mode('play'),
669 mode_study: new Mode('study', false, true),
670 mode_write: new Mode('write', false, false, false, true),
671 mode_edit: new Mode('edit'),
672 mode_control_pw_type: new Mode('control_pw_type', true),
673 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
674 mode_portal: new Mode('portal', true, true),
675 mode_password: new Mode('password', true),
676 mode_name_thing: new Mode('name_thing', true, true),
677 mode_command_thing: new Mode('command_thing', true),
678 mode_take_thing: new Mode('take_thing', true),
679 mode_drop_thing: new Mode('drop_thing', true),
680 mode_enter_face: new Mode('enter_face', true),
681 mode_admin_enter: new Mode('admin_enter', true),
682 mode_admin: new Mode('admin'),
683 mode_control_pw_pw: new Mode('control_pw_pw', true),
684 mode_control_tile_type: new Mode('control_tile_type', true),
685 mode_control_tile_draw: new Mode('control_tile_draw'),
687 'flatten': 'FLATTEN_SURROUNDINGS',
688 'take_thing': 'PICK_UP',
689 'drop_thing': 'DROP',
692 'install': 'INSTALL',
694 'command': 'COMMAND',
695 'consume': 'INTOXICATE',
701 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
702 "command_thing", "take_thing", "drop_thing"]
703 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
705 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
706 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
707 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
708 "control_tile_type", "chat",
709 "study", "play", "edit"]
710 this.mode_admin.available_actions = ["move"];
711 this.mode_control_tile_draw.available_modes = ["admin_enter"]
712 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
713 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
714 "password", "chat", "study", "play",
715 "admin_enter", "enter_face"]
716 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
717 this.inputEl = document.getElementById("input");
718 this.inputEl.focus();
719 this.switch_mode('waiting_for_server');
720 this.recalc_input_lines();
721 this.height_header = this.height_turn_line + this.height_mode_line;
724 init_keys: function() {
725 document.getElementById("move_table").hidden = true;
727 for (let key_selector of key_selectors) {
728 this.keys[key_selector.id.slice(4)] = key_selector.value;
730 this.movement_keys = {};
731 let geometry_prefix = 'undefinedMapGeometry_';
732 if (game.map_geometry) {
733 geometry_prefix = game.map_geometry.toLowerCase() + '_';
735 for (const key_name of Object.keys(key_descriptions)) {
736 if (key_name.startsWith(geometry_prefix)) {
737 let direction = key_name.split('_')[2].toUpperCase();
738 let key = this.keys[key_name];
739 this.movement_keys[key] = direction;
742 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
743 if (move_button.id.startsWith('key_')) {
746 move_button.hidden = true;
748 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
749 document.getElementById("move_table").hidden = false;
750 move_button.hidden = false;
752 for (let el of document.getElementsByTagName("button")) {
753 let action_desc = key_descriptions[el.id];
754 let action_key = '[' + this.keys[el.id] + ']';
755 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
758 task_action_on: function(action) {
759 return game.tasks.includes(this.action_tasks[action]);
761 switch_mode: function(mode_name) {
762 if (this.mode && this.mode.name == 'control_tile_draw') {
763 tui.log_msg('@ finished tile protection drawing.')
765 this.tile_draw = false;
766 const player = game.things[game.player_id];
767 if (mode_name == 'command_thing' && (!player.carrying || !player.carrying.commandable)) {
768 this.log_msg('? not carrying anything commandable');
769 terminal.blink_screen();
770 this.switch_mode('play');
773 if (mode_name == 'drop_thing' && (!player.carrying)) {
774 this.log_msg('? not carrying anything droppable');
775 terminal.blink_screen();
776 this.switch_mode('play');
779 if (mode_name == 'admin_enter' && this.is_admin) {
781 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
783 for (let t_id in game.things) {
784 if (t_id == game.player_id) {
787 let t = game.things[t_id];
788 if (player.position[0] == t.position[0]
789 && player.position[1] == t.position[1]) {
795 terminal.blink_screen();
796 this.log_msg('? not standing over thing');
799 this.selected_thing_id = thing_id;
802 this.mode = this['mode_' + mode_name];
803 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
804 this.map_mode = 'protections';
805 } else if (this.mode.name != "edit") {
806 this.map_mode = 'terrain + things';
808 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
809 this.inputEl.focus();
811 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
812 explorer.position = game.things[game.player_id].position;
814 this.inputEl.value = "";
815 this.restore_input_values();
816 for (let el of document.getElementsByTagName("button")) {
819 document.getElementById("help").disabled = false;
820 for (const action of this.mode.available_actions) {
821 if (["move", "move_explorer"].includes(action)) {
822 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
823 move_key.disabled = false;
825 } else if (Object.keys(this.action_tasks).includes(action)) {
826 if (this.task_action_on(action)) {
827 document.getElementById(action).disabled = false;
830 document.getElementById(action).disabled = false;
833 for (const mode_name of this.mode.available_modes) {
834 document.getElementById('switch_to_' + mode_name).disabled = false;
836 if (this.mode.intro_msg.length > 0) {
837 this.log_msg(this.mode.intro_msg);
839 if (this.mode.name == 'login') {
840 if (this.login_name) {
841 server.send(['LOGIN', this.login_name]);
843 this.log_msg("? need login name");
845 } else if (this.mode.is_single_char_entry) {
846 this.show_help = true;
847 } else if (this.mode.name == 'take_thing') {
848 this.log_msg("Portable things in reach for pick-up:");
849 const player = game.things[game.player_id];
850 const y = player.position[0]
851 const x = player.position[1]
852 let select_range = [y.toString() + ':' + x.toString(),
853 (y + 0).toString() + ':' + (x - 1).toString(),
854 (y + 0).toString() + ':' + (x + 1).toString(),
855 (y - 1).toString() + ':' + (x).toString(),
856 (y + 1).toString() + ':' + (x).toString()];
857 if (game.map_geometry == 'Hex') {
859 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
860 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
862 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
863 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
866 this.selectables = [];
867 for (const t_id in game.things) {
868 const t = game.things[t_id];
869 if (select_range.includes(t.position[0].toString()
870 + ':' + t.position[1].toString())
872 this.selectables.push(t_id);
875 if (this.selectables.length == 0) {
876 this.log_msg('none');
877 terminal.blink_screen();
878 this.switch_mode('play');
881 for (let [i, t_id] of this.selectables.entries()) {
882 const t = game.things[t_id];
883 this.log_msg(i + ': ' + explorer.get_thing_info(t));
886 } else if (this.mode.name == 'drop_thing') {
887 this.log_msg('Direction to drop thing to:');
888 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
889 for (let [i, direction] of this.selectables.entries()) {
890 this.log_msg(i + ': ' + direction);
892 } else if (this.mode.name == 'command_thing') {
893 server.send(['TASK:COMMAND', 'HELP']);
894 } else if (this.mode.name == 'control_pw_pw') {
895 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
896 } else if (this.mode.name == 'control_tile_draw') {
897 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 + '].')
901 offset_links: function(offset, links) {
902 for (let y in links) {
903 let real_y = offset[0] + parseInt(y);
904 if (!this.links[real_y]) {
905 this.links[real_y] = [];
907 for (let link of links[y]) {
908 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
909 this.links[real_y].push(offset_link);
913 restore_input_values: function() {
914 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
915 let info = explorer.annotations[explorer.position];
916 if (info != "(none)") {
917 this.inputEl.value = info;
919 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
920 let portal = game.portals[explorer.position]
921 this.inputEl.value = portal;
922 } else if (this.mode.name == 'password') {
923 this.inputEl.value = this.password;
924 } else if (this.mode.name == 'name_thing') {
925 let t = game.get_thing(this.selected_thing_id);
927 this.inputEl.value = t.name_;
929 } else if (this.mode.name == 'admin_thing_protect') {
930 let t = game.get_thing(this.selected_thing_id);
931 if (t && t.protection) {
932 this.inputEl.value = t.protection;
936 recalc_input_lines: function() {
937 if (this.mode.has_input_prompt) {
939 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
941 this.input_lines = [];
943 this.height_input = this.input_lines.length;
945 msg_into_lines_of_width: function(msg, width) {
946 function push_inner_link(y, end_x) {
947 if (!inner_links[y]) {
950 inner_links[y].push([url_start_x, end_x, url]);
952 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
955 for (const match of matches) {
956 const url = match[0];
957 const url_start = match.index;
958 const url_end = match.index + match[0].length;
959 link_data[url_start] = url;
960 url_ends.push(url_end);
964 let inner_links = {};
968 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
969 if (x >= width || msg[i] == "\n") {
971 push_inner_link(y, chunk.length);
973 if (url_ends[0] == i) {
981 if (msg[i] == "\n") {
986 if (msg[i] != "\n") {
989 if (i in link_data) {
993 } else if (url_ends[0] == i) {
995 push_inner_link(y, x);
1001 push_inner_link(lines.length - 1, chunk.length);
1003 return [lines, inner_links];
1005 log_msg: function(msg) {
1007 while (this.log.length > 100) {
1010 this.full_refresh();
1012 pick_selectable: function(task_name) {
1013 const i = parseInt(this.inputEl.value);
1014 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1015 tui.log_msg('? invalid index, aborted');
1017 server.send(['TASK:' + task_name, tui.selectables[i]]);
1019 this.inputEl.value = "";
1020 this.switch_mode('play');
1022 draw_map: function() {
1023 if (!game.turn_complete && this.map_lines.length == 0) {
1026 if (game.turn_complete) {
1027 let map_lines_split = [];
1029 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1030 if (j == game.map_size[1]) {
1031 map_lines_split.push(line);
1035 if (this.map_mode == 'protections') {
1036 line.push(game.map_control[i] + ' ');
1038 line.push(game.map[i] + ' ');
1041 map_lines_split.push(line);
1042 if (this.map_mode == 'terrain + annotations') {
1043 for (const coordinate of explorer.info_hints) {
1044 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
1046 } else if (this.map_mode == 'terrain + things') {
1047 for (const p in game.portals) {
1048 let coordinate = p.split(',')
1049 let original = map_lines_split[coordinate[0]][coordinate[1]];
1050 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1052 let used_positions = [];
1053 function draw_thing(t, used_positions) {
1054 let symbol = game.thing_types[t.type_];
1055 let meta_char = ' ';
1057 meta_char = t.thing_char;
1059 if (used_positions.includes(t.position.toString())) {
1065 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1066 used_positions.push(t.position.toString());
1068 for (const thing_id in game.things) {
1069 let t = game.things[thing_id];
1070 if (t.type_ != 'Player') {
1071 draw_thing(t, used_positions);
1074 for (const thing_id in game.things) {
1075 let t = game.things[thing_id];
1076 if (t.type_ == 'Player') {
1077 draw_thing(t, used_positions);
1081 let player = game.things[game.player_id];
1082 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1083 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1084 } else if (tui.map_mode != 'terrain + things') {
1085 map_lines_split[player.position[0]][player.position[1]] = '??';
1088 if (game.map_geometry == 'Square') {
1089 for (let line_split of map_lines_split) {
1090 this.map_lines.push(line_split.join(''));
1092 } else if (game.map_geometry == 'Hex') {
1094 for (let line_split of map_lines_split) {
1095 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1103 let window_center = [terminal.rows / 2, this.window_width / 2];
1104 let center_position = [player.position[0], player.position[1]];
1105 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1106 center_position = [explorer.position[0], explorer.position[1]];
1108 center_position[1] = center_position[1] * 2;
1109 this.offset = [center_position[0] - window_center[0],
1110 center_position[1] - window_center[1]]
1111 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1112 this.offset[1] += 1;
1115 let term_y = Math.max(0, -this.offset[0]);
1116 let term_x = Math.max(0, -this.offset[1]);
1117 let map_y = Math.max(0, this.offset[0]);
1118 let map_x = Math.max(0, this.offset[1]);
1119 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1120 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1121 terminal.write(term_y, term_x, to_draw);
1124 draw_mode_line: function() {
1125 let help = 'hit [' + this.keys.help + '] for help';
1126 if (this.mode.has_input_prompt) {
1127 help = 'enter /help for help';
1129 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1131 draw_turn_line: function(n) {
1132 if (game.turn_complete) {
1133 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1136 draw_history: function() {
1137 let log_display_lines = [];
1139 let y_offset_in_log = 0;
1140 for (let line of this.log) {
1141 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1143 log_display_lines = log_display_lines.concat(new_lines);
1144 for (const y in link_data) {
1145 const rel_y = y_offset_in_log + parseInt(y);
1146 log_links[rel_y] = [];
1147 for (let link of link_data[y]) {
1148 log_links[rel_y].push(link);
1151 y_offset_in_log += new_lines.length;
1153 let i = log_display_lines.length - 1;
1154 for (let y = terminal.rows - 1 - this.height_input;
1155 y >= this.height_header && i >= 0;
1157 terminal.write(y, this.window_width, log_display_lines[i]);
1159 for (const key of Object.keys(log_links)) {
1160 if (parseInt(key) <= i) {
1161 delete log_links[key];
1164 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1166 this.offset_links(offset, log_links);
1168 draw_info: function() {
1169 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1170 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1171 let offset = [this.height_header, this.window_width];
1172 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1173 terminal.write(y, offset[1], lines[i]);
1175 this.offset_links(offset, link_data);
1177 draw_input: function() {
1178 if (this.mode.has_input_prompt) {
1179 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1180 terminal.write(y, this.window_width, this.input_lines[i]);
1184 draw_help: function() {
1185 let movement_keys_desc = '';
1186 if (!this.mode.is_intro) {
1187 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1189 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1190 if (this.mode.available_actions.length > 0) {
1191 content += "Available actions:\n";
1192 for (let action of this.mode.available_actions) {
1193 if (Object.keys(this.action_tasks).includes(action)) {
1194 if (!this.task_action_on(action)) {
1198 if (action == 'move_explorer') {
1201 if (action == 'move') {
1202 content += "[" + movement_keys_desc + "] – move\n"
1204 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1209 content += this.mode.list_available_modes();
1211 if (!this.mode.has_input_prompt) {
1212 start_x = this.window_width
1214 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1215 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1216 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1217 terminal.write(y, start_x, lines[i]);
1220 toggle_tile_draw: function() {
1221 if (tui.tile_draw) {
1222 tui.tile_draw = false;
1224 tui.tile_draw = true;
1227 toggle_map_mode: function() {
1228 if (tui.map_mode == 'terrain only') {
1229 tui.map_mode = 'terrain + annotations';
1230 } else if (tui.map_mode == 'terrain + annotations') {
1231 tui.map_mode = 'terrain + things';
1232 } else if (tui.map_mode == 'terrain + things') {
1233 tui.map_mode = 'protections';
1234 } else if (tui.map_mode == 'protections') {
1235 tui.map_mode = 'terrain only';
1238 full_refresh: function() {
1240 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1241 this.recalc_input_lines();
1242 if (this.mode.is_intro) {
1243 this.draw_history();
1247 this.draw_turn_line();
1248 this.draw_mode_line();
1249 if (this.mode.shows_info) {
1252 this.draw_history();
1256 if (this.show_help) {
1268 this.map_control = "";
1269 this.map_size = [0,0];
1270 this.player_id = -1;
1274 get_thing: function(id_, create_if_not_found=false) {
1275 if (id_ in game.things) {
1276 return game.things[id_];
1277 } else if (create_if_not_found) {
1278 let t = new Thing([0,0]);
1279 game.things[id_] = t;
1283 move: function(start_position, direction) {
1284 let target = [start_position[0], start_position[1]];
1285 if (direction == 'LEFT') {
1287 } else if (direction == 'RIGHT') {
1289 } else if (game.map_geometry == 'Square') {
1290 if (direction == 'UP') {
1292 } else if (direction == 'DOWN') {
1295 } else if (game.map_geometry == 'Hex') {
1296 let start_indented = start_position[0] % 2;
1297 if (direction == 'UPLEFT') {
1299 if (!start_indented) {
1302 } else if (direction == 'UPRIGHT') {
1304 if (start_indented) {
1307 } else if (direction == 'DOWNLEFT') {
1309 if (!start_indented) {
1312 } else if (direction == 'DOWNRIGHT') {
1314 if (start_indented) {
1319 if (target[0] < 0 || target[1] < 0 ||
1320 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1325 teleport: function() {
1326 let player = this.get_thing(game.player_id);
1327 if (player.position in this.portals) {
1328 server.reconnect_to(this.portals[player.position]);
1330 terminal.blink_screen();
1331 tui.log_msg('? not standing on portal')
1339 server.init(websocket_location);
1345 move: function(direction) {
1346 let target = game.move(this.position, direction);
1348 this.position = target
1349 this.info_cached = false;
1350 if (tui.tile_draw) {
1351 this.send_tile_control_command();
1354 terminal.blink_screen();
1357 update_annotations: function(yx, str) {
1358 this.annotations[yx] = str;
1359 if (tui.mode.name == 'study') {
1363 empty_annotations: function() {
1364 this.annotations = {};
1365 if (tui.mode.name == 'study') {
1369 get_info: function() {
1370 if (this.info_cached) {
1371 return this.info_cached;
1373 let info_to_cache = '';
1374 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1375 if (game.fov[position_i] != '.') {
1376 info_to_cache += 'outside field of view';
1378 for (let t_id in game.things) {
1379 let t = game.things[t_id];
1380 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1381 info_to_cache += "THING: " + this.get_thing_info(t);
1382 let protection = t.protection;
1383 if (protection == '.') {
1384 protection = 'none';
1386 info_to_cache += " / protection: " + protection + "\n";
1388 info_to_cache += t.hat.slice(0, 6) + '\n';
1389 info_to_cache += t.hat.slice(6, 12) + '\n';
1390 info_to_cache += t.hat.slice(12, 18) + '\n';
1393 info_to_cache += t.face.slice(0, 6) + '\n';
1394 info_to_cache += t.face.slice(6, 12) + '\n';
1395 info_to_cache += t.face.slice(12, 18) + '\n';
1399 let terrain_char = game.map[position_i]
1400 let terrain_desc = '?'
1401 if (game.terrains[terrain_char]) {
1402 terrain_desc = game.terrains[terrain_char];
1404 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1405 let protection = game.map_control[position_i];
1406 if (protection == '.') {
1407 protection = 'unprotected';
1409 info_to_cache += 'PROTECTION: ' + protection + '\n';
1410 if (this.position in game.portals) {
1411 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1413 if (this.position in this.annotations) {
1414 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1417 this.info_cached = info_to_cache;
1418 return this.info_cached;
1420 get_thing_info: function(t) {
1421 const symbol = game.thing_types[t.type_];
1422 let info = t.type_ + " / " + symbol;
1424 info += t.thing_char;
1427 info += " (" + t.name_ + ")";
1430 info += " / installed";
1434 annotate: function(msg) {
1435 if (msg.length == 0) {
1436 msg = " "; // triggers annotation deletion
1438 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1440 set_portal: function(msg) {
1441 if (msg.length == 0) {
1442 msg = " "; // triggers portal deletion
1444 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1446 send_tile_control_command: function() {
1447 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1451 tui.inputEl.addEventListener('input', (event) => {
1452 if (tui.mode.has_input_prompt) {
1453 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1454 if (tui.inputEl.value.length > max_length) {
1455 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1457 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1458 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1459 tui.switch_mode('edit');
1463 document.onclick = function() {
1464 tui.show_help = false;
1466 tui.inputEl.addEventListener('keydown', (event) => {
1467 tui.show_help = false;
1468 if (event.key == 'Enter') {
1469 event.preventDefault();
1471 if (tui.mode.has_input_prompt && event.key == 'Enter'
1472 && tui.inputEl.value.length == 0
1473 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1474 'admin_enter'].includes(tui.mode.name)) {
1475 if (tui.mode.name != 'chat') {
1476 tui.log_msg('@ aborted');
1478 tui.switch_mode('play');
1479 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1480 tui.show_help = true;
1481 tui.inputEl.value = "";
1482 tui.restore_input_values();
1483 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1484 && !tui.mode.is_single_char_entry) {
1485 tui.show_help = true;
1486 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1487 tui.login_name = tui.inputEl.value;
1488 server.send(['LOGIN', tui.inputEl.value]);
1489 tui.inputEl.value = "";
1490 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1491 if (tui.inputEl.value.length != 18) {
1492 tui.log_msg('? wrong input length, aborting');
1494 server.send(['PLAYER_FACE', tui.inputEl.value]);
1496 tui.inputEl.value = "";
1497 tui.switch_mode('edit');
1498 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1499 server.send(['TASK:COMMAND', tui.inputEl.value]);
1500 tui.inputEl.value = "";
1501 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1502 tui.pick_selectable('PICK_UP');
1503 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1504 tui.pick_selectable('DROP');
1505 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1506 if (tui.inputEl.value.length == 0) {
1507 tui.log_msg('@ aborted');
1509 server.send(['SET_MAP_CONTROL_PASSWORD',
1510 tui.tile_control_char, tui.inputEl.value]);
1511 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1513 tui.switch_mode('admin');
1514 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1515 explorer.set_portal(tui.inputEl.value);
1516 tui.switch_mode('edit');
1517 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1518 if (tui.inputEl.value.length == 0) {
1519 tui.inputEl.value = " ";
1521 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1523 tui.switch_mode('edit');
1524 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1525 explorer.annotate(tui.inputEl.value);
1526 tui.switch_mode('edit');
1527 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1528 if (tui.inputEl.value.length == 0) {
1529 tui.inputEl.value = " ";
1531 tui.password = tui.inputEl.value
1532 tui.switch_mode('edit');
1533 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1534 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1535 tui.switch_mode('play');
1536 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1537 if (tui.inputEl.value.length != 1) {
1538 tui.log_msg('@ entered non-single-char, therefore aborted');
1539 tui.switch_mode('admin');
1541 tui.tile_control_char = tui.inputEl.value[0];
1542 tui.switch_mode('control_pw_pw');
1544 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1545 if (tui.inputEl.value.length != 1) {
1546 tui.log_msg('@ entered non-single-char, therefore aborted');
1547 tui.switch_mode('admin');
1549 tui.tile_control_char = tui.inputEl.value[0];
1550 tui.switch_mode('control_tile_draw');
1552 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1553 if (tui.inputEl.value.length != 1) {
1554 tui.log_msg('@ entered non-single-char, therefore aborted');
1556 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1557 tui.log_msg('@ sent new protection character for thing');
1559 tui.switch_mode('admin');
1560 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1561 let tokens = parser.tokenize(tui.inputEl.value);
1562 if (tokens.length > 0 && tokens[0].length > 0) {
1563 if (tui.inputEl.value[0][0] == '/') {
1564 if (tokens[0].slice(1) == 'nick') {
1565 if (tokens.length > 1) {
1566 server.send(['NICK', tokens[1]]);
1568 tui.log_msg('? need new name');
1571 tui.log_msg('? unknown command');
1574 server.send(['ALL', tui.inputEl.value]);
1576 } else if (tui.inputEl.valuelength > 0) {
1577 server.send(['ALL', tui.inputEl.value]);
1579 tui.inputEl.value = "";
1580 } else if (tui.mode.name == 'play') {
1581 if (tui.mode.mode_switch_on_key(event)) {
1583 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1584 server.send(["TASK:INTOXICATE"]);
1585 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1586 server.send(["TASK:DOOR"]);
1587 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1588 server.send(["TASK:INSTALL"]);
1589 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1590 server.send(["TASK:WEAR"]);
1591 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1592 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1593 } else if (event.key === tui.keys.teleport) {
1596 } else if (tui.mode.name == 'study') {
1597 if (tui.mode.mode_switch_on_key(event)) {
1599 } else if (event.key in tui.movement_keys) {
1600 explorer.move(tui.movement_keys[event.key]);
1601 } else if (event.key == tui.keys.toggle_map_mode) {
1602 tui.toggle_map_mode();
1604 } else if (tui.mode.name == 'control_tile_draw') {
1605 if (tui.mode.mode_switch_on_key(event)) {
1607 } else if (event.key in tui.movement_keys) {
1608 explorer.move(tui.movement_keys[event.key]);
1609 } else if (event.key === tui.keys.toggle_tile_draw) {
1610 tui.toggle_tile_draw();
1612 } else if (tui.mode.name == 'admin') {
1613 if (tui.mode.mode_switch_on_key(event)) {
1615 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1616 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1618 } else if (tui.mode.name == 'edit') {
1619 if (tui.mode.mode_switch_on_key(event)) {
1621 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1622 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1623 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1624 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1625 } else if (event.key == tui.keys.toggle_map_mode) {
1626 tui.toggle_map_mode();
1632 rows_selector.addEventListener('input', function() {
1633 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1636 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1637 terminal.initialize();
1640 cols_selector.addEventListener('input', function() {
1641 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1644 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1645 terminal.initialize();
1646 tui.window_width = terminal.cols / 2,
1649 for (let key_selector of key_selectors) {
1650 key_selector.addEventListener('input', function() {
1651 window.localStorage.setItem(key_selector.id, key_selector.value);
1655 window.setInterval(function() {
1656 if (server.connected) {
1657 server.send(['PING']);
1659 server.reconnect_to(server.url);
1660 tui.log_msg('@ attempting reconnect …')
1663 window.setInterval(function() {
1665 let span_decoration = "none";
1666 if (document.activeElement == tui.inputEl) {
1667 val = "on (click outside terminal to change)";
1669 val = "off (click into terminal to change)";
1670 span_decoration = "line-through";
1672 document.getElementById("keyboard_control").textContent = val;
1673 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1674 span.style.textDecoration = span_decoration;
1677 document.getElementById("terminal").onclick = function() {
1678 tui.inputEl.focus();
1680 document.getElementById("help").onclick = function() {
1681 tui.show_help = true;
1684 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1685 const mode = switchEl.id.slice("switch_to_".length);
1686 switchEl.onclick = function() {
1687 tui.switch_mode(mode);
1691 document.getElementById("toggle_tile_draw").onclick = function() {
1692 tui.toggle_tile_draw();
1694 document.getElementById("toggle_map_mode").onclick = function() {
1695 tui.toggle_map_mode();
1698 document.getElementById("flatten").onclick = function() {
1699 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1701 document.getElementById("door").onclick = function() {
1702 server.send(['TASK:DOOR']);
1704 document.getElementById("consume").onclick = function() {
1705 server.send(['TASK:INTOXICATE']);
1707 document.getElementById("install").onclick = function() {
1708 server.send(['TASK:INSTALL']);
1710 document.getElementById("wear").onclick = function() {
1711 server.send(['TASK:WEAR']);
1713 document.getElementById("teleport").onclick = function() {
1716 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1717 if (move_button.id.startsWith('key_')) { // not a move button
1720 let direction = move_button.id.split('_')[2].toUpperCase();
1721 move_button.onclick = function() {
1722 if (tui.mode.available_actions.includes("move")) {
1723 server.send(['TASK:MOVE', direction]);
1724 } else if (tui.mode.available_actions.includes("move_explorer")) {
1725 explorer.move(direction);