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 terrain (from play mode): <input id="key_switch_to_edit" type="text" value="m" /><br />
30 enter terrain password (from play mode): <input id="key_switch_to_password" type="text" value="P" /><br />
31 annotate terrain (from study mode): <input id="key_switch_to_annotate" type="text" value="m" /><br />
32 annotate portal (from study mode): <input id="key_switch_to_portal" type="text" value="P" /><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 window.setInterval(function() { server.send(['PING']) }, 30000);
180 tui.log_msg("@ server connected! :)");
181 tui.switch_mode(mode_login);
183 this.websocket.onclose = function(event) {
184 tui.log_msg("@ server disconnected :(");
185 tui.log_msg("@ hint: try the '/reconnect' command");
187 this.websocket.onmessage = this.handle_event;
189 reconnect: function() {
190 this.reconnect_to(this.url);
192 reconnect_to: function(url) {
193 this.websocket.close();
196 send: function(tokens) {
197 this.websocket.send(unparser.untokenize(tokens));
199 handle_event: function(event) {
200 let tokens = parser.tokenize(event.data)[0];
201 if (tokens[0] === 'TURN') {
202 game.turn_complete = false;
205 game.turn = parseInt(tokens[1]);
206 } else if (tokens[0] === 'THING_POS') {
207 game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]);
208 } else if (tokens[0] === 'THING_NAME') {
209 game.get_thing(tokens[1], true).name_ = tokens[2];
210 } else if (tokens[0] === 'TASKS') {
211 game.tasks = tokens[1].split(',')
212 } else if (tokens[0] === 'MAP') {
213 game.map_geometry = tokens[1];
215 game.map_size = parser.parse_yx(tokens[2]);
217 } else if (tokens[0] === 'MAP_CONTROL') {
218 game.map_control = tokens[1]
219 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
220 game.turn_complete = true;
221 explorer.empty_info_db();
222 if (tui.mode == mode_post_login_wait) {
223 tui.switch_mode(mode_play);
224 } else if (tui.mode == mode_study) {
225 explorer.query_info();
227 let t = game.get_thing(game.player_id);
228 if (t.position in game.portals) {
229 tui.teleport_target = game.portals[t.position];
230 tui.switch_mode(mode_teleport);
234 } else if (tokens[0] === 'CHAT') {
235 tui.log_msg('# ' + tokens[1], 1);
236 } else if (tokens[0] === 'PLAYER_ID') {
237 game.player_id = parseInt(tokens[1]);
238 } else if (tokens[0] === 'LOGIN_OK') {
239 this.send(['GET_GAMESTATE']);
240 tui.switch_mode(mode_post_login_wait);
241 } else if (tokens[0] === 'PORTAL') {
242 let position = parser.parse_yx(tokens[1]);
243 game.portals[position] = tokens[2];
244 } else if (tokens[0] === 'ANNOTATION') {
245 let position = parser.parse_yx(tokens[1]);
246 explorer.update_info_db(position, tokens[2]);
247 } else if (tokens[0] === 'UNHANDLED_INPUT') {
248 tui.log_msg('? unknown command');
249 } else if (tokens[0] === 'PLAY_ERROR') {
250 terminal.blink_screen();
251 } else if (tokens[0] === 'ARGUMENT_ERROR') {
252 tui.log_msg('? syntax error: ' + tokens[1]);
253 } else if (tokens[0] === 'GAME_ERROR') {
254 tui.log_msg('? game error: ' + tokens[1]);
255 } else if (tokens[0] === 'PONG') {
258 tui.log_msg('? unhandled input: ' + event.data);
264 quote: function(str) {
266 for (let i = 0; i < str.length; i++) {
268 if (['"', '\\'].includes(c)) {
274 return quoted.join('');
276 to_yx: function(yx_coordinate) {
277 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
279 untokenize: function(tokens) {
280 let quoted_tokens = [];
281 for (let token of tokens) {
282 quoted_tokens.push(this.quote(token));
284 return quoted_tokens.join(" ");
289 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
291 this.has_input_prompt = has_input_prompt;
292 this.shows_info= shows_info;
293 this.is_intro = is_intro;
294 this.help_intro = help_intro;
297 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
298 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
299 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
300 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);
301 let mode_annotate = new Mode('annotate', 'This mode allows you to add/edit a comment on the tile you are currently standing on. Hit Return to leave.', true, true);
302 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
303 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 (unless obscured by this help screen here, which you can disappear with any key).', false, true);
304 let mode_edit = new Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your terrain editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.', false, false);
305 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);
306 let mode_portal = new Mode('portal', 'This mode imprints/edits/removes a teleportation target on the ground you are currently standing on. 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);
307 let mode_password = new Mode('password', 'This mode allows you to change the password that you send to authorize yourself for editing password-protected tiles. Hit return to confirm and leave.', true, false, false);
310 mode: mode_waiting_for_server,
314 window_width: terminal.cols / 2,
321 this.inputEl = document.getElementById("input");
322 this.inputEl.focus();
323 this.recalc_input_lines();
324 this.height_header = this.height_turn_line + this.height_mode_line;
325 this.log_msg("@ waiting for server connection ...");
328 init_keys: function() {
330 for (let key_selector of key_selectors) {
331 this.keys[key_selector.id.slice(4)] = key_selector.value;
333 this.movement_keys = {
334 [this.keys.square_move_up]: 'UP',
335 [this.keys.square_move_left]: 'LEFT',
336 [this.keys.square_move_down]: 'DOWN',
337 [this.keys.square_move_right]: 'RIGHT'
339 if (game.map_geometry == 'Hex') {
340 this.movement_keys = {
341 [this.keys.hex_move_upleft]: 'UPLEFT',
342 [this.keys.hex_move_upright]: 'UPRIGHT',
343 [this.keys.hex_move_right]: 'RIGHT',
344 [this.keys.hex_move_downright]: 'DOWNRIGHT',
345 [this.keys.hex_move_downleft]: 'DOWNLEFT',
346 [this.keys.hex_move_left]: 'LEFT'
350 switch_mode: function(mode, keep_pos=false) {
351 this.show_help = false;
352 this.map_mode = 'terrain';
353 if (mode == mode_study && !keep_pos && game.player_id in game.things) {
354 explorer.position = game.things[game.player_id].position;
358 if (mode == mode_annotate && explorer.position in explorer.info_db) {
359 let info = explorer.info_db[explorer.position];
360 if (info != "(none)") {
361 this.inputEl.value = info;
362 this.recalc_input_lines();
364 } else if (mode == mode_login) {
365 if (this.login_name) {
366 server.send(['LOGIN', this.login_name]);
368 this.log_msg("? need login name");
370 } else if (mode == mode_edit) {
371 this.show_help = true;
372 } else if (mode == mode_portal && explorer.position in game.portals) {
373 let portal = game.portals[explorer.position]
374 this.inputEl.value = portal;
375 this.recalc_input_lines();
376 } else if (mode == mode_password) {
377 this.inputEl.value = this.password;
378 this.recalc_input_lines();
379 } else if (mode == mode_teleport) {
380 tui.log_msg("@ May teleport to: " + tui.teleport_target);
381 tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
385 empty_input: function(str) {
386 this.inputEl.value = "";
387 if (this.mode.has_input_prompt) {
388 this.recalc_input_lines();
390 this.height_input = 0;
393 recalc_input_lines: function() {
394 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
395 this.height_input = this.input_lines.length;
397 msg_into_lines_of_width: function(msg, width) {
400 for (let i = 0, x = 0; i < msg.length; i++, x++) {
401 if (x >= width || msg[i] == "\n") {
406 if (msg[i] != "\n") {
413 log_msg: function(msg) {
415 while (this.log.length > 100) {
420 draw_map: function() {
421 let map_lines_split = [];
423 let map_content = game.map;
424 if (this.map_mode == 'control') {
425 map_content = game.map_control;
427 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
428 if (j == game.map_size[1]) {
429 map_lines_split.push(line);
433 line.push(map_content[i]);
435 map_lines_split.push(line);
436 if (this.map_mode == 'terrain') {
437 for (const thing_id in game.things) {
438 let t = game.things[thing_id];
439 map_lines_split[t.position[0]][t.position[1]] = '@';
442 if (tui.mode.shows_info) {
443 map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
446 if (game.map_geometry == 'Square') {
447 for (let line_split of map_lines_split) {
448 map_lines.push(line_split.join(' '));
450 } else if (game.map_geometry == 'Hex') {
452 for (let line_split of map_lines_split) {
453 map_lines.push(' '.repeat(indent) + line_split.join(' '));
461 let window_center = [terminal.rows / 2, this.window_width / 2];
462 let player = game.things[game.player_id];
463 let center_position = [player.position[0], player.position[1]];
464 if (tui.mode.shows_info) {
465 center_position = [explorer.position[0], explorer.position[1]];
467 center_position[1] = center_position[1] * 2;
468 let offset = [center_position[0] - window_center[0],
469 center_position[1] - window_center[1]]
470 if (game.map_geometry == 'Hex' && offset[0] % 2) {
473 let term_y = Math.max(0, -offset[0]);
474 let term_x = Math.max(0, -offset[1]);
475 let map_y = Math.max(0, offset[0]);
476 let map_x = Math.max(0, offset[1]);
477 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
478 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
479 terminal.write(term_y, term_x, to_draw);
482 draw_mode_line: function() {
483 let help = 'hit [' + this.keys.help + '] for help';
484 if (this.mode.has_input_prompt) {
485 help = 'enter /help for help';
487 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
489 draw_turn_line: function(n) {
490 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
492 draw_history: function() {
493 let log_display_lines = [];
494 for (let line of this.log) {
495 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
497 for (let y = terminal.rows - 1 - this.height_input,
498 i = log_display_lines.length - 1;
499 y >= this.height_header && i >= 0;
501 terminal.write(y, this.window_width, log_display_lines[i]);
504 draw_info: function() {
505 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
506 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
507 terminal.write(y, this.window_width, lines[i]);
510 draw_input: function() {
511 if (this.mode.has_input_prompt) {
512 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
513 terminal.write(y, this.window_width, this.input_lines[i]);
517 draw_help: function() {
518 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
519 let content = this.mode.name + " mode help (hit any key to disappear)\n\n" + this.mode.help_intro + "\n\n";
520 if (this.mode == mode_play) {
521 content += "Available actions:\n";
522 if (game.tasks.includes('MOVE')) {
523 content += "[" + movement_keys_desc + "] – move player\n";
525 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
526 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
528 content += '\nOther modes available from here:\n';
529 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
530 content += '[' + this.keys.switch_to_password + '] – terrain password edit mode\n';
531 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
532 content += '[' + this.keys.switch_to_study + '] – study mode\n';
533 } else if (this.mode == mode_study) {
534 content += "Available actions:\n";
535 content += '[' + movement_keys_desc + '] – move question mark\n';
536 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
537 content += '\nOther modes available from here:';
538 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
539 content += '[' + this.keys.switch_to_play + '] – play mode\n';
540 content += '[' + this.keys.switch_to_portal + '] – portal mode\n';
541 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
542 } else if (this.mode == mode_chat) {
543 content += '/nick NAME – re-name yourself to NAME\n';
544 content += '/msg USER TEXT – send TEXT to USER\n';
545 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
546 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
549 if (!this.mode.has_input_prompt) {
550 start_x = this.window_width
552 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
553 let lines = this.msg_into_lines_of_width(content, this.window_width);
554 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
555 terminal.write(y, start_x, lines[i]);
558 full_refresh: function() {
559 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
560 if (this.mode.is_intro) {
564 if (game.turn_complete) {
566 this.draw_turn_line();
568 this.draw_mode_line();
569 if (this.mode.shows_info) {
576 if (this.show_help) {
588 this.map_control = "";
589 this.map_size = [0,0];
594 get_thing: function(id_, create_if_not_found=false) {
595 if (id_ in game.things) {
596 return game.things[id_];
597 } else if (create_if_not_found) {
598 let t = new Thing([0,0]);
599 game.things[id_] = t;
603 move: function(start_position, direction) {
604 let target = [start_position[0], start_position[1]];
605 if (direction == 'LEFT') {
607 } else if (direction == 'RIGHT') {
609 } else if (game.map_geometry == 'Square') {
610 if (direction == 'UP') {
612 } else if (direction == 'DOWN') {
615 } else if (game.map_geometry == 'Hex') {
616 let start_indented = start_position[0] % 2;
617 if (direction == 'UPLEFT') {
619 if (!start_indented) {
622 } else if (direction == 'UPRIGHT') {
624 if (start_indented) {
627 } else if (direction == 'DOWNLEFT') {
629 if (!start_indented) {
632 } else if (direction == 'DOWNRIGHT') {
634 if (start_indented) {
639 if (target[0] < 0 || target[1] < 0 ||
640 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
650 server.init(websocket_location);
655 move: function(direction) {
656 let target = game.move(this.position, direction);
658 this.position = target
662 terminal.blink_screen();
665 update_info_db: function(yx, str) {
666 this.info_db[yx] = str;
667 if (tui.mode == mode_study) {
671 empty_info_db: function() {
673 if (tui.mode == mode_study) {
677 query_info: function() {
678 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
680 get_info: function() {
682 let position_i = this.position[0] * game.map_size[1] + this.position[1];
683 info += "TERRAIN: " + game.map[position_i] + "\n";
684 for (let t_id in game.things) {
685 let t = game.things[t_id];
686 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
689 info += ": " + t.name_;
694 if (this.position in game.portals) {
695 info += "PORTAL: " + game.portals[this.position] + "\n";
697 if (this.position in this.info_db) {
698 info += "ANNOTATIONS: " + this.info_db[this.position];
704 annotate: function(msg) {
705 if (msg.length == 0) {
706 msg = " "; // triggers annotation deletion
708 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg]);
710 set_portal: function(msg) {
711 if (msg.length == 0) {
712 msg = " "; // triggers portal deletion
714 server.send(["PORTAL", unparser.to_yx(explorer.position), msg]);
718 tui.inputEl.addEventListener('input', (event) => {
719 if (tui.mode.has_input_prompt) {
720 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
721 if (tui.inputEl.value.length > max_length) {
722 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
724 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);
729 } else if (tui.mode == mode_teleport) {
730 if (['Y', 'y'].includes(tui.inputEl.value[0])) {
731 server.reconnect_to(tui.teleport_target);
733 tui.log_msg("@ teleportation aborted");
734 tui.switch_mode(mode_play);
738 tui.inputEl.addEventListener('keydown', (event) => {
739 tui.show_help = false;
740 if (event.key == 'Enter') {
741 event.preventDefault();
743 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
744 tui.show_help = true;
747 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
748 tui.show_help = true;
750 } else if (tui.mode == mode_login && event.key == 'Enter') {
751 tui.login_name = tui.inputEl.value;
752 server.send(['LOGIN', tui.inputEl.value]);
754 } else if (tui.mode == mode_portal && event.key == 'Enter') {
755 explorer.set_portal(tui.inputEl.value);
756 tui.switch_mode(mode_study, true);
757 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
758 explorer.annotate(tui.inputEl.value);
759 tui.switch_mode(mode_study, true);
760 } else if (tui.mode == mode_password && event.key == 'Enter') {
761 if (tui.inputEl.value.length == 0) {
762 tui.inputEl.value = " ";
764 tui.password = tui.inputEl.value
765 tui.switch_mode(mode_play);
766 } else if (tui.mode == mode_teleport && event.key == 'Enter') {
767 if (tui.inputEl.value == 'YES!') {
768 server.reconnect_to(tui.teleport_target);
770 tui.log_msg('@ teleport aborted');
771 tui.switch_mode(mode_play);
773 } else if (tui.mode == mode_chat && event.key == 'Enter') {
774 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
775 if (tokens.length > 0 && tokens[0].length > 0) {
776 if (tui.inputEl.value[0][0] == '/') {
777 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
778 tui.switch_mode(mode_play);
779 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
780 tui.switch_mode(mode_study);
781 } else if (tokens[0].slice(1) == 'nick') {
782 if (tokens.length > 1) {
783 server.send(['NICK', tokens[1]]);
785 tui.log_msg('? need new name');
787 } else if (tokens[0].slice(1) == 'msg') {
788 if (tokens.length > 2) {
789 let msg = tui.inputEl.value.slice(token_starts[2]);
790 server.send(['QUERY', tokens[1], msg]);
792 tui.log_msg('? need message target and message');
794 } else if (tokens[0].slice(1) == 'reconnect') {
795 if (tokens.length > 1) {
796 server.reconnect_to(tokens[1]);
801 tui.log_msg('? unknown command');
804 server.send(['ALL', tui.inputEl.value]);
806 } else if (tui.inputEl.valuelength > 0) {
807 server.send(['ALL', tui.inputEl.value]);
811 } else if (tui.mode == mode_play) {
812 if (event.key === tui.keys.switch_to_chat) {
813 event.preventDefault();
814 tui.switch_mode(mode_chat);
815 } else if (event.key === tui.keys.switch_to_edit
816 && game.tasks.includes('WRITE')) {
817 event.preventDefault();
818 tui.switch_mode(mode_edit);
819 } else if (event.key === tui.keys.switch_to_study) {
820 tui.switch_mode(mode_study);
821 } else if (event.key === tui.keys.switch_to_password) {
822 event.preventDefault();
823 tui.switch_mode(mode_password);
824 } else if (event.key === tui.keys.flatten
825 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
826 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
827 } else if (event.key in tui.movement_keys
828 && game.tasks.includes('MOVE')) {
829 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
831 } else if (tui.mode == mode_study) {
832 if (event.key === tui.keys.switch_to_chat) {
833 event.preventDefault();
834 tui.switch_mode(mode_chat);
835 } else if (event.key == tui.keys.switch_to_play) {
836 tui.switch_mode(mode_play);
837 } else if (event.key === tui.keys.switch_to_portal) {
838 event.preventDefault();
839 tui.switch_mode(mode_portal);
840 } else if (event.key in tui.movement_keys) {
841 explorer.move(tui.movement_keys[event.key]);
842 } else if (event.key == tui.keys.toggle_map_mode) {
843 if (tui.map_mode == 'terrain') {
844 tui.map_mode = 'control';
846 tui.map_mode = 'terrain';
849 } else if (event.key === tui.keys.switch_to_annotate) {
850 event.preventDefault();
851 tui.switch_mode(mode_annotate);
856 rows_selector.addEventListener('input', function() {
857 if (rows_selector.value % 4 != 0) {
860 window.localStorage.setItem(rows_selector.id, rows_selector.value);
861 terminal.initialize();
864 cols_selector.addEventListener('input', function() {
865 if (cols_selector.value % 4 != 0) {
868 window.localStorage.setItem(cols_selector.id, cols_selector.value);
869 terminal.initialize();
870 tui.window_width = terminal.cols / 2,
873 for (let key_selector of key_selectors) {
874 key_selector.addEventListener('input', function() {
875 window.localStorage.setItem(key_selector.id, key_selector.value);
879 window.setInterval(function() {
880 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
881 || document.activeElement.id.startsWith('key_'))) {