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 == 'admin_enter' && this.is_admin) {
775 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
777 for (let t_id in game.things) {
778 if (t_id == game.player_id) {
781 let t = game.things[t_id];
782 if (player.position[0] == t.position[0]
783 && player.position[1] == t.position[1]) {
789 terminal.blink_screen();
790 this.log_msg('? not standing over thing');
793 this.selected_thing_id = thing_id;
796 this.mode = this['mode_' + mode_name];
797 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
798 this.map_mode = 'protections';
799 } else if (this.mode.name != "edit") {
800 this.map_mode = 'terrain + things';
802 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
803 this.inputEl.focus();
805 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
806 explorer.position = game.things[game.player_id].position;
808 this.inputEl.value = "";
809 this.restore_input_values();
810 for (let el of document.getElementsByTagName("button")) {
813 document.getElementById("help").disabled = false;
814 for (const action of this.mode.available_actions) {
815 if (["move", "move_explorer"].includes(action)) {
816 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
817 move_key.disabled = false;
819 } else if (Object.keys(this.action_tasks).includes(action)) {
820 if (this.task_action_on(action)) {
821 document.getElementById(action).disabled = false;
824 document.getElementById(action).disabled = false;
827 for (const mode_name of this.mode.available_modes) {
828 document.getElementById('switch_to_' + mode_name).disabled = false;
830 if (this.mode.intro_msg.length > 0) {
831 this.log_msg(this.mode.intro_msg);
833 if (this.mode.name == 'login') {
834 if (this.login_name) {
835 server.send(['LOGIN', this.login_name]);
837 this.log_msg("? need login name");
839 } else if (this.mode.is_single_char_entry) {
840 this.show_help = true;
841 } else if (this.mode.name == 'take_thing') {
842 this.log_msg("Portable things in reach for pick-up:");
843 const player = game.things[game.player_id];
844 const y = player.position[0]
845 const x = player.position[1]
846 let select_range = [y.toString() + ':' + x.toString(),
847 (y + 0).toString() + ':' + (x - 1).toString(),
848 (y + 0).toString() + ':' + (x + 1).toString(),
849 (y - 1).toString() + ':' + (x).toString(),
850 (y + 1).toString() + ':' + (x).toString()];
851 if (game.map_geometry == 'Hex') {
853 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
854 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
856 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
857 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
860 this.selectables = [];
861 for (const t_id in game.things) {
862 const t = game.things[t_id];
863 if (select_range.includes(t.position[0].toString()
864 + ':' + t.position[1].toString())
866 this.selectables.push(t_id);
869 if (this.selectables.length == 0) {
870 this.log_msg('none');
871 terminal.blink_screen();
872 this.switch_mode('play');
875 for (let [i, t_id] of this.selectables.entries()) {
876 const t = game.things[t_id];
877 this.log_msg(i + ': ' + explorer.get_thing_info(t));
880 } else if (this.mode.name == 'drop_thing') {
881 this.log_msg('Direction to drop thing to:');
882 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
883 for (let [i, direction] of this.selectables.entries()) {
884 this.log_msg(i + ': ' + direction);
886 } else if (this.mode.name == 'command_thing') {
887 server.send(['TASK:COMMAND', 'HELP']);
888 } else if (this.mode.name == 'control_pw_pw') {
889 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
890 } else if (this.mode.name == 'control_tile_draw') {
891 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 + '].')
895 offset_links: function(offset, links) {
896 for (let y in links) {
897 let real_y = offset[0] + parseInt(y);
898 if (!this.links[real_y]) {
899 this.links[real_y] = [];
901 for (let link of links[y]) {
902 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
903 this.links[real_y].push(offset_link);
907 restore_input_values: function() {
908 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
909 let info = explorer.annotations[explorer.position];
910 if (info != "(none)") {
911 this.inputEl.value = info;
913 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
914 let portal = game.portals[explorer.position]
915 this.inputEl.value = portal;
916 } else if (this.mode.name == 'password') {
917 this.inputEl.value = this.password;
918 } else if (this.mode.name == 'name_thing') {
919 let t = game.get_thing(this.selected_thing_id);
921 this.inputEl.value = t.name_;
923 } else if (this.mode.name == 'admin_thing_protect') {
924 let t = game.get_thing(this.selected_thing_id);
925 if (t && t.protection) {
926 this.inputEl.value = t.protection;
930 recalc_input_lines: function() {
931 if (this.mode.has_input_prompt) {
933 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
935 this.input_lines = [];
937 this.height_input = this.input_lines.length;
939 msg_into_lines_of_width: function(msg, width) {
940 function push_inner_link(y, end_x) {
941 if (!inner_links[y]) {
944 inner_links[y].push([url_start_x, end_x, url]);
946 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
949 for (const match of matches) {
950 const url = match[0];
951 const url_start = match.index;
952 const url_end = match.index + match[0].length;
953 link_data[url_start] = url;
954 url_ends.push(url_end);
958 let inner_links = {};
962 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
963 if (x >= width || msg[i] == "\n") {
965 push_inner_link(y, chunk.length);
967 if (url_ends[0] == i) {
975 if (msg[i] == "\n") {
980 if (msg[i] != "\n") {
983 if (i in link_data) {
987 } else if (url_ends[0] == i) {
989 push_inner_link(y, x);
995 push_inner_link(lines.length - 1, chunk.length);
997 return [lines, inner_links];
999 log_msg: function(msg) {
1001 while (this.log.length > 100) {
1004 this.full_refresh();
1006 pick_selectable: function(task_name) {
1007 const i = parseInt(this.inputEl.value);
1008 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1009 tui.log_msg('? invalid index, aborted');
1011 server.send(['TASK:' + task_name, tui.selectables[i]]);
1013 this.inputEl.value = "";
1014 this.switch_mode('play');
1016 draw_map: function() {
1017 if (!game.turn_complete && this.map_lines.length == 0) {
1020 if (game.turn_complete) {
1021 let map_lines_split = [];
1023 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1024 if (j == game.map_size[1]) {
1025 map_lines_split.push(line);
1029 if (this.map_mode == 'protections') {
1030 line.push(game.map_control[i] + ' ');
1032 line.push(game.map[i] + ' ');
1035 map_lines_split.push(line);
1036 if (this.map_mode == 'terrain + annotations') {
1037 for (const coordinate of explorer.info_hints) {
1038 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
1040 } else if (this.map_mode == 'terrain + things') {
1041 for (const p in game.portals) {
1042 let coordinate = p.split(',')
1043 let original = map_lines_split[coordinate[0]][coordinate[1]];
1044 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1046 let used_positions = [];
1047 function draw_thing(t, used_positions) {
1048 let symbol = game.thing_types[t.type_];
1049 let meta_char = ' ';
1051 meta_char = t.thing_char;
1053 if (used_positions.includes(t.position.toString())) {
1059 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1060 used_positions.push(t.position.toString());
1062 for (const thing_id in game.things) {
1063 let t = game.things[thing_id];
1064 if (t.type_ != 'Player') {
1065 draw_thing(t, used_positions);
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);
1075 let player = game.things[game.player_id];
1076 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1077 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1078 } else if (tui.map_mode != 'terrain + things') {
1079 map_lines_split[player.position[0]][player.position[1]] = '??';
1082 if (game.map_geometry == 'Square') {
1083 for (let line_split of map_lines_split) {
1084 this.map_lines.push(line_split.join(''));
1086 } else if (game.map_geometry == 'Hex') {
1088 for (let line_split of map_lines_split) {
1089 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1097 let window_center = [terminal.rows / 2, this.window_width / 2];
1098 let center_position = [player.position[0], player.position[1]];
1099 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1100 center_position = [explorer.position[0], explorer.position[1]];
1102 center_position[1] = center_position[1] * 2;
1103 this.offset = [center_position[0] - window_center[0],
1104 center_position[1] - window_center[1]]
1105 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1106 this.offset[1] += 1;
1109 let term_y = Math.max(0, -this.offset[0]);
1110 let term_x = Math.max(0, -this.offset[1]);
1111 let map_y = Math.max(0, this.offset[0]);
1112 let map_x = Math.max(0, this.offset[1]);
1113 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1114 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1115 terminal.write(term_y, term_x, to_draw);
1118 draw_mode_line: function() {
1119 let help = 'hit [' + this.keys.help + '] for help';
1120 if (this.mode.has_input_prompt) {
1121 help = 'enter /help for help';
1123 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1125 draw_turn_line: function(n) {
1126 if (game.turn_complete) {
1127 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1130 draw_history: function() {
1131 let log_display_lines = [];
1133 let y_offset_in_log = 0;
1134 for (let line of this.log) {
1135 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1137 log_display_lines = log_display_lines.concat(new_lines);
1138 for (const y in link_data) {
1139 const rel_y = y_offset_in_log + parseInt(y);
1140 log_links[rel_y] = [];
1141 for (let link of link_data[y]) {
1142 log_links[rel_y].push(link);
1145 y_offset_in_log += new_lines.length;
1147 let i = log_display_lines.length - 1;
1148 for (let y = terminal.rows - 1 - this.height_input;
1149 y >= this.height_header && i >= 0;
1151 terminal.write(y, this.window_width, log_display_lines[i]);
1153 for (const key of Object.keys(log_links)) {
1154 if (parseInt(key) <= i) {
1155 delete log_links[key];
1158 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1160 this.offset_links(offset, log_links);
1162 draw_info: function() {
1163 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1164 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1165 let offset = [this.height_header, this.window_width];
1166 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1167 terminal.write(y, offset[1], lines[i]);
1169 this.offset_links(offset, link_data);
1171 draw_input: function() {
1172 if (this.mode.has_input_prompt) {
1173 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1174 terminal.write(y, this.window_width, this.input_lines[i]);
1178 draw_help: function() {
1179 let movement_keys_desc = '';
1180 if (!this.mode.is_intro) {
1181 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1183 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1184 if (this.mode.available_actions.length > 0) {
1185 content += "Available actions:\n";
1186 for (let action of this.mode.available_actions) {
1187 if (Object.keys(this.action_tasks).includes(action)) {
1188 if (!this.task_action_on(action)) {
1192 if (action == 'move_explorer') {
1195 if (action == 'move') {
1196 content += "[" + movement_keys_desc + "] – move\n"
1198 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1203 content += this.mode.list_available_modes();
1205 if (!this.mode.has_input_prompt) {
1206 start_x = this.window_width
1208 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1209 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1210 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1211 terminal.write(y, start_x, lines[i]);
1214 toggle_tile_draw: function() {
1215 if (tui.tile_draw) {
1216 tui.tile_draw = false;
1218 tui.tile_draw = true;
1221 toggle_map_mode: function() {
1222 if (tui.map_mode == 'terrain only') {
1223 tui.map_mode = 'terrain + annotations';
1224 } else if (tui.map_mode == 'terrain + annotations') {
1225 tui.map_mode = 'terrain + things';
1226 } else if (tui.map_mode == 'terrain + things') {
1227 tui.map_mode = 'protections';
1228 } else if (tui.map_mode == 'protections') {
1229 tui.map_mode = 'terrain only';
1232 full_refresh: function() {
1234 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1235 this.recalc_input_lines();
1236 if (this.mode.is_intro) {
1237 this.draw_history();
1241 this.draw_turn_line();
1242 this.draw_mode_line();
1243 if (this.mode.shows_info) {
1246 this.draw_history();
1250 if (this.show_help) {
1262 this.map_control = "";
1263 this.map_size = [0,0];
1264 this.player_id = -1;
1268 get_thing: function(id_, create_if_not_found=false) {
1269 if (id_ in game.things) {
1270 return game.things[id_];
1271 } else if (create_if_not_found) {
1272 let t = new Thing([0,0]);
1273 game.things[id_] = t;
1277 move: function(start_position, direction) {
1278 let target = [start_position[0], start_position[1]];
1279 if (direction == 'LEFT') {
1281 } else if (direction == 'RIGHT') {
1283 } else if (game.map_geometry == 'Square') {
1284 if (direction == 'UP') {
1286 } else if (direction == 'DOWN') {
1289 } else if (game.map_geometry == 'Hex') {
1290 let start_indented = start_position[0] % 2;
1291 if (direction == 'UPLEFT') {
1293 if (!start_indented) {
1296 } else if (direction == 'UPRIGHT') {
1298 if (start_indented) {
1301 } else if (direction == 'DOWNLEFT') {
1303 if (!start_indented) {
1306 } else if (direction == 'DOWNRIGHT') {
1308 if (start_indented) {
1313 if (target[0] < 0 || target[1] < 0 ||
1314 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1319 teleport: function() {
1320 let player = this.get_thing(game.player_id);
1321 if (player.position in this.portals) {
1322 server.reconnect_to(this.portals[player.position]);
1324 terminal.blink_screen();
1325 tui.log_msg('? not standing on portal')
1333 server.init(websocket_location);
1339 move: function(direction) {
1340 let target = game.move(this.position, direction);
1342 this.position = target
1343 this.info_cached = false;
1344 if (tui.tile_draw) {
1345 this.send_tile_control_command();
1348 terminal.blink_screen();
1351 update_annotations: function(yx, str) {
1352 this.annotations[yx] = str;
1353 if (tui.mode.name == 'study') {
1357 empty_annotations: function() {
1358 this.annotations = {};
1359 if (tui.mode.name == 'study') {
1363 get_info: function() {
1364 if (this.info_cached) {
1365 return this.info_cached;
1367 let info_to_cache = '';
1368 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1369 if (game.fov[position_i] != '.') {
1370 info_to_cache += 'outside field of view';
1372 for (let t_id in game.things) {
1373 let t = game.things[t_id];
1374 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1375 info_to_cache += "THING: " + this.get_thing_info(t);
1376 let protection = t.protection;
1377 if (protection == '.') {
1378 protection = 'none';
1380 info_to_cache += " / protection: " + protection + "\n";
1382 info_to_cache += t.hat.slice(0, 6) + '\n';
1383 info_to_cache += t.hat.slice(6, 12) + '\n';
1384 info_to_cache += t.hat.slice(12, 18) + '\n';
1387 info_to_cache += t.face.slice(0, 6) + '\n';
1388 info_to_cache += t.face.slice(6, 12) + '\n';
1389 info_to_cache += t.face.slice(12, 18) + '\n';
1393 let terrain_char = game.map[position_i]
1394 let terrain_desc = '?'
1395 if (game.terrains[terrain_char]) {
1396 terrain_desc = game.terrains[terrain_char];
1398 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1399 let protection = game.map_control[position_i];
1400 if (protection == '.') {
1401 protection = 'unprotected';
1403 info_to_cache += 'PROTECTION: ' + protection + '\n';
1404 if (this.position in game.portals) {
1405 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1407 if (this.position in this.annotations) {
1408 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1411 this.info_cached = info_to_cache;
1412 return this.info_cached;
1414 get_thing_info: function(t) {
1415 const symbol = game.thing_types[t.type_];
1416 let info = t.type_ + " / " + symbol;
1418 info += t.thing_char;
1421 info += " (" + t.name_ + ")";
1424 info += " / installed";
1428 annotate: function(msg) {
1429 if (msg.length == 0) {
1430 msg = " "; // triggers annotation deletion
1432 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1434 set_portal: function(msg) {
1435 if (msg.length == 0) {
1436 msg = " "; // triggers portal deletion
1438 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1440 send_tile_control_command: function() {
1441 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1445 tui.inputEl.addEventListener('input', (event) => {
1446 if (tui.mode.has_input_prompt) {
1447 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1448 if (tui.inputEl.value.length > max_length) {
1449 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1451 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1452 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1453 tui.switch_mode('edit');
1457 document.onclick = function() {
1458 tui.show_help = false;
1460 tui.inputEl.addEventListener('keydown', (event) => {
1461 tui.show_help = false;
1462 if (event.key == 'Enter') {
1463 event.preventDefault();
1465 if (tui.mode.has_input_prompt && event.key == 'Enter'
1466 && tui.inputEl.value.length == 0
1467 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1468 'admin_enter'].includes(tui.mode.name)) {
1469 if (tui.mode.name != 'chat') {
1470 tui.log_msg('@ aborted');
1472 tui.switch_mode('play');
1473 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1474 tui.show_help = true;
1475 tui.inputEl.value = "";
1476 tui.restore_input_values();
1477 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1478 && !tui.mode.is_single_char_entry) {
1479 tui.show_help = true;
1480 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1481 tui.login_name = tui.inputEl.value;
1482 server.send(['LOGIN', tui.inputEl.value]);
1483 tui.inputEl.value = "";
1484 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1485 if (tui.inputEl.value.length != 18) {
1486 tui.log_msg('? wrong input length, aborting');
1488 server.send(['PLAYER_FACE', tui.inputEl.value]);
1490 tui.inputEl.value = "";
1491 tui.switch_mode('edit');
1492 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1493 server.send(['TASK:COMMAND', tui.inputEl.value]);
1494 tui.inputEl.value = "";
1495 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1496 tui.pick_selectable('PICK_UP');
1497 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1498 tui.pick_selectable('DROP');
1499 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1500 if (tui.inputEl.value.length == 0) {
1501 tui.log_msg('@ aborted');
1503 server.send(['SET_MAP_CONTROL_PASSWORD',
1504 tui.tile_control_char, tui.inputEl.value]);
1505 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1507 tui.switch_mode('admin');
1508 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1509 explorer.set_portal(tui.inputEl.value);
1510 tui.switch_mode('edit');
1511 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1512 if (tui.inputEl.value.length == 0) {
1513 tui.inputEl.value = " ";
1515 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1517 tui.switch_mode('edit');
1518 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1519 explorer.annotate(tui.inputEl.value);
1520 tui.switch_mode('edit');
1521 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1522 if (tui.inputEl.value.length == 0) {
1523 tui.inputEl.value = " ";
1525 tui.password = tui.inputEl.value
1526 tui.switch_mode('edit');
1527 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1528 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1529 tui.switch_mode('play');
1530 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1531 if (tui.inputEl.value.length != 1) {
1532 tui.log_msg('@ entered non-single-char, therefore aborted');
1533 tui.switch_mode('admin');
1535 tui.tile_control_char = tui.inputEl.value[0];
1536 tui.switch_mode('control_pw_pw');
1538 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1539 if (tui.inputEl.value.length != 1) {
1540 tui.log_msg('@ entered non-single-char, therefore aborted');
1541 tui.switch_mode('admin');
1543 tui.tile_control_char = tui.inputEl.value[0];
1544 tui.switch_mode('control_tile_draw');
1546 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1547 if (tui.inputEl.value.length != 1) {
1548 tui.log_msg('@ entered non-single-char, therefore aborted');
1550 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1551 tui.log_msg('@ sent new protection character for thing');
1553 tui.switch_mode('admin');
1554 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1555 let tokens = parser.tokenize(tui.inputEl.value);
1556 if (tokens.length > 0 && tokens[0].length > 0) {
1557 if (tui.inputEl.value[0][0] == '/') {
1558 if (tokens[0].slice(1) == 'nick') {
1559 if (tokens.length > 1) {
1560 server.send(['NICK', tokens[1]]);
1562 tui.log_msg('? need new name');
1565 tui.log_msg('? unknown command');
1568 server.send(['ALL', tui.inputEl.value]);
1570 } else if (tui.inputEl.valuelength > 0) {
1571 server.send(['ALL', tui.inputEl.value]);
1573 tui.inputEl.value = "";
1574 } else if (tui.mode.name == 'play') {
1575 if (tui.mode.mode_switch_on_key(event)) {
1577 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1578 server.send(["TASK:INTOXICATE"]);
1579 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1580 server.send(["TASK:DOOR"]);
1581 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1582 server.send(["TASK:INSTALL"]);
1583 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1584 server.send(["TASK:WEAR"]);
1585 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1586 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1587 } else if (event.key === tui.keys.teleport) {
1590 } else if (tui.mode.name == 'study') {
1591 if (tui.mode.mode_switch_on_key(event)) {
1593 } else if (event.key in tui.movement_keys) {
1594 explorer.move(tui.movement_keys[event.key]);
1595 } else if (event.key == tui.keys.toggle_map_mode) {
1596 tui.toggle_map_mode();
1598 } else if (tui.mode.name == 'control_tile_draw') {
1599 if (tui.mode.mode_switch_on_key(event)) {
1601 } else if (event.key in tui.movement_keys) {
1602 explorer.move(tui.movement_keys[event.key]);
1603 } else if (event.key === tui.keys.toggle_tile_draw) {
1604 tui.toggle_tile_draw();
1606 } else if (tui.mode.name == 'admin') {
1607 if (tui.mode.mode_switch_on_key(event)) {
1609 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1610 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1612 } else if (tui.mode.name == 'edit') {
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]]);
1617 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1618 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1619 } else if (event.key == tui.keys.toggle_map_mode) {
1620 tui.toggle_map_mode();
1626 rows_selector.addEventListener('input', function() {
1627 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1630 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1631 terminal.initialize();
1634 cols_selector.addEventListener('input', function() {
1635 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1638 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1639 terminal.initialize();
1640 tui.window_width = terminal.cols / 2,
1643 for (let key_selector of key_selectors) {
1644 key_selector.addEventListener('input', function() {
1645 window.localStorage.setItem(key_selector.id, key_selector.value);
1649 window.setInterval(function() {
1650 if (server.connected) {
1651 server.send(['PING']);
1653 server.reconnect_to(server.url);
1654 tui.log_msg('@ attempting reconnect …')
1657 window.setInterval(function() {
1659 let span_decoration = "none";
1660 if (document.activeElement == tui.inputEl) {
1661 val = "on (click outside terminal to change)";
1663 val = "off (click into terminal to change)";
1664 span_decoration = "line-through";
1666 document.getElementById("keyboard_control").textContent = val;
1667 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1668 span.style.textDecoration = span_decoration;
1671 document.getElementById("terminal").onclick = function() {
1672 tui.inputEl.focus();
1674 document.getElementById("help").onclick = function() {
1675 tui.show_help = true;
1678 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1679 const mode = switchEl.id.slice("switch_to_".length);
1680 switchEl.onclick = function() {
1681 tui.switch_mode(mode);
1685 document.getElementById("toggle_tile_draw").onclick = function() {
1686 tui.toggle_tile_draw();
1688 document.getElementById("toggle_map_mode").onclick = function() {
1689 tui.toggle_map_mode();
1692 document.getElementById("flatten").onclick = function() {
1693 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1695 document.getElementById("door").onclick = function() {
1696 server.send(['TASK:DOOR']);
1698 document.getElementById("consume").onclick = function() {
1699 server.send(['TASK:INTOXICATE']);
1701 document.getElementById("install").onclick = function() {
1702 server.send(['TASK:INSTALL']);
1704 document.getElementById("wear").onclick = function() {
1705 server.send(['TASK:WEAR']);
1707 document.getElementById("teleport").onclick = function() {
1710 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1711 if (move_button.id.startsWith('key_')) { // not a move button
1714 let direction = move_button.id.split('_')[2].toUpperCase();
1715 move_button.onclick = function() {
1716 if (tui.mode.available_actions.includes("move")) {
1717 server.send(['TASK:MOVE', direction]);
1718 } else if (tui.mode.available_actions.includes("move_explorer")) {
1719 explorer.move(direction);