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/";
38 let websocket_location = "ws://localhost:8000/";
40 let rows_selector = document.getElementById("n_rows");
41 let cols_selector = document.getElementById("n_cols");
42 let key_selectors = document.querySelectorAll('[id^="key_"]');
44 function restore_selector_value(selector) {
45 let stored_selection = window.localStorage.getItem(selector.id);
46 if (stored_selection) {
47 selector.value = stored_selection;
50 restore_selector_value(rows_selector);
51 restore_selector_value(cols_selector);
52 for (let key_selector of key_selectors) {
53 restore_selector_value(key_selector);
59 initialize: function() {
60 this.rows = rows_selector.value;
61 this.cols = cols_selector.value;
62 this.pre_el = document.getElementById("terminal");
63 this.pre_el.style.color = this.foreground;
64 this.pre_el.style.backgroundColor = this.background;
67 for (let y = 0, x = 0; y <= this.rows; x++) {
71 this.content.push(line);
80 blink_screen: function() {
81 this.pre_el.style.color = this.background;
82 this.pre_el.style.backgroundColor = this.foreground;
84 this.pre_el.style.color = this.foreground;
85 this.pre_el.style.backgroundColor = this.background;
90 for (let y = 0; y < this.rows; y++) {
91 let line = this.content[y].join('');
92 pre_string += line + '\n';
94 this.pre_el.textContent = pre_string;
96 write: function(start_y, start_x, msg) {
97 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
98 this.content[start_y][x] = msg[i];
101 drawBox: function(start_y, start_x, height, width) {
102 let end_y = start_y + height;
103 let end_x = start_x + width;
104 for (let y = start_y, x = start_x; y < this.rows; x++) {
112 this.content[y][x] = ' ';
116 terminal.initialize();
119 tokenize: function(str) {
125 for (let i = 0; i < str.length; i++) {
131 } else if (c == '\\') {
133 } else if (c == '"') {
138 } else if (c == '"') {
140 } else if (c === ' ') {
141 if (token.length > 0) {
150 if (token.length > 0) {
153 let token_starts = [];
154 for (let i = 0; i < token_ends.length; i++) {
155 token_starts.push(token_ends[i] - tokens[i].length);
157 return [tokens, token_starts];
159 parse_yx: function(position_string) {
160 let coordinate_strings = position_string.split(',')
161 let position = [0, 0];
162 position[0] = parseInt(coordinate_strings[0].slice(2));
163 position[1] = parseInt(coordinate_strings[1].slice(2));
175 init: function(url) {
177 this.websocket = new WebSocket(this.url);
178 this.websocket.onopen = function(event) {
179 server.connected = true;
180 server.send(['TASKS']);
181 tui.log_msg("@ server connected! :)");
182 tui.switch_mode(mode_login);
184 this.websocket.onclose = function(event) {
185 server.connected = false;
186 tui.switch_mode(mode_waiting_for_server);
187 tui.log_msg("@ server disconnected :(");
189 this.websocket.onmessage = this.handle_event;
191 reconnect_to: function(url) {
192 this.websocket.close();
195 send: function(tokens) {
196 this.websocket.send(unparser.untokenize(tokens));
198 handle_event: function(event) {
199 let tokens = parser.tokenize(event.data)[0];
200 if (tokens[0] === 'TURN') {
201 game.turn_complete = false;
204 game.turn = parseInt(tokens[1]);
205 } else if (tokens[0] === 'THING_POS') {
206 game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]);
207 } else if (tokens[0] === 'THING_NAME') {
208 game.get_thing(tokens[1], true).name_ = tokens[2];
209 } else if (tokens[0] === 'TASKS') {
210 game.tasks = tokens[1].split(',')
211 } else if (tokens[0] === 'MAP') {
212 game.map_geometry = tokens[1];
214 game.map_size = parser.parse_yx(tokens[2]);
216 } else if (tokens[0] === 'FOV') {
218 } else if (tokens[0] === 'MAP_CONTROL') {
219 game.map_control = tokens[1]
220 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
221 game.turn_complete = true;
222 explorer.empty_info_db();
223 if (tui.mode == mode_post_login_wait) {
224 tui.switch_mode(mode_play);
225 } else if (tui.mode == mode_study) {
226 explorer.query_info();
228 let t = game.get_thing(game.player_id);
229 if (t.position in game.portals) {
230 tui.teleport_target = game.portals[t.position];
231 tui.switch_mode(mode_teleport);
235 } else if (tokens[0] === 'CHAT') {
236 tui.log_msg('# ' + tokens[1], 1);
237 } else if (tokens[0] === 'PLAYER_ID') {
238 game.player_id = parseInt(tokens[1]);
239 } else if (tokens[0] === 'LOGIN_OK') {
240 this.send(['GET_GAMESTATE']);
241 tui.switch_mode(mode_post_login_wait);
242 } else if (tokens[0] === 'PORTAL') {
243 let position = parser.parse_yx(tokens[1]);
244 game.portals[position] = tokens[2];
245 } else if (tokens[0] === 'ANNOTATION') {
246 let position = parser.parse_yx(tokens[1]);
247 explorer.update_info_db(position, tokens[2]);
248 } else if (tokens[0] === 'UNHANDLED_INPUT') {
249 tui.log_msg('? unknown command');
250 } else if (tokens[0] === 'PLAY_ERROR') {
251 terminal.blink_screen();
252 } else if (tokens[0] === 'ARGUMENT_ERROR') {
253 tui.log_msg('? syntax error: ' + tokens[1]);
254 } else if (tokens[0] === 'GAME_ERROR') {
255 tui.log_msg('? game error: ' + tokens[1]);
256 } else if (tokens[0] === 'PONG') {
259 tui.log_msg('? unhandled input: ' + event.data);
265 quote: function(str) {
267 for (let i = 0; i < str.length; i++) {
269 if (['"', '\\'].includes(c)) {
275 return quoted.join('');
277 to_yx: function(yx_coordinate) {
278 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
280 untokenize: function(tokens) {
281 let quoted_tokens = [];
282 for (let token of tokens) {
283 quoted_tokens.push(this.quote(token));
285 return quoted_tokens.join(" ");
290 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
292 this.has_input_prompt = has_input_prompt;
293 this.shows_info= shows_info;
294 this.is_intro = is_intro;
295 this.help_intro = help_intro;
298 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
299 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
300 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
301 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 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:', true, false);
302 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);
303 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
304 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);
305 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);
306 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);
307 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);
308 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);
311 mode: mode_waiting_for_server,
315 window_width: terminal.cols / 2,
322 this.inputEl = document.getElementById("input");
323 this.inputEl.focus();
324 this.recalc_input_lines();
325 this.height_header = this.height_turn_line + this.height_mode_line;
326 this.log_msg("@ waiting for server connection ...");
329 init_keys: function() {
331 for (let key_selector of key_selectors) {
332 this.keys[key_selector.id.slice(4)] = key_selector.value;
334 this.movement_keys = {
335 [this.keys.square_move_up]: 'UP',
336 [this.keys.square_move_left]: 'LEFT',
337 [this.keys.square_move_down]: 'DOWN',
338 [this.keys.square_move_right]: 'RIGHT'
340 if (game.map_geometry == 'Hex') {
341 this.movement_keys = {
342 [this.keys.hex_move_upleft]: 'UPLEFT',
343 [this.keys.hex_move_upright]: 'UPRIGHT',
344 [this.keys.hex_move_right]: 'RIGHT',
345 [this.keys.hex_move_downright]: 'DOWNRIGHT',
346 [this.keys.hex_move_downleft]: 'DOWNLEFT',
347 [this.keys.hex_move_left]: 'LEFT'
351 switch_mode: function(mode) {
352 this.show_help = false;
353 this.map_mode = 'terrain';
354 if (mode.shows_info && game.player_id in game.things) {
355 explorer.position = game.things[game.player_id].position;
359 this.restore_input_values();
360 if (mode == mode_login) {
361 if (this.login_name) {
362 server.send(['LOGIN', this.login_name]);
364 this.log_msg("? need login name");
366 } else if (mode == mode_edit) {
367 this.show_help = true;
368 } else if (mode == mode_teleport) {
369 tui.log_msg("@ May teleport to: " + tui.teleport_target);
370 tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
374 restore_input_values: function() {
375 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
376 let info = explorer.info_db[explorer.position];
377 if (info != "(none)") {
378 this.inputEl.value = info;
379 this.recalc_input_lines();
381 } else if (this.mode == mode_portal && explorer.position in game.portals) {
382 let portal = game.portals[explorer.position]
383 this.inputEl.value = portal;
384 this.recalc_input_lines();
385 } else if (this.mode == mode_password) {
386 this.inputEl.value = this.password;
387 this.recalc_input_lines();
390 empty_input: function(str) {
391 this.inputEl.value = "";
392 if (this.mode.has_input_prompt) {
393 this.recalc_input_lines();
395 this.height_input = 0;
398 recalc_input_lines: function() {
399 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
400 this.height_input = this.input_lines.length;
402 msg_into_lines_of_width: function(msg, width) {
405 for (let i = 0, x = 0; i < msg.length; i++, x++) {
406 if (x >= width || msg[i] == "\n") {
411 if (msg[i] != "\n") {
418 log_msg: function(msg) {
420 while (this.log.length > 100) {
425 draw_map: function() {
426 let map_lines_split = [];
428 let map_content = game.map;
429 if (this.map_mode == 'control') {
430 map_content = game.map_control;
432 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
433 if (j == game.map_size[1]) {
434 map_lines_split.push(line);
438 line.push(map_content[i]);
440 map_lines_split.push(line);
441 if (this.map_mode == 'terrain') {
442 for (const thing_id in game.things) {
443 let t = game.things[thing_id];
444 map_lines_split[t.position[0]][t.position[1]] = '@';
447 if (tui.mode.shows_info) {
448 map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
451 if (game.map_geometry == 'Square') {
452 for (let line_split of map_lines_split) {
453 map_lines.push(line_split.join(' '));
455 } else if (game.map_geometry == 'Hex') {
457 for (let line_split of map_lines_split) {
458 map_lines.push(' '.repeat(indent) + line_split.join(' '));
466 let window_center = [terminal.rows / 2, this.window_width / 2];
467 let player = game.things[game.player_id];
468 let center_position = [player.position[0], player.position[1]];
469 if (tui.mode.shows_info) {
470 center_position = [explorer.position[0], explorer.position[1]];
472 center_position[1] = center_position[1] * 2;
473 let offset = [center_position[0] - window_center[0],
474 center_position[1] - window_center[1]]
475 if (game.map_geometry == 'Hex' && offset[0] % 2) {
478 let term_y = Math.max(0, -offset[0]);
479 let term_x = Math.max(0, -offset[1]);
480 let map_y = Math.max(0, offset[0]);
481 let map_x = Math.max(0, offset[1]);
482 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
483 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
484 terminal.write(term_y, term_x, to_draw);
487 draw_mode_line: function() {
488 let help = 'hit [' + this.keys.help + '] for help';
489 if (this.mode.has_input_prompt) {
490 help = 'enter /help for help';
492 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
494 draw_turn_line: function(n) {
495 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
497 draw_history: function() {
498 let log_display_lines = [];
499 for (let line of this.log) {
500 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
502 for (let y = terminal.rows - 1 - this.height_input,
503 i = log_display_lines.length - 1;
504 y >= this.height_header && i >= 0;
506 terminal.write(y, this.window_width, log_display_lines[i]);
509 draw_info: function() {
510 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
511 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
512 terminal.write(y, this.window_width, lines[i]);
515 draw_input: function() {
516 if (this.mode.has_input_prompt) {
517 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
518 terminal.write(y, this.window_width, this.input_lines[i]);
522 draw_help: function() {
523 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
524 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
525 if (this.mode == mode_play) {
526 content += "Available actions:\n";
527 if (game.tasks.includes('MOVE')) {
528 content += "[" + movement_keys_desc + "] – move player\n";
530 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
531 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
533 content += '\nOther modes available from here:\n';
534 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
535 content += '[' + this.keys.switch_to_study + '] – study mode\n';
536 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
537 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
538 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
539 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
540 } else if (this.mode == mode_study) {
541 content += "Available actions:\n";
542 content += '[' + movement_keys_desc + '] – move question mark\n';
543 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
544 content += '\nOther modes available from here:\n';
545 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
546 content += '[' + this.keys.switch_to_play + '] – play mode\n';
547 } else if (this.mode == mode_chat) {
548 content += '/nick NAME – re-name yourself to NAME\n';
549 //content += '/msg USER TEXT – send TEXT to USER\n';
550 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
551 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
554 if (!this.mode.has_input_prompt) {
555 start_x = this.window_width
557 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
558 let lines = this.msg_into_lines_of_width(content, this.window_width);
559 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
560 terminal.write(y, start_x, lines[i]);
563 full_refresh: function() {
564 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
565 if (this.mode.is_intro) {
569 if (game.turn_complete) {
571 this.draw_turn_line();
573 this.draw_mode_line();
574 if (this.mode.shows_info) {
581 if (this.show_help) {
593 this.map_control = "";
594 this.map_size = [0,0];
599 get_thing: function(id_, create_if_not_found=false) {
600 if (id_ in game.things) {
601 return game.things[id_];
602 } else if (create_if_not_found) {
603 let t = new Thing([0,0]);
604 game.things[id_] = t;
608 move: function(start_position, direction) {
609 let target = [start_position[0], start_position[1]];
610 if (direction == 'LEFT') {
612 } else if (direction == 'RIGHT') {
614 } else if (game.map_geometry == 'Square') {
615 if (direction == 'UP') {
617 } else if (direction == 'DOWN') {
620 } else if (game.map_geometry == 'Hex') {
621 let start_indented = start_position[0] % 2;
622 if (direction == 'UPLEFT') {
624 if (!start_indented) {
627 } else if (direction == 'UPRIGHT') {
629 if (start_indented) {
632 } else if (direction == 'DOWNLEFT') {
634 if (!start_indented) {
637 } else if (direction == 'DOWNRIGHT') {
639 if (start_indented) {
644 if (target[0] < 0 || target[1] < 0 ||
645 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
655 server.init(websocket_location);
660 move: function(direction) {
661 let target = game.move(this.position, direction);
663 this.position = target
666 terminal.blink_screen();
669 update_info_db: function(yx, str) {
670 this.info_db[yx] = str;
671 if (tui.mode == mode_study) {
675 empty_info_db: function() {
677 if (tui.mode == mode_study) {
681 query_info: function() {
682 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
684 get_info: function() {
685 let position_i = this.position[0] * game.map_size[1] + this.position[1];
686 if (game.fov[position_i] != '.') {
687 return 'outside field of view';
690 info += "TERRAIN: " + game.map[position_i] + "\n";
691 for (let t_id in game.things) {
692 let t = game.things[t_id];
693 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
696 info += ": " + t.name_;
701 if (this.position in game.portals) {
702 info += "PORTAL: " + game.portals[this.position] + "\n";
704 if (this.position in this.info_db) {
705 info += "ANNOTATIONS: " + this.info_db[this.position];
711 annotate: function(msg) {
712 if (msg.length == 0) {
713 msg = " "; // triggers annotation deletion
715 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
717 set_portal: function(msg) {
718 if (msg.length == 0) {
719 msg = " "; // triggers portal deletion
721 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
725 tui.inputEl.addEventListener('input', (event) => {
726 if (tui.mode.has_input_prompt) {
727 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
728 if (tui.inputEl.value.length > max_length) {
729 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
731 tui.recalc_input_lines();
732 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
733 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
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;
746 tui.restore_input_values();
747 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
748 tui.show_help = true;
749 } else if (tui.mode == mode_login && event.key == 'Enter') {
750 tui.login_name = tui.inputEl.value;
751 server.send(['LOGIN', tui.inputEl.value]);
753 } else if (tui.mode == mode_portal && event.key == 'Enter') {
754 explorer.set_portal(tui.inputEl.value);
755 tui.switch_mode(mode_play);
756 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
757 explorer.annotate(tui.inputEl.value);
758 tui.switch_mode(mode_play);
759 } else if (tui.mode == mode_password && event.key == 'Enter') {
760 if (tui.inputEl.value.length == 0) {
761 tui.inputEl.value = " ";
763 tui.password = tui.inputEl.value
764 tui.switch_mode(mode_play);
765 } else if (tui.mode == mode_teleport && event.key == 'Enter') {
766 if (tui.inputEl.value == 'YES!') {
767 server.reconnect_to(tui.teleport_target);
769 tui.log_msg('@ teleport aborted');
770 tui.switch_mode(mode_play);
772 } else if (tui.mode == mode_chat && event.key == 'Enter') {
773 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
774 if (tokens.length > 0 && tokens[0].length > 0) {
775 if (tui.inputEl.value[0][0] == '/') {
776 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
777 tui.switch_mode(mode_play);
778 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
779 tui.switch_mode(mode_study);
780 } else if (tokens[0].slice(1) == 'nick') {
781 if (tokens.length > 1) {
782 server.send(['NICK', tokens[1]]);
784 tui.log_msg('? need new name');
786 //} else if (tokens[0].slice(1) == 'msg') {
787 // if (tokens.length > 2) {
788 // let msg = tui.inputEl.value.slice(token_starts[2]);
789 // server.send(['QUERY', tokens[1], msg]);
791 // tui.log_msg('? need message target and message');
794 tui.log_msg('? unknown command');
797 server.send(['ALL', tui.inputEl.value]);
799 } else if (tui.inputEl.valuelength > 0) {
800 server.send(['ALL', tui.inputEl.value]);
803 } else if (tui.mode == mode_play) {
804 if (event.key === tui.keys.switch_to_chat) {
805 event.preventDefault();
806 tui.switch_mode(mode_chat);
807 } else if (event.key === tui.keys.switch_to_edit
808 && game.tasks.includes('WRITE')) {
809 event.preventDefault();
810 tui.switch_mode(mode_edit);
811 } else if (event.key === tui.keys.switch_to_study) {
812 tui.switch_mode(mode_study);
813 } else if (event.key === tui.keys.switch_to_password) {
814 event.preventDefault();
815 tui.switch_mode(mode_password);
816 } else if (event.key === tui.keys.flatten
817 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
818 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
819 } else if (event.key in tui.movement_keys
820 && game.tasks.includes('MOVE')) {
821 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
822 } else if (event.key === tui.keys.switch_to_portal) {
823 event.preventDefault();
824 tui.switch_mode(mode_portal);
825 } else if (event.key === tui.keys.switch_to_annotate) {
826 event.preventDefault();
827 tui.switch_mode(mode_annotate);
829 } else if (tui.mode == mode_study) {
830 if (event.key === tui.keys.switch_to_chat) {
831 event.preventDefault();
832 tui.switch_mode(mode_chat);
833 } else if (event.key == tui.keys.switch_to_play) {
834 tui.switch_mode(mode_play);
835 } else if (event.key in tui.movement_keys) {
836 explorer.move(tui.movement_keys[event.key]);
837 } else if (event.key == tui.keys.toggle_map_mode) {
838 if (tui.map_mode == 'terrain') {
839 tui.map_mode = 'control';
841 tui.map_mode = 'terrain';
848 rows_selector.addEventListener('input', function() {
849 if (rows_selector.value % 4 != 0) {
852 window.localStorage.setItem(rows_selector.id, rows_selector.value);
853 terminal.initialize();
856 cols_selector.addEventListener('input', function() {
857 if (cols_selector.value % 4 != 0) {
860 window.localStorage.setItem(cols_selector.id, cols_selector.value);
861 terminal.initialize();
862 tui.window_width = terminal.cols / 2,
865 for (let key_selector of key_selectors) {
866 key_selector.addEventListener('input', function() {
867 window.localStorage.setItem(key_selector.id, key_selector.value);
871 window.setInterval(function() {
872 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
873 || document.activeElement.id.startsWith('key_'))) {
877 window.setInterval(function() {
878 if (server.connected) {
879 server.send(['PING']);
881 server.reconnect_to(server.url);
882 tui.log_msg('@ attempting reconnect …')