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 game.thing_types = {};
181 server.send(['TASKS']);
182 server.send(['THING_TYPES']);
183 tui.log_msg("@ server connected! :)");
184 tui.switch_mode(mode_login);
186 this.websocket.onclose = function(event) {
187 server.connected = false;
188 tui.switch_mode(mode_waiting_for_server);
189 tui.log_msg("@ server disconnected :(");
191 this.websocket.onmessage = this.handle_event;
193 reconnect_to: function(url) {
194 this.websocket.close();
197 send: function(tokens) {
198 this.websocket.send(unparser.untokenize(tokens));
200 handle_event: function(event) {
201 let tokens = parser.tokenize(event.data)[0];
202 if (tokens[0] === 'TURN') {
203 game.turn_complete = false;
206 game.turn = parseInt(tokens[1]);
207 } else if (tokens[0] === 'THING') {
208 let t = game.get_thing(tokens[3], true);
209 t.position = parser.parse_yx(tokens[1]);
211 } else if (tokens[0] === 'THING_NAME') {
212 let t = game.get_thing(tokens[1], false);
216 } else if (tokens[0] === 'TASKS') {
217 game.tasks = tokens[1].split(',')
218 } else if (tokens[0] === 'THING_TYPE') {
219 game.thing_types[tokens[1]] = tokens[2]
220 } else if (tokens[0] === 'MAP') {
221 game.map_geometry = tokens[1];
223 game.map_size = parser.parse_yx(tokens[2]);
225 } else if (tokens[0] === 'FOV') {
227 } else if (tokens[0] === 'MAP_CONTROL') {
228 game.map_control = tokens[1]
229 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
230 game.turn_complete = true;
231 explorer.empty_info_db();
232 if (tui.mode == mode_post_login_wait) {
233 tui.switch_mode(mode_play);
234 } else if (tui.mode == mode_study) {
235 explorer.query_info();
237 let t = game.get_thing(game.player_id);
238 if (t.position in game.portals) {
239 tui.teleport_target = game.portals[t.position];
240 tui.switch_mode(mode_teleport);
244 } else if (tokens[0] === 'CHAT') {
245 tui.log_msg('# ' + tokens[1], 1);
246 } else if (tokens[0] === 'PLAYER_ID') {
247 game.player_id = parseInt(tokens[1]);
248 } else if (tokens[0] === 'LOGIN_OK') {
249 this.send(['GET_GAMESTATE']);
250 tui.switch_mode(mode_post_login_wait);
251 } else if (tokens[0] === 'PORTAL') {
252 let position = parser.parse_yx(tokens[1]);
253 game.portals[position] = tokens[2];
254 } else if (tokens[0] === 'ANNOTATION') {
255 let position = parser.parse_yx(tokens[1]);
256 explorer.update_info_db(position, tokens[2]);
257 } else if (tokens[0] === 'UNHANDLED_INPUT') {
258 tui.log_msg('? unknown command');
259 } else if (tokens[0] === 'PLAY_ERROR') {
260 terminal.blink_screen();
261 } else if (tokens[0] === 'ARGUMENT_ERROR') {
262 tui.log_msg('? syntax error: ' + tokens[1]);
263 } else if (tokens[0] === 'GAME_ERROR') {
264 tui.log_msg('? game error: ' + tokens[1]);
265 } else if (tokens[0] === 'PONG') {
268 tui.log_msg('? unhandled input: ' + event.data);
274 quote: function(str) {
276 for (let i = 0; i < str.length; i++) {
278 if (['"', '\\'].includes(c)) {
284 return quoted.join('');
286 to_yx: function(yx_coordinate) {
287 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
289 untokenize: function(tokens) {
290 let quoted_tokens = [];
291 for (let token of tokens) {
292 quoted_tokens.push(this.quote(token));
294 return quoted_tokens.join(" ");
299 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
301 this.has_input_prompt = has_input_prompt;
302 this.shows_info= shows_info;
303 this.is_intro = is_intro;
304 this.help_intro = help_intro;
307 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
308 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
309 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
310 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);
311 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);
312 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
313 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);
314 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);
315 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);
316 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);
317 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);
320 mode: mode_waiting_for_server,
324 window_width: terminal.cols / 2,
331 this.inputEl = document.getElementById("input");
332 this.inputEl.focus();
333 this.recalc_input_lines();
334 this.height_header = this.height_turn_line + this.height_mode_line;
335 this.log_msg("@ waiting for server connection ...");
338 init_keys: function() {
340 for (let key_selector of key_selectors) {
341 this.keys[key_selector.id.slice(4)] = key_selector.value;
343 this.movement_keys = {
344 [this.keys.square_move_up]: 'UP',
345 [this.keys.square_move_left]: 'LEFT',
346 [this.keys.square_move_down]: 'DOWN',
347 [this.keys.square_move_right]: 'RIGHT'
349 if (game.map_geometry == 'Hex') {
350 this.movement_keys = {
351 [this.keys.hex_move_upleft]: 'UPLEFT',
352 [this.keys.hex_move_upright]: 'UPRIGHT',
353 [this.keys.hex_move_right]: 'RIGHT',
354 [this.keys.hex_move_downright]: 'DOWNRIGHT',
355 [this.keys.hex_move_downleft]: 'DOWNLEFT',
356 [this.keys.hex_move_left]: 'LEFT'
360 switch_mode: function(mode) {
361 this.show_help = false;
362 this.map_mode = 'terrain';
363 if (mode.shows_info && game.player_id in game.things) {
364 explorer.position = game.things[game.player_id].position;
368 this.restore_input_values();
369 if (mode == mode_login) {
370 if (this.login_name) {
371 server.send(['LOGIN', this.login_name]);
373 this.log_msg("? need login name");
375 } else if (mode == mode_edit) {
376 this.show_help = true;
377 } else if (mode == mode_teleport) {
378 tui.log_msg("@ May teleport to: " + tui.teleport_target);
379 tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
383 restore_input_values: function() {
384 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
385 let info = explorer.info_db[explorer.position];
386 if (info != "(none)") {
387 this.inputEl.value = info;
388 this.recalc_input_lines();
390 } else if (this.mode == mode_portal && explorer.position in game.portals) {
391 let portal = game.portals[explorer.position]
392 this.inputEl.value = portal;
393 this.recalc_input_lines();
394 } else if (this.mode == mode_password) {
395 this.inputEl.value = this.password;
396 this.recalc_input_lines();
399 empty_input: function(str) {
400 this.inputEl.value = "";
401 if (this.mode.has_input_prompt) {
402 this.recalc_input_lines();
404 this.height_input = 0;
407 recalc_input_lines: function() {
408 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
409 this.height_input = this.input_lines.length;
411 msg_into_lines_of_width: function(msg, width) {
414 for (let i = 0, x = 0; i < msg.length; i++, x++) {
415 if (x >= width || msg[i] == "\n") {
420 if (msg[i] != "\n") {
427 log_msg: function(msg) {
429 while (this.log.length > 100) {
434 draw_map: function() {
435 let map_lines_split = [];
437 let map_content = game.map;
438 if (this.map_mode == 'control') {
439 map_content = game.map_control;
441 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
442 if (j == game.map_size[1]) {
443 map_lines_split.push(line);
447 line.push(map_content[i]);
449 map_lines_split.push(line);
450 if (this.map_mode == 'terrain') {
451 for (const thing_id in game.things) {
452 let t = game.things[thing_id];
453 let symbol = game.thing_types[t.type_];
454 map_lines_split[t.position[0]][t.position[1]] = symbol;
457 if (tui.mode.shows_info) {
458 map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
461 if (game.map_geometry == 'Square') {
462 for (let line_split of map_lines_split) {
463 map_lines.push(line_split.join(' '));
465 } else if (game.map_geometry == 'Hex') {
467 for (let line_split of map_lines_split) {
468 map_lines.push(' '.repeat(indent) + line_split.join(' '));
476 let window_center = [terminal.rows / 2, this.window_width / 2];
477 let player = game.things[game.player_id];
478 let center_position = [player.position[0], player.position[1]];
479 if (tui.mode.shows_info) {
480 center_position = [explorer.position[0], explorer.position[1]];
482 center_position[1] = center_position[1] * 2;
483 let offset = [center_position[0] - window_center[0],
484 center_position[1] - window_center[1]]
485 if (game.map_geometry == 'Hex' && offset[0] % 2) {
488 let term_y = Math.max(0, -offset[0]);
489 let term_x = Math.max(0, -offset[1]);
490 let map_y = Math.max(0, offset[0]);
491 let map_x = Math.max(0, offset[1]);
492 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
493 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
494 terminal.write(term_y, term_x, to_draw);
497 draw_mode_line: function() {
498 let help = 'hit [' + this.keys.help + '] for help';
499 if (this.mode.has_input_prompt) {
500 help = 'enter /help for help';
502 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
504 draw_turn_line: function(n) {
505 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
507 draw_history: function() {
508 let log_display_lines = [];
509 for (let line of this.log) {
510 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
512 for (let y = terminal.rows - 1 - this.height_input,
513 i = log_display_lines.length - 1;
514 y >= this.height_header && i >= 0;
516 terminal.write(y, this.window_width, log_display_lines[i]);
519 draw_info: function() {
520 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
521 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
522 terminal.write(y, this.window_width, lines[i]);
525 draw_input: function() {
526 if (this.mode.has_input_prompt) {
527 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
528 terminal.write(y, this.window_width, this.input_lines[i]);
532 draw_help: function() {
533 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
534 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
535 if (this.mode == mode_play) {
536 content += "Available actions:\n";
537 if (game.tasks.includes('MOVE')) {
538 content += "[" + movement_keys_desc + "] – move player\n";
540 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
541 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
543 content += '\nOther modes available from here:\n';
544 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
545 content += '[' + this.keys.switch_to_study + '] – study mode\n';
546 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
547 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
548 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
549 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
550 } else if (this.mode == mode_study) {
551 content += "Available actions:\n";
552 content += '[' + movement_keys_desc + '] – move question mark\n';
553 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
554 content += '\nOther modes available from here:\n';
555 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
556 content += '[' + this.keys.switch_to_play + '] – play mode\n';
557 } else if (this.mode == mode_chat) {
558 content += '/nick NAME – re-name yourself to NAME\n';
559 //content += '/msg USER TEXT – send TEXT to USER\n';
560 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
561 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
564 if (!this.mode.has_input_prompt) {
565 start_x = this.window_width
567 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
568 let lines = this.msg_into_lines_of_width(content, this.window_width);
569 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
570 terminal.write(y, start_x, lines[i]);
573 full_refresh: function() {
574 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
575 if (this.mode.is_intro) {
579 if (game.turn_complete) {
581 this.draw_turn_line();
583 this.draw_mode_line();
584 if (this.mode.shows_info) {
591 if (this.show_help) {
603 this.map_control = "";
604 this.map_size = [0,0];
609 get_thing: function(id_, create_if_not_found=false) {
610 if (id_ in game.things) {
611 return game.things[id_];
612 } else if (create_if_not_found) {
613 let t = new Thing([0,0]);
614 game.things[id_] = t;
618 move: function(start_position, direction) {
619 let target = [start_position[0], start_position[1]];
620 if (direction == 'LEFT') {
622 } else if (direction == 'RIGHT') {
624 } else if (game.map_geometry == 'Square') {
625 if (direction == 'UP') {
627 } else if (direction == 'DOWN') {
630 } else if (game.map_geometry == 'Hex') {
631 let start_indented = start_position[0] % 2;
632 if (direction == 'UPLEFT') {
634 if (!start_indented) {
637 } else if (direction == 'UPRIGHT') {
639 if (start_indented) {
642 } else if (direction == 'DOWNLEFT') {
644 if (!start_indented) {
647 } else if (direction == 'DOWNRIGHT') {
649 if (start_indented) {
654 if (target[0] < 0 || target[1] < 0 ||
655 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
665 server.init(websocket_location);
670 move: function(direction) {
671 let target = game.move(this.position, direction);
673 this.position = target
676 terminal.blink_screen();
679 update_info_db: function(yx, str) {
680 this.info_db[yx] = str;
681 if (tui.mode == mode_study) {
685 empty_info_db: function() {
687 if (tui.mode == mode_study) {
691 query_info: function() {
692 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
694 get_info: function() {
695 let position_i = this.position[0] * game.map_size[1] + this.position[1];
696 if (game.fov[position_i] != '.') {
697 return 'outside field of view';
700 info += "TERRAIN: " + game.map[position_i] + "\n";
701 for (let t_id in game.things) {
702 let t = game.things[t_id];
703 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
704 info += "THING: " + t.type_;
706 info += " (name: " + t.name_ + ")";
711 if (this.position in game.portals) {
712 info += "PORTAL: " + game.portals[this.position] + "\n";
714 if (this.position in this.info_db) {
715 info += "ANNOTATIONS: " + this.info_db[this.position];
721 annotate: function(msg) {
722 if (msg.length == 0) {
723 msg = " "; // triggers annotation deletion
725 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
727 set_portal: function(msg) {
728 if (msg.length == 0) {
729 msg = " "; // triggers portal deletion
731 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
735 tui.inputEl.addEventListener('input', (event) => {
736 if (tui.mode.has_input_prompt) {
737 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
738 if (tui.inputEl.value.length > max_length) {
739 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
741 tui.recalc_input_lines();
742 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
743 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
744 tui.switch_mode(mode_play);
748 tui.inputEl.addEventListener('keydown', (event) => {
749 tui.show_help = false;
750 if (event.key == 'Enter') {
751 event.preventDefault();
753 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
754 tui.show_help = true;
756 tui.restore_input_values();
757 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
758 tui.show_help = true;
759 } else if (tui.mode == mode_login && event.key == 'Enter') {
760 tui.login_name = tui.inputEl.value;
761 server.send(['LOGIN', tui.inputEl.value]);
763 } else if (tui.mode == mode_portal && event.key == 'Enter') {
764 explorer.set_portal(tui.inputEl.value);
765 tui.switch_mode(mode_play);
766 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
767 explorer.annotate(tui.inputEl.value);
768 tui.switch_mode(mode_play);
769 } else if (tui.mode == mode_password && event.key == 'Enter') {
770 if (tui.inputEl.value.length == 0) {
771 tui.inputEl.value = " ";
773 tui.password = tui.inputEl.value
774 tui.switch_mode(mode_play);
775 } else if (tui.mode == mode_teleport && event.key == 'Enter') {
776 if (tui.inputEl.value == 'YES!') {
777 server.reconnect_to(tui.teleport_target);
779 tui.log_msg('@ teleport aborted');
780 tui.switch_mode(mode_play);
782 } else if (tui.mode == mode_chat && event.key == 'Enter') {
783 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
784 if (tokens.length > 0 && tokens[0].length > 0) {
785 if (tui.inputEl.value[0][0] == '/') {
786 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
787 tui.switch_mode(mode_play);
788 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
789 tui.switch_mode(mode_study);
790 } else if (tokens[0].slice(1) == 'nick') {
791 if (tokens.length > 1) {
792 server.send(['NICK', tokens[1]]);
794 tui.log_msg('? need new name');
796 //} else if (tokens[0].slice(1) == 'msg') {
797 // if (tokens.length > 2) {
798 // let msg = tui.inputEl.value.slice(token_starts[2]);
799 // server.send(['QUERY', tokens[1], msg]);
801 // tui.log_msg('? need message target and message');
804 tui.log_msg('? unknown command');
807 server.send(['ALL', tui.inputEl.value]);
809 } else if (tui.inputEl.valuelength > 0) {
810 server.send(['ALL', tui.inputEl.value]);
813 } else if (tui.mode == mode_play) {
814 if (event.key === tui.keys.switch_to_chat) {
815 event.preventDefault();
816 tui.switch_mode(mode_chat);
817 } else if (event.key === tui.keys.switch_to_edit
818 && game.tasks.includes('WRITE')) {
819 event.preventDefault();
820 tui.switch_mode(mode_edit);
821 } else if (event.key === tui.keys.switch_to_study) {
822 tui.switch_mode(mode_study);
823 } else if (event.key === tui.keys.switch_to_password) {
824 event.preventDefault();
825 tui.switch_mode(mode_password);
826 } else if (event.key === tui.keys.flatten
827 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
828 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
829 } else if (event.key in tui.movement_keys
830 && game.tasks.includes('MOVE')) {
831 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
832 } else if (event.key === tui.keys.switch_to_portal) {
833 event.preventDefault();
834 tui.switch_mode(mode_portal);
835 } else if (event.key === tui.keys.switch_to_annotate) {
836 event.preventDefault();
837 tui.switch_mode(mode_annotate);
839 } else if (tui.mode == mode_study) {
840 if (event.key === tui.keys.switch_to_chat) {
841 event.preventDefault();
842 tui.switch_mode(mode_chat);
843 } else if (event.key == tui.keys.switch_to_play) {
844 tui.switch_mode(mode_play);
845 } else if (event.key in tui.movement_keys) {
846 explorer.move(tui.movement_keys[event.key]);
847 } else if (event.key == tui.keys.toggle_map_mode) {
848 if (tui.map_mode == 'terrain') {
849 tui.map_mode = 'control';
851 tui.map_mode = 'terrain';
858 rows_selector.addEventListener('input', function() {
859 if (rows_selector.value % 4 != 0) {
862 window.localStorage.setItem(rows_selector.id, rows_selector.value);
863 terminal.initialize();
866 cols_selector.addEventListener('input', function() {
867 if (cols_selector.value % 4 != 0) {
870 window.localStorage.setItem(cols_selector.id, cols_selector.value);
871 terminal.initialize();
872 tui.window_width = terminal.cols / 2,
875 for (let key_selector of key_selectors) {
876 key_selector.addEventListener('input', function() {
877 window.localStorage.setItem(key_selector.id, key_selector.value);
881 window.setInterval(function() {
882 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
883 || document.activeElement.id.startsWith('key_'))) {
887 window.setInterval(function() {
888 if (server.connected) {
889 server.send(['PING']);
891 server.reconnect_to(server.url);
892 tui.log_msg('@ attempting reconnect …')