7 terminal rows: <input id="n_rows" type="number" step=4 min=8 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=20 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 keys (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
14 move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)<br />
15 move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)<br />
16 move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)<br />
17 move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)<br />
18 move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" /><br />
19 move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" /><br />
20 move right (hex grid): <input id="key_hex_move_right" type="text" value="d" /><br />
21 move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" /><br />
22 move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" /><br />
23 move left (hex grid): <input id="key_hex_move_left" type="text" value="a" /><br />
24 help: <input id="key_help" type="text" value="h" /><br />
25 flatten surroundings: <input id="key_flatten" type="text" value="F" /><br />
26 switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" /><br />
27 switch to play mode: <input id="key_switch_to_play" type="text" value="p" /><br />
28 switch to study mode: <input id="key_switch_to_study" type="text" value="?" /><br />
29 edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" /><br />
30 enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" /><br />
31 annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" /><br />
32 annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" /><br />
33 toggle terrain/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" /><br />
37 let websocket_location = "wss://plomlompom.com/rogue_chat/";
39 let rows_selector = document.getElementById("n_rows");
40 let cols_selector = document.getElementById("n_cols");
41 let key_selectors = document.querySelectorAll('[id^="key_"]');
43 function restore_selector_value(selector) {
44 let stored_selection = window.localStorage.getItem(selector.id);
45 if (stored_selection) {
46 selector.value = stored_selection;
49 restore_selector_value(rows_selector);
50 restore_selector_value(cols_selector);
51 for (let key_selector of key_selectors) {
52 restore_selector_value(key_selector);
58 initialize: function() {
59 this.rows = rows_selector.value;
60 this.cols = cols_selector.value;
61 this.pre_el = document.getElementById("terminal");
62 this.pre_el.style.color = this.foreground;
63 this.pre_el.style.backgroundColor = this.background;
66 for (let y = 0, x = 0; y <= this.rows; x++) {
70 this.content.push(line);
79 blink_screen: function() {
80 this.pre_el.style.color = this.background;
81 this.pre_el.style.backgroundColor = this.foreground;
83 this.pre_el.style.color = this.foreground;
84 this.pre_el.style.backgroundColor = this.background;
89 for (let y = 0; y < this.rows; y++) {
90 let line = this.content[y].join('');
91 pre_string += line + '\n';
93 this.pre_el.textContent = pre_string;
95 write: function(start_y, start_x, msg) {
96 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
97 this.content[start_y][x] = msg[i];
100 drawBox: function(start_y, start_x, height, width) {
101 let end_y = start_y + height;
102 let end_x = start_x + width;
103 for (let y = start_y, x = start_x; y < this.rows; x++) {
111 this.content[y][x] = ' ';
115 terminal.initialize();
118 tokenize: function(str) {
124 for (let i = 0; i < str.length; i++) {
130 } else if (c == '\\') {
132 } else if (c == '"') {
137 } else if (c == '"') {
139 } else if (c === ' ') {
140 if (token.length > 0) {
149 if (token.length > 0) {
152 let token_starts = [];
153 for (let i = 0; i < token_ends.length; i++) {
154 token_starts.push(token_ends[i] - tokens[i].length);
156 return [tokens, token_starts];
158 parse_yx: function(position_string) {
159 let coordinate_strings = position_string.split(',')
160 let position = [0, 0];
161 position[0] = parseInt(coordinate_strings[0].slice(2));
162 position[1] = parseInt(coordinate_strings[1].slice(2));
174 init: function(url) {
176 this.websocket = new WebSocket(this.url);
177 this.websocket.onopen = function(event) {
178 server.connected = true;
179 server.send(['TASKS']);
180 tui.log_msg("@ server connected! :)");
181 tui.switch_mode(mode_login);
183 this.websocket.onclose = function(event) {
184 server.connected = false;
185 tui.switch_mode(mode_waiting_for_server);
186 tui.log_msg("@ server disconnected :(");
188 this.websocket.onmessage = this.handle_event;
190 reconnect_to: function(url) {
191 this.websocket.close();
194 send: function(tokens) {
195 this.websocket.send(unparser.untokenize(tokens));
197 handle_event: function(event) {
198 let tokens = parser.tokenize(event.data)[0];
199 if (tokens[0] === 'TURN') {
200 game.turn_complete = false;
203 game.turn = parseInt(tokens[1]);
204 } else if (tokens[0] === 'THING_POS') {
205 game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]);
206 } else if (tokens[0] === 'THING_NAME') {
207 game.get_thing(tokens[1], true).name_ = tokens[2];
208 } else if (tokens[0] === 'TASKS') {
209 game.tasks = tokens[1].split(',')
210 } else if (tokens[0] === 'MAP') {
211 game.map_geometry = tokens[1];
213 game.map_size = parser.parse_yx(tokens[2]);
215 } else if (tokens[0] === 'MAP_CONTROL') {
216 game.map_control = tokens[1]
217 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
218 game.turn_complete = true;
219 explorer.empty_info_db();
220 if (tui.mode == mode_post_login_wait) {
221 tui.switch_mode(mode_play);
222 } else if (tui.mode == mode_study) {
223 explorer.query_info();
225 let t = game.get_thing(game.player_id);
226 if (t.position in game.portals) {
227 tui.teleport_target = game.portals[t.position];
228 tui.switch_mode(mode_teleport);
232 } else if (tokens[0] === 'CHAT') {
233 tui.log_msg('# ' + tokens[1], 1);
234 } else if (tokens[0] === 'PLAYER_ID') {
235 game.player_id = parseInt(tokens[1]);
236 } else if (tokens[0] === 'LOGIN_OK') {
237 this.send(['GET_GAMESTATE']);
238 tui.switch_mode(mode_post_login_wait);
239 } else if (tokens[0] === 'PORTAL') {
240 let position = parser.parse_yx(tokens[1]);
241 game.portals[position] = tokens[2];
242 } else if (tokens[0] === 'ANNOTATION') {
243 let position = parser.parse_yx(tokens[1]);
244 explorer.update_info_db(position, tokens[2]);
245 } else if (tokens[0] === 'UNHANDLED_INPUT') {
246 tui.log_msg('? unknown command');
247 } else if (tokens[0] === 'PLAY_ERROR') {
248 terminal.blink_screen();
249 } else if (tokens[0] === 'ARGUMENT_ERROR') {
250 tui.log_msg('? syntax error: ' + tokens[1]);
251 } else if (tokens[0] === 'GAME_ERROR') {
252 tui.log_msg('? game error: ' + tokens[1]);
253 } else if (tokens[0] === 'PONG') {
256 tui.log_msg('? unhandled input: ' + event.data);
262 quote: function(str) {
264 for (let i = 0; i < str.length; i++) {
266 if (['"', '\\'].includes(c)) {
272 return quoted.join('');
274 to_yx: function(yx_coordinate) {
275 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
277 untokenize: function(tokens) {
278 let quoted_tokens = [];
279 for (let token of tokens) {
280 quoted_tokens.push(this.quote(token));
282 return quoted_tokens.join(" ");
287 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
289 this.has_input_prompt = has_input_prompt;
290 this.shows_info= shows_info;
291 this.is_intro = is_intro;
292 this.help_intro = help_intro;
295 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
296 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
297 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
298 let mode_chat = new Mode('chat', '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 to all users. Lines that start with a "/" are used for commands like:', true, false);
299 let mode_annotate = new Mode('annotate', 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.', true, true);
300 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
301 let mode_study = new Mode('study', '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.', false, true);
302 let mode_edit = new Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.', false, false);
303 let mode_teleport = new Mode('teleport', 'Follow the instructions to re-connect and log-in to another server, or enter anything else to abort.', true);
304 let mode_portal = new Mode('portal', 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map 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.', true, true);
305 let mode_password = new Mode('password', 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.', true, false, false);
308 mode: mode_waiting_for_server,
312 window_width: terminal.cols / 2,
319 this.inputEl = document.getElementById("input");
320 this.inputEl.focus();
321 this.recalc_input_lines();
322 this.height_header = this.height_turn_line + this.height_mode_line;
323 this.log_msg("@ waiting for server connection ...");
326 init_keys: function() {
328 for (let key_selector of key_selectors) {
329 this.keys[key_selector.id.slice(4)] = key_selector.value;
331 this.movement_keys = {
332 [this.keys.square_move_up]: 'UP',
333 [this.keys.square_move_left]: 'LEFT',
334 [this.keys.square_move_down]: 'DOWN',
335 [this.keys.square_move_right]: 'RIGHT'
337 if (game.map_geometry == 'Hex') {
338 this.movement_keys = {
339 [this.keys.hex_move_upleft]: 'UPLEFT',
340 [this.keys.hex_move_upright]: 'UPRIGHT',
341 [this.keys.hex_move_right]: 'RIGHT',
342 [this.keys.hex_move_downright]: 'DOWNRIGHT',
343 [this.keys.hex_move_downleft]: 'DOWNLEFT',
344 [this.keys.hex_move_left]: 'LEFT'
348 switch_mode: function(mode) {
349 this.show_help = false;
350 this.map_mode = 'terrain';
351 if (mode.shows_info && game.player_id in game.things) {
352 explorer.position = game.things[game.player_id].position;
356 this.restore_input_values();
357 if (mode == mode_login) {
358 if (this.login_name) {
359 server.send(['LOGIN', this.login_name]);
361 this.log_msg("? need login name");
363 } else if (mode == mode_edit) {
364 this.show_help = true;
365 } else if (mode == mode_teleport) {
366 tui.log_msg("@ May teleport to: " + tui.teleport_target);
367 tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
371 restore_input_values: function() {
372 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
373 let info = explorer.info_db[explorer.position];
374 if (info != "(none)") {
375 this.inputEl.value = info;
376 this.recalc_input_lines();
378 } else if (this.mode == mode_portal && explorer.position in game.portals) {
379 let portal = game.portals[explorer.position]
380 this.inputEl.value = portal;
381 this.recalc_input_lines();
382 } else if (this.mode == mode_password) {
383 this.inputEl.value = this.password;
384 this.recalc_input_lines();
387 empty_input: function(str) {
388 this.inputEl.value = "";
389 if (this.mode.has_input_prompt) {
390 this.recalc_input_lines();
392 this.height_input = 0;
395 recalc_input_lines: function() {
396 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
397 this.height_input = this.input_lines.length;
399 msg_into_lines_of_width: function(msg, width) {
402 for (let i = 0, x = 0; i < msg.length; i++, x++) {
403 if (x >= width || msg[i] == "\n") {
408 if (msg[i] != "\n") {
415 log_msg: function(msg) {
417 while (this.log.length > 100) {
422 draw_map: function() {
423 let map_lines_split = [];
425 let map_content = game.map;
426 if (this.map_mode == 'control') {
427 map_content = game.map_control;
429 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
430 if (j == game.map_size[1]) {
431 map_lines_split.push(line);
435 line.push(map_content[i]);
437 map_lines_split.push(line);
438 if (this.map_mode == 'terrain') {
439 for (const thing_id in game.things) {
440 let t = game.things[thing_id];
441 map_lines_split[t.position[0]][t.position[1]] = '@';
444 if (tui.mode.shows_info) {
445 map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
448 if (game.map_geometry == 'Square') {
449 for (let line_split of map_lines_split) {
450 map_lines.push(line_split.join(' '));
452 } else if (game.map_geometry == 'Hex') {
454 for (let line_split of map_lines_split) {
455 map_lines.push(' '.repeat(indent) + line_split.join(' '));
463 let window_center = [terminal.rows / 2, this.window_width / 2];
464 let player = game.things[game.player_id];
465 let center_position = [player.position[0], player.position[1]];
466 if (tui.mode.shows_info) {
467 center_position = [explorer.position[0], explorer.position[1]];
469 center_position[1] = center_position[1] * 2;
470 let offset = [center_position[0] - window_center[0],
471 center_position[1] - window_center[1]]
472 if (game.map_geometry == 'Hex' && offset[0] % 2) {
475 let term_y = Math.max(0, -offset[0]);
476 let term_x = Math.max(0, -offset[1]);
477 let map_y = Math.max(0, offset[0]);
478 let map_x = Math.max(0, offset[1]);
479 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
480 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
481 terminal.write(term_y, term_x, to_draw);
484 draw_mode_line: function() {
485 let help = 'hit [' + this.keys.help + '] for help';
486 if (this.mode.has_input_prompt) {
487 help = 'enter /help for help';
489 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
491 draw_turn_line: function(n) {
492 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
494 draw_history: function() {
495 let log_display_lines = [];
496 for (let line of this.log) {
497 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
499 for (let y = terminal.rows - 1 - this.height_input,
500 i = log_display_lines.length - 1;
501 y >= this.height_header && i >= 0;
503 terminal.write(y, this.window_width, log_display_lines[i]);
506 draw_info: function() {
507 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
508 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
509 terminal.write(y, this.window_width, lines[i]);
512 draw_input: function() {
513 if (this.mode.has_input_prompt) {
514 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
515 terminal.write(y, this.window_width, this.input_lines[i]);
519 draw_help: function() {
520 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
521 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
522 if (this.mode == mode_play) {
523 content += "Available actions:\n";
524 if (game.tasks.includes('MOVE')) {
525 content += "[" + movement_keys_desc + "] – move player\n";
527 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
528 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
530 content += '\nOther modes available from here:\n';
531 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
532 content += '[' + this.keys.switch_to_study + '] – study mode\n';
533 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
534 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
535 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
536 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
537 } else if (this.mode == mode_study) {
538 content += "Available actions:\n";
539 content += '[' + movement_keys_desc + '] – move question mark\n';
540 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
541 content += '\nOther modes available from here:\n';
542 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
543 content += '[' + this.keys.switch_to_play + '] – play mode\n';
544 } else if (this.mode == mode_chat) {
545 content += '/nick NAME – re-name yourself to NAME\n';
546 content += '/msg USER TEXT – send TEXT to USER\n';
547 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
548 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
551 if (!this.mode.has_input_prompt) {
552 start_x = this.window_width
554 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
555 let lines = this.msg_into_lines_of_width(content, this.window_width);
556 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
557 terminal.write(y, start_x, lines[i]);
560 full_refresh: function() {
561 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
562 if (this.mode.is_intro) {
566 if (game.turn_complete) {
568 this.draw_turn_line();
570 this.draw_mode_line();
571 if (this.mode.shows_info) {
578 if (this.show_help) {
590 this.map_control = "";
591 this.map_size = [0,0];
596 get_thing: function(id_, create_if_not_found=false) {
597 if (id_ in game.things) {
598 return game.things[id_];
599 } else if (create_if_not_found) {
600 let t = new Thing([0,0]);
601 game.things[id_] = t;
605 move: function(start_position, direction) {
606 let target = [start_position[0], start_position[1]];
607 if (direction == 'LEFT') {
609 } else if (direction == 'RIGHT') {
611 } else if (game.map_geometry == 'Square') {
612 if (direction == 'UP') {
614 } else if (direction == 'DOWN') {
617 } else if (game.map_geometry == 'Hex') {
618 let start_indented = start_position[0] % 2;
619 if (direction == 'UPLEFT') {
621 if (!start_indented) {
624 } else if (direction == 'UPRIGHT') {
626 if (start_indented) {
629 } else if (direction == 'DOWNLEFT') {
631 if (!start_indented) {
634 } else if (direction == 'DOWNRIGHT') {
636 if (start_indented) {
641 if (target[0] < 0 || target[1] < 0 ||
642 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
652 server.init(websocket_location);
657 move: function(direction) {
658 let target = game.move(this.position, direction);
660 this.position = target
663 terminal.blink_screen();
666 update_info_db: function(yx, str) {
667 this.info_db[yx] = str;
668 if (tui.mode == mode_study) {
672 empty_info_db: function() {
674 if (tui.mode == mode_study) {
678 query_info: function() {
679 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
681 get_info: function() {
683 let position_i = this.position[0] * game.map_size[1] + this.position[1];
684 info += "TERRAIN: " + game.map[position_i] + "\n";
685 for (let t_id in game.things) {
686 let t = game.things[t_id];
687 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
690 info += ": " + t.name_;
695 if (this.position in game.portals) {
696 info += "PORTAL: " + game.portals[this.position] + "\n";
698 if (this.position in this.info_db) {
699 info += "ANNOTATIONS: " + this.info_db[this.position];
705 annotate: function(msg) {
706 if (msg.length == 0) {
707 msg = " "; // triggers annotation deletion
709 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
711 set_portal: function(msg) {
712 if (msg.length == 0) {
713 msg = " "; // triggers portal deletion
715 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
719 tui.inputEl.addEventListener('input', (event) => {
720 if (tui.mode.has_input_prompt) {
721 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
722 if (tui.inputEl.value.length > max_length) {
723 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
725 tui.recalc_input_lines();
726 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
727 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
728 tui.switch_mode(mode_play);
732 tui.inputEl.addEventListener('keydown', (event) => {
733 tui.show_help = false;
734 if (event.key == 'Enter') {
735 event.preventDefault();
737 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
738 tui.show_help = true;
740 tui.restore_input_values();
741 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
742 tui.show_help = true;
743 } else if (tui.mode == mode_login && event.key == 'Enter') {
744 tui.login_name = tui.inputEl.value;
745 server.send(['LOGIN', tui.inputEl.value]);
747 } else if (tui.mode == mode_portal && event.key == 'Enter') {
748 explorer.set_portal(tui.inputEl.value);
749 tui.switch_mode(mode_play);
750 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
751 explorer.annotate(tui.inputEl.value);
752 tui.switch_mode(mode_play);
753 } else if (tui.mode == mode_password && event.key == 'Enter') {
754 if (tui.inputEl.value.length == 0) {
755 tui.inputEl.value = " ";
757 tui.password = tui.inputEl.value
758 tui.switch_mode(mode_play);
759 } else if (tui.mode == mode_teleport && event.key == 'Enter') {
760 if (tui.inputEl.value == 'YES!') {
761 server.reconnect_to(tui.teleport_target);
763 tui.log_msg('@ teleport aborted');
764 tui.switch_mode(mode_play);
766 } else if (tui.mode == mode_chat && event.key == 'Enter') {
767 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
768 if (tokens.length > 0 && tokens[0].length > 0) {
769 if (tui.inputEl.value[0][0] == '/') {
770 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
771 tui.switch_mode(mode_play);
772 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
773 tui.switch_mode(mode_study);
774 } else if (tokens[0].slice(1) == 'nick') {
775 if (tokens.length > 1) {
776 server.send(['NICK', tokens[1]]);
778 tui.log_msg('? need new name');
780 } else if (tokens[0].slice(1) == 'msg') {
781 if (tokens.length > 2) {
782 let msg = tui.inputEl.value.slice(token_starts[2]);
783 server.send(['QUERY', tokens[1], msg]);
785 tui.log_msg('? need message target and message');
788 tui.log_msg('? unknown command');
791 server.send(['ALL', tui.inputEl.value]);
793 } else if (tui.inputEl.valuelength > 0) {
794 server.send(['ALL', tui.inputEl.value]);
797 } else if (tui.mode == mode_play) {
798 if (event.key === tui.keys.switch_to_chat) {
799 event.preventDefault();
800 tui.switch_mode(mode_chat);
801 } else if (event.key === tui.keys.switch_to_edit
802 && game.tasks.includes('WRITE')) {
803 event.preventDefault();
804 tui.switch_mode(mode_edit);
805 } else if (event.key === tui.keys.switch_to_study) {
806 tui.switch_mode(mode_study);
807 } else if (event.key === tui.keys.switch_to_password) {
808 event.preventDefault();
809 tui.switch_mode(mode_password);
810 } else if (event.key === tui.keys.flatten
811 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
812 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
813 } else if (event.key in tui.movement_keys
814 && game.tasks.includes('MOVE')) {
815 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
816 } else if (event.key === tui.keys.switch_to_portal) {
817 event.preventDefault();
818 tui.switch_mode(mode_portal);
819 } else if (event.key === tui.keys.switch_to_annotate) {
820 event.preventDefault();
821 tui.switch_mode(mode_annotate);
823 } else if (tui.mode == mode_study) {
824 if (event.key === tui.keys.switch_to_chat) {
825 event.preventDefault();
826 tui.switch_mode(mode_chat);
827 } else if (event.key == tui.keys.switch_to_play) {
828 tui.switch_mode(mode_play);
829 } else if (event.key in tui.movement_keys) {
830 explorer.move(tui.movement_keys[event.key]);
831 } else if (event.key == tui.keys.toggle_map_mode) {
832 if (tui.map_mode == 'terrain') {
833 tui.map_mode = 'control';
835 tui.map_mode = 'terrain';
842 rows_selector.addEventListener('input', function() {
843 if (rows_selector.value % 4 != 0) {
846 window.localStorage.setItem(rows_selector.id, rows_selector.value);
847 terminal.initialize();
850 cols_selector.addEventListener('input', function() {
851 if (cols_selector.value % 4 != 0) {
854 window.localStorage.setItem(cols_selector.id, cols_selector.value);
855 terminal.initialize();
856 tui.window_width = terminal.cols / 2,
859 for (let key_selector of key_selectors) {
860 key_selector.addEventListener('input', function() {
861 window.localStorage.setItem(key_selector.id, key_selector.value);
865 window.setInterval(function() {
866 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
867 || document.activeElement.id.startsWith('key_'))) {
871 window.setInterval(function() {
872 if (server.connected) {
873 server.send(['PING']);
875 server.reconnect_to(server.url);
876 tui.log_msg('@ attempting reconnect …')