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;
532 game.fov = game.fov_new;
534 game.map_control = game.map_control_new;
535 explorer.annotations = explorer.annotations_new;
536 explorer.info_cached = false;
537 game.things = game.things_new;
538 game.player = game.things[game.player_id];
539 game.turn_complete = true;
540 if (tui.mode.name == 'post_login_wait') {
541 tui.switch_mode('play');
545 } else if (tokens[0] === 'CHAT') {
546 tui.log_msg('# ' + tokens[1], 1);
547 } else if (tokens[0] === 'CHATFACE') {
548 tui.draw_face = tokens[1];
550 } else if (tokens[0] === 'REPLY') {
551 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
552 } else if (tokens[0] === 'PLAYER_ID') {
553 game.player_id = parseInt(tokens[1]);
554 } else if (tokens[0] === 'LOGIN_OK') {
555 this.send(['GET_GAMESTATE']);
556 tui.switch_mode('post_login_wait');
557 } else if (tokens[0] === 'DEFAULT_COLORS') {
558 terminal.set_default_colors();
559 } else if (tokens[0] === 'RANDOM_COLORS') {
560 terminal.set_random_colors();
561 } else if (tokens[0] === 'ADMIN_OK') {
563 tui.log_msg('@ you now have admin rights');
564 tui.switch_mode('admin');
565 } else if (tokens[0] === 'PORTAL') {
566 let position = parser.parse_yx(tokens[1]);
567 game.portals_new[position] = tokens[2];
568 } else if (tokens[0] === 'ANNOTATION') {
569 let position = parser.parse_yx(tokens[1]);
570 explorer.annotations_new[position] = tokens[2];
571 } else if (tokens[0] === 'UNHANDLED_INPUT') {
572 tui.log_msg('? unknown command');
573 } else if (tokens[0] === 'PLAY_ERROR') {
574 tui.log_msg('? ' + tokens[1]);
575 terminal.blink_screen();
576 } else if (tokens[0] === 'ARGUMENT_ERROR') {
577 tui.log_msg('? syntax error: ' + tokens[1]);
578 } else if (tokens[0] === 'GAME_ERROR') {
579 tui.log_msg('? game error: ' + tokens[1]);
580 } else if (tokens[0] === 'PONG') {
583 tui.log_msg('? unhandled input: ' + event.data);
589 quote: function(str) {
591 for (let i = 0; i < str.length; i++) {
593 if (['"', '\\'].includes(c)) {
599 return quoted.join('');
601 to_yx: function(yx_coordinate) {
602 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
604 untokenize: function(tokens) {
605 let quoted_tokens = [];
606 for (let token of tokens) {
607 quoted_tokens.push(this.quote(token));
609 return quoted_tokens.join(" ");
614 constructor(name, has_input_prompt=false, shows_info=false,
615 is_intro=false, is_single_char_entry=false) {
617 this.short_desc = mode_helps[name].short;
618 this.available_modes = [];
619 this.available_actions = [];
620 this.has_input_prompt = has_input_prompt;
621 this.shows_info= shows_info;
622 this.is_intro = is_intro;
623 this.help_intro = mode_helps[name].long;
624 this.intro_msg = mode_helps[name].intro;
625 this.is_single_char_entry = is_single_char_entry;
628 *iter_available_modes() {
629 for (let mode_name of this.available_modes) {
630 let mode = tui['mode_' + mode_name];
634 let key = tui.keys['switch_to_' + mode.name];
638 list_available_modes() {
640 if (this.available_modes.length > 0) {
641 msg += 'Other modes available from here:\n';
642 for (let [mode, key] of this.iter_available_modes()) {
643 msg += '[' + key + '] – ' + mode.short_desc + '\n';
648 mode_switch_on_key(key_event) {
649 for (let [mode, key] of this.iter_available_modes()) {
650 if (key_event.key == key) {
651 event.preventDefault();
652 tui.switch_mode(mode.name);
664 window_width: terminal.cols / 2,
672 mode_waiting_for_server: new Mode('waiting_for_server',
674 mode_login: new Mode('login', true, false, true),
675 mode_post_login_wait: new Mode('post_login_wait'),
676 mode_chat: new Mode('chat', true),
677 mode_annotate: new Mode('annotate', true, true),
678 mode_play: new Mode('play'),
679 mode_study: new Mode('study', false, true),
680 mode_write: new Mode('write', false, false, false, true),
681 mode_edit: new Mode('edit'),
682 mode_control_pw_type: new Mode('control_pw_type', true),
683 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
684 mode_portal: new Mode('portal', true, true),
685 mode_password: new Mode('password', true),
686 mode_name_thing: new Mode('name_thing', true, true),
687 mode_command_thing: new Mode('command_thing', true),
688 mode_take_thing: new Mode('take_thing', true),
689 mode_drop_thing: new Mode('drop_thing', true),
690 mode_enter_face: new Mode('enter_face', true),
691 mode_admin_enter: new Mode('admin_enter', true),
692 mode_admin: new Mode('admin'),
693 mode_control_pw_pw: new Mode('control_pw_pw', true),
694 mode_control_tile_type: new Mode('control_tile_type', true),
695 mode_control_tile_draw: new Mode('control_tile_draw'),
697 'flatten': 'FLATTEN_SURROUNDINGS',
698 'take_thing': 'PICK_UP',
699 'drop_thing': 'DROP',
702 'install': 'INSTALL',
704 'command': 'COMMAND',
705 'consume': 'INTOXICATE',
713 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
714 "command_thing", "take_thing", "drop_thing"]
715 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
717 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
718 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
719 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
720 "control_tile_type", "chat",
721 "study", "play", "edit"]
722 this.mode_admin.available_actions = ["move"];
723 this.mode_control_tile_draw.available_modes = ["admin_enter"]
724 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
725 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
726 "password", "chat", "study", "play",
727 "admin_enter", "enter_face"]
728 this.mode_edit.available_actions = ["move", "flatten", "install",
730 this.inputEl = document.getElementById("input");
731 this.inputEl.focus();
732 this.switch_mode('waiting_for_server');
733 this.recalc_input_lines();
734 this.height_header = this.height_turn_line + this.height_mode_line;
737 init_keys: function() {
738 document.getElementById("move_table").hidden = true;
740 for (let key_selector of key_selectors) {
741 this.keys[key_selector.id.slice(4)] = key_selector.value;
743 this.movement_keys = {};
744 let geometry_prefix = 'undefinedMapGeometry_';
745 if (game.map_geometry) {
746 geometry_prefix = game.map_geometry.toLowerCase() + '_';
748 for (const key_name of Object.keys(key_descriptions)) {
749 if (key_name.startsWith(geometry_prefix)) {
750 let direction = key_name.split('_')[2].toUpperCase();
751 let key = this.keys[key_name];
752 this.movement_keys[key] = direction;
755 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
756 if (move_button.id.startsWith('key_')) {
759 move_button.hidden = true;
761 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
762 document.getElementById("move_table").hidden = false;
763 move_button.hidden = false;
765 for (let el of document.getElementsByTagName("button")) {
766 let action_desc = key_descriptions[el.id];
767 let action_key = '[' + this.keys[el.id] + ']';
768 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
771 task_action_on: function(action) {
772 return game.tasks.includes(this.action_tasks[action]);
774 switch_mode: function(mode_name) {
776 function fail(msg, return_mode) {
777 tui.log_msg('? ' + msg);
778 terminal.blink_screen();
779 tui.switch_mode(return_mode);
782 if (this.mode && this.mode.name == 'control_tile_draw') {
783 tui.log_msg('@ finished tile protection drawing.')
785 this.draw_face = false;
786 this.tile_draw = false;
787 if (mode_name == 'command_thing' && (!game.player.carrying
788 || !game.player.carrying.commandable)) {
789 return fail('not carrying anything commandable', 'play');
791 if (mode_name == 'take_thing' && game.player.carrying) {
792 return fail('already carrying something', 'play');
794 if (mode_name == 'drop_thing' && !game.player.carrying) {
795 return fail('not carrying anything droppable', 'play');
797 if (mode_name == 'admin_enter' && this.is_admin) {
799 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
801 for (let t_id in game.things) {
802 if (t_id == game.player_id) {
805 let t = game.things[t_id];
806 if (game.player.position[0] == t.position[0]
807 && game.player.position[1] == t.position[1]) {
813 return fail('not standing over thing', 'fail');
815 this.selected_thing_id = thing_id;
818 this.mode = this['mode_' + mode_name];
819 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
820 this.map_mode = 'protections';
821 } else if (this.mode.name != "edit") {
822 this.map_mode = 'terrain + things';
824 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
825 this.inputEl.focus();
827 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
828 explorer.position = game.player.position;
830 this.inputEl.value = "";
831 this.restore_input_values();
832 for (let el of document.getElementsByTagName("button")) {
835 document.getElementById("help").disabled = false;
836 for (const action of this.mode.available_actions) {
837 if (["move", "move_explorer"].includes(action)) {
838 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
839 move_key.disabled = false;
841 } else if (Object.keys(this.action_tasks).includes(action)) {
842 if (this.task_action_on(action)) {
843 document.getElementById(action).disabled = false;
846 document.getElementById(action).disabled = false;
849 for (const mode_name of this.mode.available_modes) {
850 document.getElementById('switch_to_' + mode_name).disabled = false;
852 if (this.mode.intro_msg.length > 0) {
853 this.log_msg(this.mode.intro_msg);
855 if (this.mode.name == 'login') {
856 if (this.login_name) {
857 server.send(['LOGIN', this.login_name]);
859 this.log_msg("? need login name");
861 } else if (this.mode.is_single_char_entry) {
862 this.show_help = true;
863 } else if (this.mode.name == 'take_thing') {
864 this.log_msg("Portable things in reach for pick-up:");
865 const y = game.player.position[0]
866 const x = game.player.position[1]
867 let select_range = [y.toString() + ':' + x.toString(),
868 (y + 0).toString() + ':' + (x - 1).toString(),
869 (y + 0).toString() + ':' + (x + 1).toString(),
870 (y - 1).toString() + ':' + (x).toString(),
871 (y + 1).toString() + ':' + (x).toString()];
872 if (game.map_geometry == 'Hex') {
874 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
875 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
877 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
878 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
881 this.selectables = [];
882 for (const t_id in game.things) {
883 const t = game.things[t_id];
884 if (select_range.includes(t.position[0].toString()
885 + ':' + t.position[1].toString())
887 this.selectables.push(t_id);
890 if (this.selectables.length == 0) {
891 this.log_msg('none');
892 terminal.blink_screen();
893 this.switch_mode('play');
896 for (let [i, t_id] of this.selectables.entries()) {
897 const t = game.things[t_id];
898 this.log_msg(i + ': ' + explorer.get_thing_info(t));
901 } else if (this.mode.name == 'drop_thing') {
902 this.log_msg('Direction to drop thing to:');
903 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
904 for (let [i, direction] of this.selectables.entries()) {
905 this.log_msg(i + ': ' + direction);
907 } else if (this.mode.name == 'command_thing') {
908 server.send(['TASK:COMMAND', 'HELP']);
909 } else if (this.mode.name == 'control_pw_pw') {
910 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
911 } else if (this.mode.name == 'control_tile_draw') {
912 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 + '].')
916 offset_links: function(offset, links) {
917 for (let y in links) {
918 let real_y = offset[0] + parseInt(y);
919 if (!this.links[real_y]) {
920 this.links[real_y] = [];
922 for (let link of links[y]) {
923 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
924 this.links[real_y].push(offset_link);
928 restore_input_values: function() {
929 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
930 let info = explorer.annotations[explorer.position];
931 if (info != "(none)") {
932 this.inputEl.value = info;
934 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
935 let portal = game.portals[explorer.position]
936 this.inputEl.value = portal;
937 } else if (this.mode.name == 'password') {
938 this.inputEl.value = this.password;
939 } else if (this.mode.name == 'name_thing') {
940 let t = game.get_thing(this.selected_thing_id);
942 this.inputEl.value = t.name_;
944 } else if (this.mode.name == 'admin_thing_protect') {
945 let t = game.get_thing(this.selected_thing_id);
946 if (t && t.protection) {
947 this.inputEl.value = t.protection;
951 recalc_input_lines: function() {
952 if (this.mode.has_input_prompt) {
954 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
956 this.input_lines = [];
958 this.height_input = this.input_lines.length;
960 msg_into_lines_of_width: function(msg, width) {
961 function push_inner_link(y, end_x) {
962 if (!inner_links[y]) {
965 inner_links[y].push([url_start_x, end_x, url]);
967 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
970 for (const match of matches) {
971 const url = match[0];
972 const url_start = match.index;
973 const url_end = match.index + match[0].length;
974 link_data[url_start] = url;
975 url_ends.push(url_end);
979 let inner_links = {};
983 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
984 if (x >= width || msg[i] == "\n") {
986 push_inner_link(y, chunk.length);
988 if (url_ends[0] == i) {
996 if (msg[i] == "\n") {
1001 if (msg[i] != "\n") {
1004 if (i in link_data) {
1008 } else if (url_ends[0] == i) {
1010 push_inner_link(y, x);
1016 push_inner_link(lines.length - 1, chunk.length);
1018 return [lines, inner_links];
1020 log_msg: function(msg) {
1022 while (this.log.length > 100) {
1025 this.full_refresh();
1027 pick_selectable: function(task_name) {
1028 const i = parseInt(this.inputEl.value);
1029 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1030 tui.log_msg('? invalid index, aborted');
1032 server.send(['TASK:' + task_name, tui.selectables[i]]);
1034 this.inputEl.value = "";
1035 this.switch_mode('play');
1037 draw_map: function() {
1038 if (!game.turn_complete && this.map_lines.length == 0) {
1041 if (game.turn_complete) {
1042 let map_lines_split = [];
1044 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1045 if (j == game.map_size[1]) {
1046 map_lines_split.push(line);
1050 if (this.map_mode == 'protections') {
1051 line.push(game.map_control[i] + ' ');
1053 line.push(game.map[i] + ' ');
1056 map_lines_split.push(line);
1057 if (this.map_mode == 'terrain + annotations') {
1058 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1059 const yx = coordinate.split(',')
1060 map_lines_split[yx[0]][yx[1]] = 'A ';
1062 } else if (this.map_mode == 'terrain + things') {
1063 for (const p in game.portals) {
1064 let coordinate = p.split(',')
1065 let original = map_lines_split[coordinate[0]][coordinate[1]];
1066 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1068 let used_positions = [];
1069 function draw_thing(t, used_positions) {
1070 let symbol = game.thing_types[t.type_];
1071 let meta_char = ' ';
1073 meta_char = t.thing_char;
1075 if (used_positions.includes(t.position.toString())) {
1081 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1082 used_positions.push(t.position.toString());
1084 for (const thing_id in game.things) {
1085 let t = game.things[thing_id];
1086 if (t.type_ != 'Player') {
1087 draw_thing(t, used_positions);
1090 for (const thing_id in game.things) {
1091 let t = game.things[thing_id];
1092 if (t.type_ == 'Player') {
1093 draw_thing(t, used_positions);
1097 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1098 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1099 } else if (tui.map_mode != 'terrain + things') {
1100 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1103 if (game.map_geometry == 'Square') {
1104 for (let line_split of map_lines_split) {
1105 this.map_lines.push(line_split.join(''));
1107 } else if (game.map_geometry == 'Hex') {
1109 for (let line_split of map_lines_split) {
1110 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1118 let window_center = [terminal.rows / 2, this.window_width / 2];
1119 let center_position = [game.player.position[0], game.player.position[1]];
1120 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1121 center_position = [explorer.position[0], explorer.position[1]];
1123 center_position[1] = center_position[1] * 2;
1124 this.offset = [center_position[0] - window_center[0],
1125 center_position[1] - window_center[1]]
1126 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1127 this.offset[1] += 1;
1130 let term_y = Math.max(0, -this.offset[0]);
1131 let term_x = Math.max(0, -this.offset[1]);
1132 let map_y = Math.max(0, this.offset[0]);
1133 let map_x = Math.max(0, this.offset[1]);
1134 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1135 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1136 terminal.write(term_y, term_x, to_draw);
1139 draw_face_popup: function() {
1140 const t = game.things[this.draw_face];
1141 if (!t || !t.face) {
1142 this.draw_face = false;
1145 const start_x = tui.window_width - 10;
1148 t_char = t.thing_char;
1150 function draw_body_part(body_part, end_y) {
1151 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1152 terminal.write(end_y - 3, start_x, '| |');
1153 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1154 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1155 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1158 draw_body_part(t.face, terminal.rows - 2);
1161 draw_body_part(t.hat, terminal.rows - 5);
1163 terminal.write(terminal.rows - 1, start_x, '| |');
1165 draw_mode_line: function() {
1166 let help = 'hit [' + this.keys.help + '] for help';
1167 if (this.mode.has_input_prompt) {
1168 help = 'enter /help for help';
1170 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1172 draw_turn_line: function(n) {
1173 if (game.turn_complete) {
1174 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1177 draw_history: function() {
1178 let log_display_lines = [];
1180 let y_offset_in_log = 0;
1181 for (let line of this.log) {
1182 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1184 log_display_lines = log_display_lines.concat(new_lines);
1185 for (const y in link_data) {
1186 const rel_y = y_offset_in_log + parseInt(y);
1187 log_links[rel_y] = [];
1188 for (let link of link_data[y]) {
1189 log_links[rel_y].push(link);
1192 y_offset_in_log += new_lines.length;
1194 let i = log_display_lines.length - 1;
1195 for (let y = terminal.rows - 1 - this.height_input;
1196 y >= this.height_header && i >= 0;
1198 terminal.write(y, this.window_width, log_display_lines[i]);
1200 for (const key of Object.keys(log_links)) {
1201 if (parseInt(key) <= i) {
1202 delete log_links[key];
1205 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1207 this.offset_links(offset, log_links);
1209 draw_info: function() {
1210 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1211 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1212 let offset = [this.height_header, this.window_width];
1213 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1214 terminal.write(y, offset[1], lines[i]);
1216 this.offset_links(offset, link_data);
1218 draw_input: function() {
1219 if (this.mode.has_input_prompt) {
1220 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1221 terminal.write(y, this.window_width, this.input_lines[i]);
1225 draw_help: function() {
1226 let movement_keys_desc = '';
1227 if (!this.mode.is_intro) {
1228 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1230 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1231 if (this.mode.available_actions.length > 0) {
1232 content += "Available actions:\n";
1233 for (let action of this.mode.available_actions) {
1234 if (Object.keys(this.action_tasks).includes(action)) {
1235 if (!this.task_action_on(action)) {
1239 if (action == 'move_explorer') {
1242 if (action == 'move') {
1243 content += "[" + movement_keys_desc + "] – move\n"
1245 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1250 content += this.mode.list_available_modes();
1252 if (!this.mode.has_input_prompt) {
1253 start_x = this.window_width;
1254 this.draw_links = false;
1256 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1257 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1258 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1259 terminal.write(y, start_x, lines[i]);
1262 toggle_tile_draw: function() {
1263 if (tui.tile_draw) {
1264 tui.tile_draw = false;
1266 tui.tile_draw = true;
1269 toggle_map_mode: function() {
1270 if (tui.map_mode == 'terrain only') {
1271 tui.map_mode = 'terrain + annotations';
1272 } else if (tui.map_mode == 'terrain + annotations') {
1273 tui.map_mode = 'terrain + things';
1274 } else if (tui.map_mode == 'terrain + things') {
1275 tui.map_mode = 'protections';
1276 } else if (tui.map_mode == 'protections') {
1277 tui.map_mode = 'terrain only';
1280 full_refresh: function() {
1281 this.draw_links = true;
1283 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1284 this.recalc_input_lines();
1285 if (this.mode.is_intro) {
1286 this.draw_history();
1290 this.draw_turn_line();
1291 this.draw_mode_line();
1292 if (this.mode.shows_info) {
1295 this.draw_history();
1299 if (this.show_help) {
1302 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1303 this.draw_face_popup();
1305 if (!this.draw_links) {
1315 this.player_id = -1;
1318 this.things_new = {};
1323 this.map_control = "";
1324 this.map_control_new = "";
1325 this.map_size = [0,0];
1326 this.map_size_new = [0,0];
1328 this.portals_new = {};
1330 get_thing_temp: function(id_, create_if_not_found=false) {
1331 if (id_ in game.things_new) {
1332 return game.things_new[id_];
1333 } else if (create_if_not_found) {
1334 let t = new Thing([0,0]);
1335 game.things_new[id_] = t;
1339 get_thing: function(id_, create_if_not_found=false) {
1340 if (id_ in game.things) {
1341 return game.things[id_];
1344 move: function(start_position, direction) {
1345 let target = [start_position[0], start_position[1]];
1346 if (direction == 'LEFT') {
1348 } else if (direction == 'RIGHT') {
1350 } else if (game.map_geometry == 'Square') {
1351 if (direction == 'UP') {
1353 } else if (direction == 'DOWN') {
1356 } else if (game.map_geometry == 'Hex') {
1357 let start_indented = start_position[0] % 2;
1358 if (direction == 'UPLEFT') {
1360 if (!start_indented) {
1363 } else if (direction == 'UPRIGHT') {
1365 if (start_indented) {
1368 } else if (direction == 'DOWNLEFT') {
1370 if (!start_indented) {
1373 } else if (direction == 'DOWNRIGHT') {
1375 if (start_indented) {
1380 if (target[0] < 0 || target[1] < 0 ||
1381 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1386 teleport: function() {
1387 if (game.player.position in this.portals) {
1388 server.reconnect_to(this.portals[game.player.position]);
1390 terminal.blink_screen();
1391 tui.log_msg('? not standing on portal')
1399 server.init(websocket_location);
1404 annotations_new: {},
1406 move: function(direction) {
1407 let target = game.move(this.position, direction);
1409 this.position = target
1410 this.info_cached = false;
1411 if (tui.tile_draw) {
1412 this.send_tile_control_command();
1415 terminal.blink_screen();
1418 get_info: function() {
1419 if (this.info_cached) {
1420 return this.info_cached;
1422 let info_to_cache = '';
1423 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1424 if (game.fov[position_i] != '.') {
1425 info_to_cache += 'outside field of view';
1427 for (let t_id in game.things) {
1428 let t = game.things[t_id];
1429 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1430 info_to_cache += "THING: " + this.get_thing_info(t);
1431 let protection = t.protection;
1432 if (protection == '.') {
1433 protection = 'none';
1435 info_to_cache += " / protection: " + protection + "\n";
1437 info_to_cache += t.hat.slice(0, 6) + '\n';
1438 info_to_cache += t.hat.slice(6, 12) + '\n';
1439 info_to_cache += t.hat.slice(12, 18) + '\n';
1442 info_to_cache += t.face.slice(0, 6) + '\n';
1443 info_to_cache += t.face.slice(6, 12) + '\n';
1444 info_to_cache += t.face.slice(12, 18) + '\n';
1448 let terrain_char = game.map[position_i]
1449 let terrain_desc = '?'
1450 if (game.terrains[terrain_char]) {
1451 terrain_desc = game.terrains[terrain_char];
1453 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1454 let protection = game.map_control[position_i];
1455 if (protection == '.') {
1456 protection = 'unprotected';
1458 info_to_cache += 'PROTECTION: ' + protection + '\n';
1459 if (this.position in game.portals) {
1460 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1462 if (this.position in this.annotations) {
1463 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1466 this.info_cached = info_to_cache;
1467 return this.info_cached;
1469 get_thing_info: function(t) {
1470 const symbol = game.thing_types[t.type_];
1471 let info = t.type_ + " / " + symbol;
1473 info += t.thing_char;
1476 info += " (" + t.name_ + ")";
1479 info += " / installed";
1483 annotate: function(msg) {
1484 if (msg.length == 0) {
1485 msg = " "; // triggers annotation deletion
1487 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1489 set_portal: function(msg) {
1490 if (msg.length == 0) {
1491 msg = " "; // triggers portal deletion
1493 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1495 send_tile_control_command: function() {
1496 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1500 tui.inputEl.addEventListener('input', (event) => {
1501 if (tui.mode.has_input_prompt) {
1502 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1503 if (tui.inputEl.value.length > max_length) {
1504 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1506 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1507 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1508 tui.switch_mode('edit');
1512 document.onclick = function() {
1513 if (!tui.mode.is_single_char_entry) {
1514 tui.show_help = false;
1517 tui.inputEl.addEventListener('keydown', (event) => {
1518 tui.show_help = false;
1519 if (event.key == 'Enter') {
1520 event.preventDefault();
1522 if ((!tui.mode.is_intro && event.key == 'Escape')
1523 || (tui.mode.has_input_prompt && event.key == 'Enter'
1524 && tui.inputEl.value.length == 0
1525 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1526 'admin_enter'].includes(tui.mode.name))) {
1527 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1528 tui.log_msg('@ aborted');
1530 tui.switch_mode('play');
1531 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1532 tui.show_help = true;
1533 tui.inputEl.value = "";
1534 tui.restore_input_values();
1535 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1536 && !tui.mode.is_single_char_entry) {
1537 tui.show_help = true;
1538 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1539 tui.login_name = tui.inputEl.value;
1540 server.send(['LOGIN', tui.inputEl.value]);
1541 tui.inputEl.value = "";
1542 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1543 if (tui.inputEl.value.length != 18) {
1544 tui.log_msg('? wrong input length, aborting');
1546 server.send(['PLAYER_FACE', tui.inputEl.value]);
1548 tui.inputEl.value = "";
1549 tui.switch_mode('edit');
1550 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1551 server.send(['TASK:COMMAND', tui.inputEl.value]);
1552 tui.inputEl.value = "";
1553 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1554 tui.pick_selectable('PICK_UP');
1555 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1556 tui.pick_selectable('DROP');
1557 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1558 if (tui.inputEl.value.length == 0) {
1559 tui.log_msg('@ aborted');
1561 server.send(['SET_MAP_CONTROL_PASSWORD',
1562 tui.tile_control_char, tui.inputEl.value]);
1563 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1565 tui.switch_mode('admin');
1566 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1567 explorer.set_portal(tui.inputEl.value);
1568 tui.switch_mode('edit');
1569 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1570 if (tui.inputEl.value.length == 0) {
1571 tui.inputEl.value = " ";
1573 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1575 tui.switch_mode('edit');
1576 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1577 explorer.annotate(tui.inputEl.value);
1578 tui.switch_mode('edit');
1579 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1580 if (tui.inputEl.value.length == 0) {
1581 tui.inputEl.value = " ";
1583 tui.password = tui.inputEl.value
1584 tui.switch_mode('edit');
1585 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1586 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1587 tui.switch_mode('play');
1588 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1589 if (tui.inputEl.value.length != 1) {
1590 tui.log_msg('@ entered non-single-char, therefore aborted');
1591 tui.switch_mode('admin');
1593 tui.tile_control_char = tui.inputEl.value[0];
1594 tui.switch_mode('control_pw_pw');
1596 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1597 if (tui.inputEl.value.length != 1) {
1598 tui.log_msg('@ entered non-single-char, therefore aborted');
1599 tui.switch_mode('admin');
1601 tui.tile_control_char = tui.inputEl.value[0];
1602 tui.switch_mode('control_tile_draw');
1604 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1605 if (tui.inputEl.value.length != 1) {
1606 tui.log_msg('@ entered non-single-char, therefore aborted');
1608 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1609 tui.log_msg('@ sent new protection character for thing');
1611 tui.switch_mode('admin');
1612 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1613 let tokens = parser.tokenize(tui.inputEl.value);
1614 if (tokens.length > 0 && tokens[0].length > 0) {
1615 if (tui.inputEl.value[0][0] == '/') {
1616 if (tokens[0].slice(1) == 'nick') {
1617 if (tokens.length > 1) {
1618 server.send(['NICK', tokens[1]]);
1620 tui.log_msg('? need new name');
1623 tui.log_msg('? unknown command');
1626 server.send(['ALL', tui.inputEl.value]);
1628 } else if (tui.inputEl.valuelength > 0) {
1629 server.send(['ALL', tui.inputEl.value]);
1631 tui.inputEl.value = "";
1632 } else if (tui.mode.name == 'play') {
1633 if (tui.mode.mode_switch_on_key(event)) {
1635 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1636 server.send(["TASK:INTOXICATE"]);
1637 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1638 server.send(["TASK:DOOR"]);
1639 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1640 server.send(["TASK:WEAR"]);
1641 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1642 server.send(["TASK:SPIN"]);
1643 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1644 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1645 } else if (event.key === tui.keys.teleport) {
1648 } else if (tui.mode.name == 'study') {
1649 if (tui.mode.mode_switch_on_key(event)) {
1651 } else if (event.key in tui.movement_keys) {
1652 explorer.move(tui.movement_keys[event.key]);
1653 } else if (event.key == tui.keys.toggle_map_mode) {
1654 tui.toggle_map_mode();
1656 } else if (tui.mode.name == 'control_tile_draw') {
1657 if (tui.mode.mode_switch_on_key(event)) {
1659 } else if (event.key in tui.movement_keys) {
1660 explorer.move(tui.movement_keys[event.key]);
1661 } else if (event.key === tui.keys.toggle_tile_draw) {
1662 tui.toggle_tile_draw();
1664 } else if (tui.mode.name == 'admin') {
1665 if (tui.mode.mode_switch_on_key(event)) {
1667 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1668 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1670 } else if (tui.mode.name == 'edit') {
1671 if (tui.mode.mode_switch_on_key(event)) {
1673 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1674 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1675 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1676 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1677 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1678 server.send(["TASK:INSTALL", tui.password]);
1679 } else if (event.key == tui.keys.toggle_map_mode) {
1680 tui.toggle_map_mode();
1686 rows_selector.addEventListener('input', function() {
1687 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1690 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1691 terminal.initialize();
1694 cols_selector.addEventListener('input', function() {
1695 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1698 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1699 terminal.initialize();
1700 tui.window_width = terminal.cols / 2,
1703 for (let key_selector of key_selectors) {
1704 key_selector.addEventListener('input', function() {
1705 window.localStorage.setItem(key_selector.id, key_selector.value);
1709 window.setInterval(function() {
1710 if (server.websocket.readyState == 1) {
1711 server.send(['PING']);
1712 } else if (server.websocket.readyState != 0) {
1713 server.reconnect_to(server.url);
1714 tui.log_msg('@ attempting reconnect …')
1717 window.setInterval(function() {
1718 if (document.activeElement.tagName.toLowerCase() != 'input') {
1719 tui.inputEl.focus();
1722 document.getElementById("terminal").onclick = function() {
1723 tui.inputEl.focus();
1725 document.getElementById("help").onclick = function() {
1726 tui.show_help = true;
1729 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1730 const mode = switchEl.id.slice("switch_to_".length);
1731 switchEl.onclick = function() {
1732 tui.switch_mode(mode);
1736 document.getElementById("toggle_tile_draw").onclick = function() {
1737 tui.toggle_tile_draw();
1739 document.getElementById("toggle_map_mode").onclick = function() {
1740 tui.toggle_map_mode();
1743 document.getElementById("flatten").onclick = function() {
1744 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1746 document.getElementById("door").onclick = function() {
1747 server.send(['TASK:DOOR']);
1749 document.getElementById("consume").onclick = function() {
1750 server.send(['TASK:INTOXICATE']);
1752 document.getElementById("install").onclick = function() {
1753 server.send(['TASK:INSTALL']);
1755 document.getElementById("wear").onclick = function() {
1756 server.send(['TASK:WEAR']);
1758 document.getElementById("spin").onclick = function() {
1759 server.send(['TASK:SPIN']);
1761 document.getElementById("teleport").onclick = function() {
1764 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1765 if (move_button.id.startsWith('key_')) { // not a move button
1768 let direction = move_button.id.split('_')[2].toUpperCase();
1770 move_button.onmousedown = function() {
1771 move_repeat = window.setInterval(function() {
1772 if (tui.mode.available_actions.includes("move")) {
1773 server.send(['TASK:MOVE', direction]);
1774 } else if (tui.mode.available_actions.includes("move_explorer")) {
1775 explorer.move(direction);
1780 move_button.onmouseup = function() {
1781 window.clearInterval(move_repeat);