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>
19 <h3>button controls for hard-to-remember keybindings</h3>
20 <table id="move_table" style="float: left">
22 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
23 <td style="text-align: center"><button id="square_move_up"></button></td>
24 <td><button id="hex_move_upright"></button></td>
27 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
28 <td stlye="text-align: center;">move</td>
29 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
32 <td><button id="hex_move_downleft"></button></td>
33 <td style="text-align: center"><button id="square_move_down"></button></td>
34 <td><button id="hex_move_downright"></button></td>
39 <td><button id="help"></button></td>
42 <td><button id="switch_to_chat"></button><br /></td>
45 <td><button id="switch_to_study"></button></td>
46 <td><button id="toggle_map_mode"></button>
49 <td><button id="switch_to_play"></button></td>
51 <button id="switch_to_take_thing"></button>
52 <button id="switch_to_drop_thing"></button>
53 <button id="door"></button>
54 <button id="consume"></button>
55 <button id="switch_to_command_thing"></button>
56 <button id="teleport"></button>
57 <button id="wear"></button>
58 <button id="spin"></button>
62 <td><button id="switch_to_edit"></button></td>
64 <button id="switch_to_write"></button>
65 <button id="flatten"></button>
66 <button id="install"></button>
67 <button id="switch_to_annotate"></button>
68 <button id="switch_to_portal"></button>
69 <button id="switch_to_name_thing"></button>
70 <button id="switch_to_password"></button>
71 <button id="switch_to_enter_face"></button>
75 <td><button id="switch_to_admin_enter"></button></td>
77 <button id="switch_to_control_pw_type"></button>
78 <button id="switch_to_control_tile_type"></button>
79 <button id="switch_to_admin_thing_protect"></button>
80 <button id="toggle_tile_draw"></button>
85 <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 />
87 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
88 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
89 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
90 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
91 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
92 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
93 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
94 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
95 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
96 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
97 <li>help: <input id="key_help" type="text" value="h" />
98 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
99 <li>teleport: <input id="key_teleport" type="text" value="p" />
100 <li>spin: <input id="key_spin" type="text" value="S" />
101 <li>open/close: <input id="key_door" type="text" value="D" />
102 <li>consume: <input id="key_consume" type="text" value="C" />
103 <li>install: <input id="key_install" type="text" value="I" />
104 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
105 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
106 <li><input id="key_switch_to_enter_face" type="text" value="f" />
107 <li><input id="key_switch_to_take_thing" type="text" value="z" />
108 <li><input id="key_switch_to_chat" type="text" value="t" />
109 <li><input id="key_switch_to_play" type="text" value="p" />
110 <li><input id="key_switch_to_study" type="text" value="?" />
111 <li><input id="key_switch_to_edit" type="text" value="E" />
112 <li><input id="key_switch_to_write" type="text" value="m" />
113 <li><input id="key_switch_to_name_thing" type="text" value="N" />
114 <li><input id="key_switch_to_command_thing" type="text" value="O" />
115 <li><input id="key_switch_to_password" type="text" value="P" />
116 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
117 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
118 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
119 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
120 <li><input id="key_switch_to_annotate" type="text" value="M" />
121 <li><input id="key_switch_to_portal" type="text" value="T" />
122 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
123 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
128 let websocket_location = "wss://plomlompom.com/rogue_chat/";
129 //let websocket_location = "ws://localhost:8000/";
135 'long': 'This mode allows you to interact with the map in various ways.'
140 '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.'},
142 'short': 'world edit',
144 '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.'
147 'short': 'name thing',
149 'long': 'Give name to/change name of thing here.'
152 'short': 'command thing',
154 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
157 'short': 'take thing',
158 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
159 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
162 'short': 'drop thing',
163 'intro': 'Enter number of direction to which you want to drop thing.',
164 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
166 'admin_thing_protect': {
167 'short': 'change thing protection',
168 'intro': '@ enter thing protection character:',
169 'long': 'Change protection character for thing here.'
172 'short': 'enter your face',
173 'intro': '@ enter face line (enter nothing to abort):',
174 '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..'
177 'short': 'change terrain',
179 '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.'
182 'short': 'change protection character password',
183 'intro': '@ enter protection character for which you want to change the password:',
184 '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.'
187 'short': 'change protection character password',
189 '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.'
191 'control_tile_type': {
192 'short': 'change tiles protection',
193 'intro': '@ enter protection character which you want to draw:',
194 '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.'
196 'control_tile_draw': {
197 'short': 'change tiles protection',
199 '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.'
202 'short': 'annotate tile',
204 '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.'
207 'short': 'edit portal',
209 '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.'
214 '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'
219 'long': 'Enter your player name.'
221 'waiting_for_server': {
222 'short': 'waiting for server response',
223 'intro': '@ waiting for server …',
224 'long': 'Waiting for a server response.'
227 'short': 'waiting for server response',
229 'long': 'Waiting for a server response.'
232 'short': 'set world edit password',
234 '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.'
237 'short': 'become admin',
238 'intro': '@ enter admin password:',
239 'long': 'This mode allows you to become admin if you know an admin password.'
244 'long': 'This mode allows you access to actions limited to administrators.'
247 let key_descriptions = {
249 'flatten': 'flatten surroundings',
250 'teleport': 'teleport',
251 'door': 'open/close',
252 'consume': 'consume',
253 '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 game.thing_types = {};
456 server.send(['TASKS']);
457 server.send(['TERRAINS']);
458 server.send(['THING_TYPES']);
459 tui.log_msg("@ server connected! :)");
460 tui.switch_mode('login');
462 this.websocket.onclose = function(event) {
463 tui.switch_mode('waiting_for_server');
464 tui.log_msg("@ server disconnected :(");
466 this.websocket.onmessage = this.handle_event;
468 reconnect_to: function(url) {
469 this.websocket.close();
472 send: function(tokens) {
473 this.websocket.send(unparser.untokenize(tokens));
475 handle_event: function(event) {
476 let tokens = parser.tokenize(event.data);
477 if (tokens[0] === 'TURN') {
478 game.turn_complete = false;
479 game.turn = parseInt(tokens[1]);
480 } else if (tokens[0] === 'PSEUDO_FOV_WIPE') {
481 game.portals_new = {};
482 explorer.annotations_new = {};
483 game.things_new = [];
484 } else if (tokens[0] === 'THING') {
485 let t = game.get_thing_temp(tokens[4], true);
486 t.position = parser.parse_yx(tokens[1]);
488 t.protection = tokens[3];
489 t.portable = parseInt(tokens[5]);
490 t.commandable = parseInt(tokens[6]);
491 } else if (tokens[0] === 'THING_NAME') {
492 let t = game.get_thing_temp(tokens[1]);
494 } else if (tokens[0] === 'THING_FACE') {
495 let t = game.get_thing_temp(tokens[1]);
497 } else if (tokens[0] === 'THING_HAT') {
498 let t = game.get_thing_temp(tokens[1]);
500 } else if (tokens[0] === 'THING_CHAR') {
501 let t = game.get_thing_temp(tokens[1]);
502 t.thing_char = tokens[2];
503 } else if (tokens[0] === 'TASKS') {
504 game.tasks = tokens[1].split(',');
505 tui.mode_write.legal = game.tasks.includes('WRITE');
506 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
507 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
508 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
509 } else if (tokens[0] === 'THING_TYPE') {
510 game.thing_types[tokens[1]] = tokens[2]
511 } else if (tokens[0] === 'THING_CARRYING') {
512 let t = game.get_thing_temp(tokens[1]);
513 t.carrying = game.get_thing(tokens[2], false);
514 } else if (tokens[0] === 'THING_INSTALLED') {
515 let t = game.get_thing_temp(tokens[1]);
517 } else if (tokens[0] === 'TERRAIN') {
518 game.terrains[tokens[1]] = tokens[2]
519 } else if (tokens[0] === 'MAP') {
520 game.map_geometry_new = tokens[1];
521 game.map_size_new = parser.parse_yx(tokens[2]);
522 game.map_new = tokens[3]
523 } else if (tokens[0] === 'FOV') {
524 game.fov_new = tokens[1]
525 } else if (tokens[0] === 'MAP_CONTROL') {
526 game.map_control_new = tokens[1]
527 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
528 game.portals = game.portals_new;
529 game.map_geometry = game.map_geometry_new;
530 game.map_size = game.map_size_new;
531 game.map = game.map_new;
533 game.map_control = game.map_control_new;
534 explorer.annotations = explorer.annotations_new;
535 explorer.info_cached = false;
536 game.things = game.things_new;
537 game.player = game.things[game.player_id];
538 game.turn_complete = true;
539 if (tui.mode.name == 'post_login_wait') {
540 tui.switch_mode('play');
544 } else if (tokens[0] === 'CHAT') {
545 tui.log_msg('# ' + tokens[1], 1);
546 } else if (tokens[0] === 'CHATFACE') {
547 tui.draw_face = tokens[1];
549 } else if (tokens[0] === 'REPLY') {
550 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
551 } else if (tokens[0] === 'PLAYER_ID') {
552 game.player_id = parseInt(tokens[1]);
553 } else if (tokens[0] === 'LOGIN_OK') {
554 this.send(['GET_GAMESTATE']);
555 tui.switch_mode('post_login_wait');
556 } else if (tokens[0] === 'DEFAULT_COLORS') {
557 terminal.set_default_colors();
558 } else if (tokens[0] === 'RANDOM_COLORS') {
559 terminal.set_random_colors();
560 } else if (tokens[0] === 'ADMIN_OK') {
562 tui.log_msg('@ you now have admin rights');
563 tui.switch_mode('admin');
564 } else if (tokens[0] === 'PORTAL') {
565 let position = parser.parse_yx(tokens[1]);
566 game.portals_new[position] = tokens[2];
567 } else if (tokens[0] === 'ANNOTATION') {
568 let position = parser.parse_yx(tokens[1]);
569 explorer.annotations_new[position] = tokens[2];
570 } else if (tokens[0] === 'UNHANDLED_INPUT') {
571 tui.log_msg('? unknown command');
572 } else if (tokens[0] === 'PLAY_ERROR') {
573 tui.log_msg('? ' + tokens[1]);
574 terminal.blink_screen();
575 } else if (tokens[0] === 'ARGUMENT_ERROR') {
576 tui.log_msg('? syntax error: ' + tokens[1]);
577 } else if (tokens[0] === 'GAME_ERROR') {
578 tui.log_msg('? game error: ' + tokens[1]);
579 } else if (tokens[0] === 'PONG') {
582 tui.log_msg('? unhandled input: ' + event.data);
588 quote: function(str) {
590 for (let i = 0; i < str.length; i++) {
592 if (['"', '\\'].includes(c)) {
598 return quoted.join('');
600 to_yx: function(yx_coordinate) {
601 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
603 untokenize: function(tokens) {
604 let quoted_tokens = [];
605 for (let token of tokens) {
606 quoted_tokens.push(this.quote(token));
608 return quoted_tokens.join(" ");
613 constructor(name, has_input_prompt=false, shows_info=false,
614 is_intro=false, is_single_char_entry=false) {
616 this.short_desc = mode_helps[name].short;
617 this.available_modes = [];
618 this.available_actions = [];
619 this.has_input_prompt = has_input_prompt;
620 this.shows_info= shows_info;
621 this.is_intro = is_intro;
622 this.help_intro = mode_helps[name].long;
623 this.intro_msg = mode_helps[name].intro;
624 this.is_single_char_entry = is_single_char_entry;
627 *iter_available_modes() {
628 for (let mode_name of this.available_modes) {
629 let mode = tui['mode_' + mode_name];
633 let key = tui.keys['switch_to_' + mode.name];
637 list_available_modes() {
639 if (this.available_modes.length > 0) {
640 msg += 'Other modes available from here:\n';
641 for (let [mode, key] of this.iter_available_modes()) {
642 msg += '[' + key + '] – ' + mode.short_desc + '\n';
647 mode_switch_on_key(key_event) {
648 for (let [mode, key] of this.iter_available_modes()) {
649 if (key_event.key == key) {
650 event.preventDefault();
651 tui.switch_mode(mode.name);
663 window_width: terminal.cols / 2,
671 mode_waiting_for_server: new Mode('waiting_for_server',
673 mode_login: new Mode('login', true, false, true),
674 mode_post_login_wait: new Mode('post_login_wait'),
675 mode_chat: new Mode('chat', true),
676 mode_annotate: new Mode('annotate', true, true),
677 mode_play: new Mode('play'),
678 mode_study: new Mode('study', false, true),
679 mode_write: new Mode('write', false, false, false, true),
680 mode_edit: new Mode('edit'),
681 mode_control_pw_type: new Mode('control_pw_type', true),
682 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
683 mode_portal: new Mode('portal', true, true),
684 mode_password: new Mode('password', true),
685 mode_name_thing: new Mode('name_thing', true, true),
686 mode_command_thing: new Mode('command_thing', true),
687 mode_take_thing: new Mode('take_thing', true),
688 mode_drop_thing: new Mode('drop_thing', true),
689 mode_enter_face: new Mode('enter_face', true),
690 mode_admin_enter: new Mode('admin_enter', true),
691 mode_admin: new Mode('admin'),
692 mode_control_pw_pw: new Mode('control_pw_pw', true),
693 mode_control_tile_type: new Mode('control_tile_type', true),
694 mode_control_tile_draw: new Mode('control_tile_draw'),
696 'flatten': 'FLATTEN_SURROUNDINGS',
697 'take_thing': 'PICK_UP',
698 'drop_thing': 'DROP',
701 'install': 'INSTALL',
703 'command': 'COMMAND',
704 'consume': 'INTOXICATE',
712 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
713 "command_thing", "take_thing", "drop_thing"]
714 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
716 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
717 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
718 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
719 "control_tile_type", "chat",
720 "study", "play", "edit"]
721 this.mode_admin.available_actions = ["move"];
722 this.mode_control_tile_draw.available_modes = ["admin_enter"]
723 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
724 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
725 "password", "chat", "study", "play",
726 "admin_enter", "enter_face"]
727 this.mode_edit.available_actions = ["move", "flatten", "install",
729 this.inputEl = document.getElementById("input");
730 this.inputEl.focus();
731 this.switch_mode('waiting_for_server');
732 this.recalc_input_lines();
733 this.height_header = this.height_turn_line + this.height_mode_line;
736 init_keys: function() {
737 document.getElementById("move_table").hidden = true;
739 for (let key_selector of key_selectors) {
740 this.keys[key_selector.id.slice(4)] = key_selector.value;
742 this.movement_keys = {};
743 let geometry_prefix = 'undefinedMapGeometry_';
744 if (game.map_geometry) {
745 geometry_prefix = game.map_geometry.toLowerCase() + '_';
747 for (const key_name of Object.keys(key_descriptions)) {
748 if (key_name.startsWith(geometry_prefix)) {
749 let direction = key_name.split('_')[2].toUpperCase();
750 let key = this.keys[key_name];
751 this.movement_keys[key] = direction;
754 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
755 if (move_button.id.startsWith('key_')) {
758 move_button.hidden = true;
760 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
761 document.getElementById("move_table").hidden = false;
762 move_button.hidden = false;
764 for (let el of document.getElementsByTagName("button")) {
765 let action_desc = key_descriptions[el.id];
766 let action_key = '[' + this.keys[el.id] + ']';
767 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
770 task_action_on: function(action) {
771 return game.tasks.includes(this.action_tasks[action]);
773 switch_mode: function(mode_name) {
775 function fail(msg, return_mode) {
776 tui.log_msg('? ' + msg);
777 terminal.blink_screen();
778 tui.switch_mode(return_mode);
781 if (this.mode && this.mode.name == 'control_tile_draw') {
782 tui.log_msg('@ finished tile protection drawing.')
784 this.draw_face = false;
785 this.tile_draw = false;
786 if (mode_name == 'command_thing' && (!game.player.carrying
787 || !game.player.carrying.commandable)) {
788 return fail('not carrying anything commandable', 'play');
790 if (mode_name == 'take_thing' && game.player.carrying) {
791 return fail('already carrying something', 'play');
793 if (mode_name == 'drop_thing' && !game.player.carrying) {
794 return fail('not carrying anything droppable', 'play');
796 if (mode_name == 'admin_enter' && this.is_admin) {
798 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
800 for (let t_id in game.things) {
801 if (t_id == game.player_id) {
804 let t = game.things[t_id];
805 if (game.player.position[0] == t.position[0]
806 && game.player.position[1] == t.position[1]) {
812 return fail('not standing over thing', 'fail');
814 this.selected_thing_id = thing_id;
817 this.mode = this['mode_' + mode_name];
818 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
819 this.map_mode = 'protections';
820 } else if (this.mode.name != "edit") {
821 this.map_mode = 'terrain + things';
823 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
824 this.inputEl.focus();
826 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
827 explorer.position = game.player.position;
829 this.inputEl.value = "";
830 this.restore_input_values();
831 for (let el of document.getElementsByTagName("button")) {
834 document.getElementById("help").disabled = false;
835 for (const action of this.mode.available_actions) {
836 if (["move", "move_explorer"].includes(action)) {
837 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
838 move_key.disabled = false;
840 } else if (Object.keys(this.action_tasks).includes(action)) {
841 if (this.task_action_on(action)) {
842 document.getElementById(action).disabled = false;
845 document.getElementById(action).disabled = false;
848 for (const mode_name of this.mode.available_modes) {
849 document.getElementById('switch_to_' + mode_name).disabled = false;
851 if (this.mode.intro_msg.length > 0) {
852 this.log_msg(this.mode.intro_msg);
854 if (this.mode.name == 'login') {
855 if (this.login_name) {
856 server.send(['LOGIN', this.login_name]);
858 this.log_msg("? need login name");
860 } else if (this.mode.is_single_char_entry) {
861 this.show_help = true;
862 } else if (this.mode.name == 'take_thing') {
863 this.log_msg("Portable things in reach for pick-up:");
864 const y = game.player.position[0]
865 const x = game.player.position[1]
866 let select_range = [y.toString() + ':' + x.toString(),
867 (y + 0).toString() + ':' + (x - 1).toString(),
868 (y + 0).toString() + ':' + (x + 1).toString(),
869 (y - 1).toString() + ':' + (x).toString(),
870 (y + 1).toString() + ':' + (x).toString()];
871 if (game.map_geometry == 'Hex') {
873 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
874 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
876 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
877 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
880 this.selectables = [];
881 for (const t_id in game.things) {
882 const t = game.things[t_id];
883 if (select_range.includes(t.position[0].toString()
884 + ':' + t.position[1].toString())
886 this.selectables.push(t_id);
889 if (this.selectables.length == 0) {
890 this.log_msg('none');
891 terminal.blink_screen();
892 this.switch_mode('play');
895 for (let [i, t_id] of this.selectables.entries()) {
896 const t = game.things[t_id];
897 this.log_msg(i + ': ' + explorer.get_thing_info(t));
900 } else if (this.mode.name == 'drop_thing') {
901 this.log_msg('Direction to drop thing to:');
902 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
903 for (let [i, direction] of this.selectables.entries()) {
904 this.log_msg(i + ': ' + direction);
906 } else if (this.mode.name == 'command_thing') {
907 server.send(['TASK:COMMAND', 'HELP']);
908 } else if (this.mode.name == 'control_pw_pw') {
909 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
910 } else if (this.mode.name == 'control_tile_draw') {
911 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 + '].')
915 offset_links: function(offset, links) {
916 for (let y in links) {
917 let real_y = offset[0] + parseInt(y);
918 if (!this.links[real_y]) {
919 this.links[real_y] = [];
921 for (let link of links[y]) {
922 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
923 this.links[real_y].push(offset_link);
927 restore_input_values: function() {
928 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
929 let info = explorer.annotations[explorer.position];
930 if (info != "(none)") {
931 this.inputEl.value = info;
933 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
934 let portal = game.portals[explorer.position]
935 this.inputEl.value = portal;
936 } else if (this.mode.name == 'password') {
937 this.inputEl.value = this.password;
938 } else if (this.mode.name == 'name_thing') {
939 let t = game.get_thing(this.selected_thing_id);
941 this.inputEl.value = t.name_;
943 } else if (this.mode.name == 'admin_thing_protect') {
944 let t = game.get_thing(this.selected_thing_id);
945 if (t && t.protection) {
946 this.inputEl.value = t.protection;
950 recalc_input_lines: function() {
951 if (this.mode.has_input_prompt) {
953 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
955 this.input_lines = [];
957 this.height_input = this.input_lines.length;
959 msg_into_lines_of_width: function(msg, width) {
960 function push_inner_link(y, end_x) {
961 if (!inner_links[y]) {
964 inner_links[y].push([url_start_x, end_x, url]);
966 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
969 for (const match of matches) {
970 const url = match[0];
971 const url_start = match.index;
972 const url_end = match.index + match[0].length;
973 link_data[url_start] = url;
974 url_ends.push(url_end);
978 let inner_links = {};
982 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
983 if (x >= width || msg[i] == "\n") {
985 push_inner_link(y, chunk.length);
987 if (url_ends[0] == i) {
995 if (msg[i] == "\n") {
1000 if (msg[i] != "\n") {
1003 if (i in link_data) {
1007 } else if (url_ends[0] == i) {
1009 push_inner_link(y, x);
1015 push_inner_link(lines.length - 1, chunk.length);
1017 return [lines, inner_links];
1019 log_msg: function(msg) {
1021 while (this.log.length > 100) {
1024 this.full_refresh();
1026 pick_selectable: function(task_name) {
1027 const i = parseInt(this.inputEl.value);
1028 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1029 tui.log_msg('? invalid index, aborted');
1031 server.send(['TASK:' + task_name, tui.selectables[i]]);
1033 this.inputEl.value = "";
1034 this.switch_mode('play');
1036 draw_map: function() {
1037 if (!game.turn_complete && this.map_lines.length == 0) {
1040 if (game.turn_complete) {
1041 let map_lines_split = [];
1043 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1044 if (j == game.map_size[1]) {
1045 map_lines_split.push(line);
1049 if (this.map_mode == 'protections') {
1050 line.push(game.map_control[i] + ' ');
1052 line.push(game.map[i] + ' ');
1055 map_lines_split.push(line);
1056 if (this.map_mode == 'terrain + annotations') {
1057 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1058 const yx = coordinate.split(',')
1059 map_lines_split[yx[0]][yx[1]] = 'A ';
1061 } else if (this.map_mode == 'terrain + things') {
1062 for (const p in game.portals) {
1063 let coordinate = p.split(',')
1064 let original = map_lines_split[coordinate[0]][coordinate[1]];
1065 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1067 let used_positions = [];
1068 function draw_thing(t, used_positions) {
1069 let symbol = game.thing_types[t.type_];
1070 let meta_char = ' ';
1072 meta_char = t.thing_char;
1074 if (used_positions.includes(t.position.toString())) {
1080 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1081 used_positions.push(t.position.toString());
1083 for (const thing_id in game.things) {
1084 let t = game.things[thing_id];
1085 if (t.type_ != 'Player') {
1086 draw_thing(t, used_positions);
1089 for (const thing_id in game.things) {
1090 let t = game.things[thing_id];
1091 if (t.type_ == 'Player') {
1092 draw_thing(t, used_positions);
1096 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1097 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1098 } else if (tui.map_mode != 'terrain + things') {
1099 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1102 if (game.map_geometry == 'Square') {
1103 for (let line_split of map_lines_split) {
1104 this.map_lines.push(line_split.join(''));
1106 } else if (game.map_geometry == 'Hex') {
1108 for (let line_split of map_lines_split) {
1109 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1117 let window_center = [terminal.rows / 2, this.window_width / 2];
1118 let center_position = [game.player.position[0], game.player.position[1]];
1119 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1120 center_position = [explorer.position[0], explorer.position[1]];
1122 center_position[1] = center_position[1] * 2;
1123 this.offset = [center_position[0] - window_center[0],
1124 center_position[1] - window_center[1]]
1125 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1126 this.offset[1] += 1;
1129 let term_y = Math.max(0, -this.offset[0]);
1130 let term_x = Math.max(0, -this.offset[1]);
1131 let map_y = Math.max(0, this.offset[0]);
1132 let map_x = Math.max(0, this.offset[1]);
1133 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1134 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1135 terminal.write(term_y, term_x, to_draw);
1138 draw_face_popup: function() {
1139 const t = game.things[this.draw_face];
1140 if (!t || !t.face) {
1141 this.draw_face = false;
1144 const start_x = tui.window_width - 10;
1147 t_char = t.thing_char;
1149 function draw_body_part(body_part, end_y) {
1150 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1151 terminal.write(end_y - 3, start_x, '| |');
1152 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1153 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1154 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1157 draw_body_part(t.face, terminal.rows - 2);
1160 draw_body_part(t.hat, terminal.rows - 5);
1162 terminal.write(terminal.rows - 1, start_x, '| |');
1164 draw_mode_line: function() {
1165 let help = 'hit [' + this.keys.help + '] for help';
1166 if (this.mode.has_input_prompt) {
1167 help = 'enter /help for help';
1169 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1171 draw_turn_line: function(n) {
1172 if (game.turn_complete) {
1173 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1176 draw_history: function() {
1177 let log_display_lines = [];
1179 let y_offset_in_log = 0;
1180 for (let line of this.log) {
1181 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1183 log_display_lines = log_display_lines.concat(new_lines);
1184 for (const y in link_data) {
1185 const rel_y = y_offset_in_log + parseInt(y);
1186 log_links[rel_y] = [];
1187 for (let link of link_data[y]) {
1188 log_links[rel_y].push(link);
1191 y_offset_in_log += new_lines.length;
1193 let i = log_display_lines.length - 1;
1194 for (let y = terminal.rows - 1 - this.height_input;
1195 y >= this.height_header && i >= 0;
1197 terminal.write(y, this.window_width, log_display_lines[i]);
1199 for (const key of Object.keys(log_links)) {
1200 if (parseInt(key) <= i) {
1201 delete log_links[key];
1204 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1206 this.offset_links(offset, log_links);
1208 draw_info: function() {
1209 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1210 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1211 let offset = [this.height_header, this.window_width];
1212 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1213 terminal.write(y, offset[1], lines[i]);
1215 this.offset_links(offset, link_data);
1217 draw_input: function() {
1218 if (this.mode.has_input_prompt) {
1219 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1220 terminal.write(y, this.window_width, this.input_lines[i]);
1224 draw_help: function() {
1225 let movement_keys_desc = '';
1226 if (!this.mode.is_intro) {
1227 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1229 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1230 if (this.mode.available_actions.length > 0) {
1231 content += "Available actions:\n";
1232 for (let action of this.mode.available_actions) {
1233 if (Object.keys(this.action_tasks).includes(action)) {
1234 if (!this.task_action_on(action)) {
1238 if (action == 'move_explorer') {
1241 if (action == 'move') {
1242 content += "[" + movement_keys_desc + "] – move\n"
1244 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1249 content += this.mode.list_available_modes();
1251 if (!this.mode.has_input_prompt) {
1252 start_x = this.window_width;
1253 this.draw_links = false;
1255 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1256 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1257 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1258 terminal.write(y, start_x, lines[i]);
1261 toggle_tile_draw: function() {
1262 if (tui.tile_draw) {
1263 tui.tile_draw = false;
1265 tui.tile_draw = true;
1268 toggle_map_mode: function() {
1269 if (tui.map_mode == 'terrain only') {
1270 tui.map_mode = 'terrain + annotations';
1271 } else if (tui.map_mode == 'terrain + annotations') {
1272 tui.map_mode = 'terrain + things';
1273 } else if (tui.map_mode == 'terrain + things') {
1274 tui.map_mode = 'protections';
1275 } else if (tui.map_mode == 'protections') {
1276 tui.map_mode = 'terrain only';
1279 full_refresh: function() {
1280 this.draw_links = true;
1282 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1283 this.recalc_input_lines();
1284 if (this.mode.is_intro) {
1285 this.draw_history();
1289 this.draw_turn_line();
1290 this.draw_mode_line();
1291 if (this.mode.shows_info) {
1294 this.draw_history();
1298 if (this.show_help) {
1301 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1302 this.draw_face_popup();
1304 if (!this.draw_links) {
1314 this.player_id = -1;
1317 this.things_new = {};
1322 this.map_control = "";
1323 this.map_control_new = "";
1324 this.map_size = [0,0];
1325 this.map_size_new = [0,0];
1327 this.portals_new = {};
1329 get_thing_temp: function(id_, create_if_not_found=false) {
1330 if (id_ in game.things_new) {
1331 return game.things_new[id_];
1332 } else if (create_if_not_found) {
1333 let t = new Thing([0,0]);
1334 game.things_new[id_] = t;
1338 get_thing: function(id_, create_if_not_found=false) {
1339 if (id_ in game.things) {
1340 return game.things[id_];
1343 move: function(start_position, direction) {
1344 let target = [start_position[0], start_position[1]];
1345 if (direction == 'LEFT') {
1347 } else if (direction == 'RIGHT') {
1349 } else if (game.map_geometry == 'Square') {
1350 if (direction == 'UP') {
1352 } else if (direction == 'DOWN') {
1355 } else if (game.map_geometry == 'Hex') {
1356 let start_indented = start_position[0] % 2;
1357 if (direction == 'UPLEFT') {
1359 if (!start_indented) {
1362 } else if (direction == 'UPRIGHT') {
1364 if (start_indented) {
1367 } else if (direction == 'DOWNLEFT') {
1369 if (!start_indented) {
1372 } else if (direction == 'DOWNRIGHT') {
1374 if (start_indented) {
1379 if (target[0] < 0 || target[1] < 0 ||
1380 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1385 teleport: function() {
1386 if (game.player.position in this.portals) {
1387 server.reconnect_to(this.portals[game.player.position]);
1389 terminal.blink_screen();
1390 tui.log_msg('? not standing on portal')
1398 server.init(websocket_location);
1403 annotations_new: {},
1405 move: function(direction) {
1406 let target = game.move(this.position, direction);
1408 this.position = target
1409 this.info_cached = false;
1410 if (tui.tile_draw) {
1411 this.send_tile_control_command();
1414 terminal.blink_screen();
1417 get_info: function() {
1418 if (this.info_cached) {
1419 return this.info_cached;
1421 let info_to_cache = '';
1422 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1423 if (game.fov[position_i] != '.') {
1424 info_to_cache += 'outside field of view';
1426 for (let t_id in game.things) {
1427 let t = game.things[t_id];
1428 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1429 info_to_cache += "THING: " + this.get_thing_info(t);
1430 let protection = t.protection;
1431 if (protection == '.') {
1432 protection = 'none';
1434 info_to_cache += " / protection: " + protection + "\n";
1436 info_to_cache += t.hat.slice(0, 6) + '\n';
1437 info_to_cache += t.hat.slice(6, 12) + '\n';
1438 info_to_cache += t.hat.slice(12, 18) + '\n';
1441 info_to_cache += t.face.slice(0, 6) + '\n';
1442 info_to_cache += t.face.slice(6, 12) + '\n';
1443 info_to_cache += t.face.slice(12, 18) + '\n';
1447 let terrain_char = game.map[position_i]
1448 let terrain_desc = '?'
1449 if (game.terrains[terrain_char]) {
1450 terrain_desc = game.terrains[terrain_char];
1452 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1453 let protection = game.map_control[position_i];
1454 if (protection == '.') {
1455 protection = 'unprotected';
1457 info_to_cache += 'PROTECTION: ' + protection + '\n';
1458 if (this.position in game.portals) {
1459 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1461 if (this.position in this.annotations) {
1462 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1465 this.info_cached = info_to_cache;
1466 return this.info_cached;
1468 get_thing_info: function(t) {
1469 const symbol = game.thing_types[t.type_];
1470 let info = t.type_ + " / " + symbol;
1472 info += t.thing_char;
1475 info += " (" + t.name_ + ")";
1478 info += " / installed";
1482 annotate: function(msg) {
1483 if (msg.length == 0) {
1484 msg = " "; // triggers annotation deletion
1486 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1488 set_portal: function(msg) {
1489 if (msg.length == 0) {
1490 msg = " "; // triggers portal deletion
1492 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1494 send_tile_control_command: function() {
1495 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1499 tui.inputEl.addEventListener('input', (event) => {
1500 if (tui.mode.has_input_prompt) {
1501 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1502 if (tui.inputEl.value.length > max_length) {
1503 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1505 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1506 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1507 tui.switch_mode('edit');
1511 document.onclick = function() {
1512 if (!tui.mode.is_single_char_entry) {
1513 tui.show_help = false;
1516 tui.inputEl.addEventListener('keydown', (event) => {
1517 tui.show_help = false;
1518 if (event.key == 'Enter') {
1519 event.preventDefault();
1521 if ((!tui.mode.is_intro && event.key == 'Escape')
1522 || (tui.mode.has_input_prompt && event.key == 'Enter'
1523 && tui.inputEl.value.length == 0
1524 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1525 'admin_enter'].includes(tui.mode.name))) {
1526 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1527 tui.log_msg('@ aborted');
1529 tui.switch_mode('play');
1530 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1531 tui.show_help = true;
1532 tui.inputEl.value = "";
1533 tui.restore_input_values();
1534 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1535 && !tui.mode.is_single_char_entry) {
1536 tui.show_help = true;
1537 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1538 tui.login_name = tui.inputEl.value;
1539 server.send(['LOGIN', tui.inputEl.value]);
1540 tui.inputEl.value = "";
1541 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1542 if (tui.inputEl.value.length != 18) {
1543 tui.log_msg('? wrong input length, aborting');
1545 server.send(['PLAYER_FACE', tui.inputEl.value]);
1547 tui.inputEl.value = "";
1548 tui.switch_mode('edit');
1549 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1550 server.send(['TASK:COMMAND', tui.inputEl.value]);
1551 tui.inputEl.value = "";
1552 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1553 tui.pick_selectable('PICK_UP');
1554 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1555 tui.pick_selectable('DROP');
1556 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1557 if (tui.inputEl.value.length == 0) {
1558 tui.log_msg('@ aborted');
1560 server.send(['SET_MAP_CONTROL_PASSWORD',
1561 tui.tile_control_char, tui.inputEl.value]);
1562 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1564 tui.switch_mode('admin');
1565 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1566 explorer.set_portal(tui.inputEl.value);
1567 tui.switch_mode('edit');
1568 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1569 if (tui.inputEl.value.length == 0) {
1570 tui.inputEl.value = " ";
1572 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1574 tui.switch_mode('edit');
1575 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1576 explorer.annotate(tui.inputEl.value);
1577 tui.switch_mode('edit');
1578 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1579 if (tui.inputEl.value.length == 0) {
1580 tui.inputEl.value = " ";
1582 tui.password = tui.inputEl.value
1583 tui.switch_mode('edit');
1584 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1585 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1586 tui.switch_mode('play');
1587 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1588 if (tui.inputEl.value.length != 1) {
1589 tui.log_msg('@ entered non-single-char, therefore aborted');
1590 tui.switch_mode('admin');
1592 tui.tile_control_char = tui.inputEl.value[0];
1593 tui.switch_mode('control_pw_pw');
1595 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1596 if (tui.inputEl.value.length != 1) {
1597 tui.log_msg('@ entered non-single-char, therefore aborted');
1598 tui.switch_mode('admin');
1600 tui.tile_control_char = tui.inputEl.value[0];
1601 tui.switch_mode('control_tile_draw');
1603 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1604 if (tui.inputEl.value.length != 1) {
1605 tui.log_msg('@ entered non-single-char, therefore aborted');
1607 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1608 tui.log_msg('@ sent new protection character for thing');
1610 tui.switch_mode('admin');
1611 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1612 let tokens = parser.tokenize(tui.inputEl.value);
1613 if (tokens.length > 0 && tokens[0].length > 0) {
1614 if (tui.inputEl.value[0][0] == '/') {
1615 if (tokens[0].slice(1) == 'nick') {
1616 if (tokens.length > 1) {
1617 server.send(['NICK', tokens[1]]);
1619 tui.log_msg('? need new name');
1622 tui.log_msg('? unknown command');
1625 server.send(['ALL', tui.inputEl.value]);
1627 } else if (tui.inputEl.valuelength > 0) {
1628 server.send(['ALL', tui.inputEl.value]);
1630 tui.inputEl.value = "";
1631 } else if (tui.mode.name == 'play') {
1632 if (tui.mode.mode_switch_on_key(event)) {
1634 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1635 server.send(["TASK:INTOXICATE"]);
1636 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1637 server.send(["TASK:DOOR"]);
1638 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1639 server.send(["TASK:WEAR"]);
1640 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1641 server.send(["TASK:SPIN"]);
1642 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1643 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1644 } else if (event.key === tui.keys.teleport) {
1647 } else if (tui.mode.name == 'study') {
1648 if (tui.mode.mode_switch_on_key(event)) {
1650 } else if (event.key in tui.movement_keys) {
1651 explorer.move(tui.movement_keys[event.key]);
1652 } else if (event.key == tui.keys.toggle_map_mode) {
1653 tui.toggle_map_mode();
1655 } else if (tui.mode.name == 'control_tile_draw') {
1656 if (tui.mode.mode_switch_on_key(event)) {
1658 } else if (event.key in tui.movement_keys) {
1659 explorer.move(tui.movement_keys[event.key]);
1660 } else if (event.key === tui.keys.toggle_tile_draw) {
1661 tui.toggle_tile_draw();
1663 } else if (tui.mode.name == 'admin') {
1664 if (tui.mode.mode_switch_on_key(event)) {
1666 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1667 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1669 } else if (tui.mode.name == 'edit') {
1670 if (tui.mode.mode_switch_on_key(event)) {
1672 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1673 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1674 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1675 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1676 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1677 server.send(["TASK:INSTALL", tui.password]);
1678 } else if (event.key == tui.keys.toggle_map_mode) {
1679 tui.toggle_map_mode();
1685 rows_selector.addEventListener('input', function() {
1686 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1689 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1690 terminal.initialize();
1693 cols_selector.addEventListener('input', function() {
1694 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1697 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1698 terminal.initialize();
1699 tui.window_width = terminal.cols / 2,
1702 for (let key_selector of key_selectors) {
1703 key_selector.addEventListener('input', function() {
1704 window.localStorage.setItem(key_selector.id, key_selector.value);
1708 window.setInterval(function() {
1709 if (server.websocket.readyState == 1) {
1710 server.send(['PING']);
1711 } else if (server.websocket.readyState != 0) {
1712 server.reconnect_to(server.url);
1713 tui.log_msg('@ attempting reconnect …')
1716 window.setInterval(function() {
1717 if (document.activeElement.tagName.toLowerCase() != 'input') {
1718 tui.inputEl.focus();
1721 document.getElementById("terminal").onclick = function() {
1722 tui.inputEl.focus();
1724 document.getElementById("help").onclick = function() {
1725 tui.show_help = true;
1728 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1729 const mode = switchEl.id.slice("switch_to_".length);
1730 switchEl.onclick = function() {
1731 tui.switch_mode(mode);
1735 document.getElementById("toggle_tile_draw").onclick = function() {
1736 tui.toggle_tile_draw();
1738 document.getElementById("toggle_map_mode").onclick = function() {
1739 tui.toggle_map_mode();
1742 document.getElementById("flatten").onclick = function() {
1743 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1745 document.getElementById("door").onclick = function() {
1746 server.send(['TASK:DOOR']);
1748 document.getElementById("consume").onclick = function() {
1749 server.send(['TASK:INTOXICATE']);
1751 document.getElementById("install").onclick = function() {
1752 server.send(['TASK:INSTALL']);
1754 document.getElementById("wear").onclick = function() {
1755 server.send(['TASK:WEAR']);
1757 document.getElementById("spin").onclick = function() {
1758 server.send(['TASK:SPIN']);
1760 document.getElementById("teleport").onclick = function() {
1763 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1764 if (move_button.id.startsWith('key_')) { // not a move button
1767 let direction = move_button.id.split('_')[2].toUpperCase();
1769 move_button.onmousedown = function() {
1770 move_repeat = window.setInterval(function() {
1771 if (tui.mode.available_actions.includes("move")) {
1772 server.send(['TASK:MOVE', direction]);
1773 } else if (tui.mode.available_actions.includes("move_explorer")) {
1774 explorer.move(direction);
1779 move_button.onmouseup = function() {
1780 window.clearInterval(move_repeat);