7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
33 <button id="switch_to_admin">become admin</button>
34 <button id="switch_to_control_pw">change tile control password</button>
36 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
38 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
39 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
40 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
41 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
42 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
43 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
44 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
45 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
46 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
47 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
48 <li>help: <input id="key_help" type="text" value="h" />
49 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
50 <li>teleport: <input id="key_teleport" type="text" value="p" />
51 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
52 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
53 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
54 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
55 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
56 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
57 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
58 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
59 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw" type="text" value="C" />
60 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
61 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
62 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
67 let websocket_location = "wss://plomlompom.com/rogue_chat/";
68 //let websocket_location = "ws://localhost:8000/";
71 'play': 'This mode allows you to interact with the map.',
72 '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.',
73 '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.',
74 'control_pw_type': 'This mode is the first of two steps to change the password for a tile control character. First enter the tile control character for which you want to change the password!',
75 'control_pw_pw': 'This mode is the second of two steps to change the password for a tile control character. Enter the new password for the tile control character you chose.',
76 '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.',
77 '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.',
78 '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:',
79 'login': 'Pick your player name.',
80 'post_login_wait': 'Waiting for a server response.',
81 '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.',
82 'admin': 'This mode allows you to become admin if you know an admin password.'
85 let rows_selector = document.getElementById("n_rows");
86 let cols_selector = document.getElementById("n_cols");
87 let key_selectors = document.querySelectorAll('[id^="key_"]');
89 function restore_selector_value(selector) {
90 let stored_selection = window.localStorage.getItem(selector.id);
91 if (stored_selection) {
92 selector.value = stored_selection;
95 restore_selector_value(rows_selector);
96 restore_selector_value(cols_selector);
97 for (let key_selector of key_selectors) {
98 restore_selector_value(key_selector);
104 initialize: function() {
105 this.rows = rows_selector.value;
106 this.cols = cols_selector.value;
107 this.pre_el = document.getElementById("terminal");
108 this.pre_el.style.color = this.foreground;
109 this.pre_el.style.backgroundColor = this.background;
112 for (let y = 0, x = 0; y <= this.rows; x++) {
113 if (x == this.cols) {
116 this.content.push(line);
118 if (y == this.rows) {
125 blink_screen: function() {
126 this.pre_el.style.color = this.background;
127 this.pre_el.style.backgroundColor = this.foreground;
129 this.pre_el.style.color = this.foreground;
130 this.pre_el.style.backgroundColor = this.background;
133 refresh: function() {
135 for (let y = 0; y < this.rows; y++) {
136 let line = this.content[y].join('');
137 pre_string += line + '\n';
139 this.pre_el.textContent = pre_string;
141 write: function(start_y, start_x, msg) {
142 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
143 this.content[start_y][x] = msg[i];
146 drawBox: function(start_y, start_x, height, width) {
147 let end_y = start_y + height;
148 let end_x = start_x + width;
149 for (let y = start_y, x = start_x; y < this.rows; x++) {
157 this.content[y][x] = ' ';
161 terminal.initialize();
164 tokenize: function(str) {
169 for (let i = 0; i < str.length; i++) {
175 } else if (c == '\\') {
177 } else if (c == '"') {
182 } else if (c == '"') {
184 } else if (c === ' ') {
185 if (token.length > 0) {
193 if (token.length > 0) {
198 parse_yx: function(position_string) {
199 let coordinate_strings = position_string.split(',')
200 let position = [0, 0];
201 position[0] = parseInt(coordinate_strings[0].slice(2));
202 position[1] = parseInt(coordinate_strings[1].slice(2));
214 init: function(url) {
216 this.websocket = new WebSocket(this.url);
217 this.websocket.onopen = function(event) {
218 server.connected = true;
219 game.thing_types = {};
221 server.send(['TASKS']);
222 server.send(['TERRAINS']);
223 server.send(['THING_TYPES']);
224 tui.log_msg("@ server connected! :)");
225 tui.switch_mode('login');
227 this.websocket.onclose = function(event) {
228 server.connected = false;
229 tui.switch_mode('waiting_for_server');
230 tui.log_msg("@ server disconnected :(");
232 this.websocket.onmessage = this.handle_event;
234 reconnect_to: function(url) {
235 this.websocket.close();
238 send: function(tokens) {
239 this.websocket.send(unparser.untokenize(tokens));
241 handle_event: function(event) {
242 let tokens = parser.tokenize(event.data);
243 if (tokens[0] === 'TURN') {
244 game.turn_complete = false;
245 explorer.empty_info_db();
248 game.turn = parseInt(tokens[1]);
249 } else if (tokens[0] === 'THING') {
250 let t = game.get_thing(tokens[3], true);
251 t.position = parser.parse_yx(tokens[1]);
253 } else if (tokens[0] === 'THING_NAME') {
254 let t = game.get_thing(tokens[1], false);
258 } else if (tokens[0] === 'THING_CHAR') {
259 let t = game.get_thing(tokens[1], false);
261 t.player_char = tokens[2];
263 } else if (tokens[0] === 'TASKS') {
264 game.tasks = tokens[1].split(',')
265 } else if (tokens[0] === 'THING_TYPE') {
266 game.thing_types[tokens[1]] = tokens[2]
267 } else if (tokens[0] === 'TERRAIN') {
268 game.terrains[tokens[1]] = tokens[2]
269 } else if (tokens[0] === 'MAP') {
270 game.map_geometry = tokens[1];
272 game.map_size = parser.parse_yx(tokens[2]);
274 } else if (tokens[0] === 'FOV') {
276 } else if (tokens[0] === 'MAP_CONTROL') {
277 game.map_control = tokens[1]
278 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
279 game.turn_complete = true;
280 if (tui.mode.name == 'post_login_wait') {
281 tui.switch_mode('play');
282 } else if (tui.mode.name == 'study') {
283 explorer.query_info();
286 } else if (tokens[0] === 'CHAT') {
287 tui.log_msg('# ' + tokens[1], 1);
288 } else if (tokens[0] === 'PLAYER_ID') {
289 game.player_id = parseInt(tokens[1]);
290 } else if (tokens[0] === 'LOGIN_OK') {
291 this.send(['GET_GAMESTATE']);
292 tui.switch_mode('post_login_wait');
293 } else if (tokens[0] === 'PORTAL') {
294 let position = parser.parse_yx(tokens[1]);
295 game.portals[position] = tokens[2];
296 } else if (tokens[0] === 'ANNOTATION_HINT') {
297 let position = parser.parse_yx(tokens[1]);
298 explorer.info_hints = explorer.info_hints.concat([position]);
299 } else if (tokens[0] === 'ANNOTATION') {
300 let position = parser.parse_yx(tokens[1]);
301 explorer.update_info_db(position, tokens[2]);
302 tui.restore_input_values();
304 } else if (tokens[0] === 'UNHANDLED_INPUT') {
305 tui.log_msg('? unknown command');
306 } else if (tokens[0] === 'PLAY_ERROR') {
307 tui.log_msg('? ' + tokens[1]);
308 terminal.blink_screen();
309 } else if (tokens[0] === 'ARGUMENT_ERROR') {
310 tui.log_msg('? syntax error: ' + tokens[1]);
311 } else if (tokens[0] === 'GAME_ERROR') {
312 tui.log_msg('? game error: ' + tokens[1]);
313 } else if (tokens[0] === 'PONG') {
316 tui.log_msg('? unhandled input: ' + event.data);
322 quote: function(str) {
324 for (let i = 0; i < str.length; i++) {
326 if (['"', '\\'].includes(c)) {
332 return quoted.join('');
334 to_yx: function(yx_coordinate) {
335 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
337 untokenize: function(tokens) {
338 let quoted_tokens = [];
339 for (let token of tokens) {
340 quoted_tokens.push(this.quote(token));
342 return quoted_tokens.join(" ");
347 constructor(name, has_input_prompt=false, shows_info=false,
348 is_intro=false, is_single_char_entry=false) {
350 this.has_input_prompt = has_input_prompt;
351 this.shows_info= shows_info;
352 this.is_intro = is_intro;
353 this.help_intro = mode_helps[name];
354 this.is_single_char_entry = is_single_char_entry;
361 window_width: terminal.cols / 2,
367 mode_waiting_for_server: new Mode('waiting_for_server',
369 mode_login: new Mode('login', true, false, true),
370 mode_post_login_wait: new Mode('post_login_wait'),
371 mode_chat: new Mode('chat', true),
372 mode_annotate: new Mode('annotate', true, true),
373 mode_play: new Mode('play'),
374 mode_study: new Mode('study', false, true),
375 mode_edit: new Mode('edit', false, false, false, true),
376 mode_control_pw_type: new Mode('control_pw_type',
377 false, false, false, true),
378 mode_portal: new Mode('portal', true, true),
379 mode_password: new Mode('password', true),
380 mode_admin: new Mode('admin', true),
381 mode_control_pw_pw: new Mode('control_pw_pw', true),
383 this.mode = this.mode_waiting_for_server;
384 this.inputEl = document.getElementById("input");
385 this.inputEl.focus();
386 this.recalc_input_lines();
387 this.height_header = this.height_turn_line + this.height_mode_line;
388 this.log_msg("@ waiting for server connection ...");
391 init_keys: function() {
393 for (let key_selector of key_selectors) {
394 this.keys[key_selector.id.slice(4)] = key_selector.value;
396 this.movement_keys = {
397 [this.keys.square_move_up]: 'UP',
398 [this.keys.square_move_left]: 'LEFT',
399 [this.keys.square_move_down]: 'DOWN',
400 [this.keys.square_move_right]: 'RIGHT'
402 if (game.map_geometry == 'Hex') {
403 this.movement_keys = {
404 [this.keys.hex_move_upleft]: 'UPLEFT',
405 [this.keys.hex_move_upright]: 'UPRIGHT',
406 [this.keys.hex_move_right]: 'RIGHT',
407 [this.keys.hex_move_downright]: 'DOWNRIGHT',
408 [this.keys.hex_move_downleft]: 'DOWNLEFT',
409 [this.keys.hex_move_left]: 'LEFT'
413 switch_mode: function(mode_name) {
414 this.inputEl.focus();
415 this.map_mode = 'terrain';
416 this.mode = this['mode_' + mode_name];
417 if (this.mode.shows_info && game.player_id in game.things) {
418 explorer.position = game.things[game.player_id].position;
419 explorer.query_info();
422 this.restore_input_values();
423 document.getElementById("take_thing").disabled = true;
424 document.getElementById("drop_thing").disabled = true;
425 document.getElementById("flatten").disabled = true;
426 document.getElementById("teleport").disabled = true;
427 document.getElementById("toggle_map_mode").disabled = true;
428 document.getElementById("switch_to_chat").disabled = true;
429 document.getElementById("switch_to_play").disabled = true;
430 document.getElementById("switch_to_study").disabled = true;
431 document.getElementById("switch_to_edit").disabled = true;
432 document.getElementById("switch_to_portal").disabled = true;
433 document.getElementById("switch_to_annotate").disabled = true;
434 document.getElementById("switch_to_password").disabled = true;
435 document.getElementById("switch_to_admin").disabled = true;
436 document.getElementById("switch_to_control_pw").disabled = true;
437 document.getElementById("move_left").disabled = true;
438 document.getElementById("move_upleft").disabled = true;
439 document.getElementById("move_up").disabled = true;
440 document.getElementById("move_upright").disabled = true;
441 document.getElementById("move_downleft").disabled = true;
442 document.getElementById("move_down").disabled = true;
443 document.getElementById("move_downright").disabled = true;
444 document.getElementById("move_right").disabled = true;
445 if (this.mode.name == 'play' || this.mode.name == 'study') {
446 document.getElementById("move_left").disabled = false;
447 document.getElementById("move_right").disabled = false;
448 if (game.map_geometry == 'Hex') {
449 document.getElementById("move_upleft").disabled = false;
450 document.getElementById("move_upright").disabled = false;
451 document.getElementById("move_downleft").disabled = false;
452 document.getElementById("move_downright").disabled = false;
454 document.getElementById("move_up").disabled = false;
455 document.getElementById("move_down").disabled = false;
458 if (!this.mode.is_intro && this.mode.name != 'play') {
459 document.getElementById("switch_to_play").disabled = false;
461 if (!this.mode.is_intro && this.mode.name != 'study') {
462 document.getElementById("switch_to_study").disabled = false;
464 if (!this.mode.is_intro && this.mode.name != 'chat') {
465 document.getElementById("switch_to_chat").disabled = false;
467 if (this.mode.name == 'login') {
468 if (this.login_name) {
469 server.send(['LOGIN', this.login_name]);
471 this.log_msg("? need login name");
473 } else if (this.mode.name == 'play') {
474 if (game.tasks.includes('PICK_UP')) {
475 document.getElementById("take_thing").disabled = false;
477 if (game.tasks.includes('DROP')) {
478 document.getElementById("drop_thing").disabled = false;
480 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
481 document.getElementById("flatten").disabled = false;
483 if (game.tasks.includes('MOVE')) {
485 document.getElementById("teleport").disabled = false;
486 document.getElementById("switch_to_annotate").disabled = false;
487 document.getElementById("switch_to_edit").disabled = false;
488 document.getElementById("switch_to_portal").disabled = false;
489 document.getElementById("switch_to_password").disabled = false;
490 document.getElementById("switch_to_admin").disabled = false;
491 document.getElementById("switch_to_control_pw").disabled = false;
492 } else if (this.mode.name == 'study') {
493 document.getElementById("toggle_map_mode").disabled = false;
494 } else if (this.mode.is_single_char_entry) {
495 this.show_help = true;
496 } else if (this.mode.name == 'admin') {
497 this.log_msg('@ enter admin password:')
498 } else if (this.mode.name == 'control_pw_pw') {
499 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
503 restore_input_values: function() {
504 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
505 let info = explorer.info_db[explorer.position];
506 if (info != "(none)") {
507 this.inputEl.value = info;
508 this.recalc_input_lines();
510 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
511 let portal = game.portals[explorer.position]
512 this.inputEl.value = portal;
513 this.recalc_input_lines();
514 } else if (this.mode.name == 'password') {
515 this.inputEl.value = this.password;
516 this.recalc_input_lines();
519 empty_input: function(str) {
520 this.inputEl.value = "";
521 if (this.mode.has_input_prompt) {
522 this.recalc_input_lines();
524 this.height_input = 0;
527 recalc_input_lines: function() {
528 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
529 this.height_input = this.input_lines.length;
531 msg_into_lines_of_width: function(msg, width) {
534 for (let i = 0, x = 0; i < msg.length; i++, x++) {
535 if (x >= width || msg[i] == "\n") {
540 if (msg[i] != "\n") {
547 log_msg: function(msg) {
549 while (this.log.length > 100) {
554 draw_map: function() {
555 let map_lines_split = [];
557 let map_content = game.map;
558 if (this.map_mode == 'control') {
559 map_content = game.map_control;
561 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
562 if (j == game.map_size[1]) {
563 map_lines_split.push(line);
567 line.push(map_content[i] + ' ');
569 map_lines_split.push(line);
570 if (this.map_mode == 'annotations') {
571 for (const coordinate of explorer.info_hints) {
572 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
574 } else if (this.map_mode == 'terrain') {
575 for (const p in game.portals) {
576 let coordinate = p.split(',')
577 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
579 let used_positions = [];
580 for (const thing_id in game.things) {
581 let t = game.things[thing_id];
582 let symbol = game.thing_types[t.type_];
585 meta_char = t.player_char;
587 if (used_positions.includes(t.position.toString())) {
590 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
591 used_positions.push(t.position.toString());
594 if (tui.mode.shows_info) {
595 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
598 if (game.map_geometry == 'Square') {
599 for (let line_split of map_lines_split) {
600 map_lines.push(line_split.join(''));
602 } else if (game.map_geometry == 'Hex') {
604 for (let line_split of map_lines_split) {
605 map_lines.push(' '.repeat(indent) + line_split.join(''));
613 let window_center = [terminal.rows / 2, this.window_width / 2];
614 let player = game.things[game.player_id];
615 let center_position = [player.position[0], player.position[1]];
616 if (tui.mode.shows_info) {
617 center_position = [explorer.position[0], explorer.position[1]];
619 center_position[1] = center_position[1] * 2;
620 let offset = [center_position[0] - window_center[0],
621 center_position[1] - window_center[1]]
622 if (game.map_geometry == 'Hex' && offset[0] % 2) {
625 let term_y = Math.max(0, -offset[0]);
626 let term_x = Math.max(0, -offset[1]);
627 let map_y = Math.max(0, offset[0]);
628 let map_x = Math.max(0, offset[1]);
629 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
630 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
631 terminal.write(term_y, term_x, to_draw);
634 draw_mode_line: function() {
635 let help = 'hit [' + this.keys.help + '] for help';
636 if (this.mode.has_input_prompt) {
637 help = 'enter /help for help';
639 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
641 draw_turn_line: function(n) {
642 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
644 draw_history: function() {
645 let log_display_lines = [];
646 for (let line of this.log) {
647 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
649 for (let y = terminal.rows - 1 - this.height_input,
650 i = log_display_lines.length - 1;
651 y >= this.height_header && i >= 0;
653 terminal.write(y, this.window_width, log_display_lines[i]);
656 draw_info: function() {
657 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
658 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
659 terminal.write(y, this.window_width, lines[i]);
662 draw_input: function() {
663 if (this.mode.has_input_prompt) {
664 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
665 terminal.write(y, this.window_width, this.input_lines[i]);
669 draw_help: function() {
670 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
671 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
672 if (this.mode.name == 'play') {
673 content += "Available actions:\n";
674 if (game.tasks.includes('MOVE')) {
675 content += "[" + movement_keys_desc + "] – move player\n";
677 if (game.tasks.includes('PICK_UP')) {
678 content += "[" + this.keys.take_thing + "] – take thing under player\n";
680 if (game.tasks.includes('DROP')) {
681 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
683 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
684 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
686 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
687 content += '\nOther modes available from here:\n';
688 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
689 content += '[' + this.keys.switch_to_study + '] – study mode\n';
690 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
691 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
692 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
693 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
694 content += '[' + this.keys.switch_to_admin + '] – become admin\n';
695 content += '[' + this.keys.switch_to_control_pw + '] – change tile control password\n';
696 } else if (this.mode.name == 'study') {
697 content += "Available actions:\n";
698 content += '[' + movement_keys_desc + '] – move question mark\n';
699 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
700 content += '\nOther modes available from here:\n';
701 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
702 content += '[' + this.keys.switch_to_play + '] – play mode\n';
703 } else if (this.mode.name == 'chat') {
704 content += '/nick NAME – re-name yourself to NAME\n';
705 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
706 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
709 if (!this.mode.has_input_prompt) {
710 start_x = this.window_width
712 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
713 let lines = this.msg_into_lines_of_width(content, this.window_width);
714 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
715 terminal.write(y, start_x, lines[i]);
718 full_refresh: function() {
719 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
720 if (this.mode.is_intro) {
724 if (game.turn_complete) {
726 this.draw_turn_line();
728 this.draw_mode_line();
729 if (this.mode.shows_info) {
736 if (this.show_help) {
748 this.map_control = "";
749 this.map_size = [0,0];
754 get_thing: function(id_, create_if_not_found=false) {
755 if (id_ in game.things) {
756 return game.things[id_];
757 } else if (create_if_not_found) {
758 let t = new Thing([0,0]);
759 game.things[id_] = t;
763 move: function(start_position, direction) {
764 let target = [start_position[0], start_position[1]];
765 if (direction == 'LEFT') {
767 } else if (direction == 'RIGHT') {
769 } else if (game.map_geometry == 'Square') {
770 if (direction == 'UP') {
772 } else if (direction == 'DOWN') {
775 } else if (game.map_geometry == 'Hex') {
776 let start_indented = start_position[0] % 2;
777 if (direction == 'UPLEFT') {
779 if (!start_indented) {
782 } else if (direction == 'UPRIGHT') {
784 if (start_indented) {
787 } else if (direction == 'DOWNLEFT') {
789 if (!start_indented) {
792 } else if (direction == 'DOWNRIGHT') {
794 if (start_indented) {
799 if (target[0] < 0 || target[1] < 0 ||
800 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
805 teleport: function() {
806 let player = this.get_thing(game.player_id);
807 if (player.position in this.portals) {
808 server.reconnect_to(this.portals[player.position]);
810 terminal.blink_screen();
811 tui.log_msg('? not standing on portal')
819 server.init(websocket_location);
825 move: function(direction) {
826 let target = game.move(this.position, direction);
828 this.position = target
831 terminal.blink_screen();
834 update_info_db: function(yx, str) {
835 this.info_db[yx] = str;
836 if (tui.mode.name == 'study') {
840 empty_info_db: function() {
842 this.info_hints = [];
843 if (tui.mode.name == 'study') {
847 query_info: function() {
848 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
850 get_info: function() {
851 let position_i = this.position[0] * game.map_size[1] + this.position[1];
852 if (game.fov[position_i] != '.') {
853 return 'outside field of view';
856 let terrain_char = game.map[position_i]
857 let terrain_desc = '?'
858 if (game.terrains[terrain_char]) {
859 terrain_desc = game.terrains[terrain_char];
861 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
862 let protection = game.map_control[position_i];
863 if (protection == '.') {
864 protection = 'unprotected';
866 info += 'PROTECTION: ' + protection + '\n';
867 for (let t_id in game.things) {
868 let t = game.things[t_id];
869 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
870 let symbol = game.thing_types[t.type_];
871 info += "THING: " + t.type_ + " / " + symbol;
873 info += t.player_char;
876 info += " (" + t.name_ + ")";
881 if (this.position in game.portals) {
882 info += "PORTAL: " + game.portals[this.position] + "\n";
884 if (this.position in this.info_db) {
885 info += "ANNOTATIONS: " + this.info_db[this.position];
891 annotate: function(msg) {
892 if (msg.length == 0) {
893 msg = " "; // triggers annotation deletion
895 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
897 set_portal: function(msg) {
898 if (msg.length == 0) {
899 msg = " "; // triggers portal deletion
901 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
905 tui.inputEl.addEventListener('input', (event) => {
906 if (tui.mode.has_input_prompt) {
907 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
908 if (tui.inputEl.value.length > max_length) {
909 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
911 tui.recalc_input_lines();
912 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
913 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
914 tui.switch_mode('play');
915 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
916 tui.tile_control_char = tui.inputEl.value[0];
917 tui.switch_mode('control_pw_pw');
921 tui.inputEl.addEventListener('keydown', (event) => {
922 tui.show_help = false;
923 if (event.key == 'Enter') {
924 event.preventDefault();
926 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
927 tui.show_help = true;
929 tui.restore_input_values();
930 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
931 && !tui.mode.is_single_char_entry) {
932 tui.show_help = true;
933 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
934 tui.login_name = tui.inputEl.value;
935 server.send(['LOGIN', tui.inputEl.value]);
937 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
938 if (tui.inputEl.value.length == 0) {
939 tui.log_msg('@ aborted');
941 server.send(['SET_MAP_CONTROL_PASSWORD',
942 tui.tile_control_char, tui.inputEl.value]);
944 tui.switch_mode('play');
945 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
946 explorer.set_portal(tui.inputEl.value);
947 tui.switch_mode('play');
948 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
949 explorer.annotate(tui.inputEl.value);
950 tui.switch_mode('play');
951 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
952 if (tui.inputEl.value.length == 0) {
953 tui.inputEl.value = " ";
955 tui.password = tui.inputEl.value
956 tui.switch_mode('play');
957 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
958 server.send(['BECOME_ADMIN', tui.inputEl.value]);
959 tui.switch_mode('play');
960 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
961 let tokens = parser.tokenize(tui.inputEl.value);
962 if (tokens.length > 0 && tokens[0].length > 0) {
963 if (tui.inputEl.value[0][0] == '/') {
964 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
965 tui.switch_mode('play');
966 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
967 tui.switch_mode('study');
968 } else if (tokens[0].slice(1) == 'nick') {
969 if (tokens.length > 1) {
970 server.send(['NICK', tokens[1]]);
972 tui.log_msg('? need new name');
975 tui.log_msg('? unknown command');
978 server.send(['ALL', tui.inputEl.value]);
980 } else if (tui.inputEl.valuelength > 0) {
981 server.send(['ALL', tui.inputEl.value]);
984 } else if (tui.mode.name == 'play') {
985 if (event.key === tui.keys.switch_to_chat) {
986 event.preventDefault();
987 tui.switch_mode('chat');
988 } else if (event.key === tui.keys.switch_to_edit
989 && game.tasks.includes('WRITE')) {
990 event.preventDefault();
991 tui.switch_mode('edit');
992 } else if (event.key === tui.keys.switch_to_study) {
993 tui.switch_mode('study');
994 } else if (event.key === tui.keys.switch_to_admin) {
995 event.preventDefault();
996 tui.switch_mode('admin');
997 } else if (event.key === tui.keys.switch_to_control_pw) {
998 event.preventDefault();
999 tui.switch_mode('control_pw_type');
1000 } else if (event.key === tui.keys.switch_to_password) {
1001 event.preventDefault();
1002 tui.switch_mode('password');
1003 } else if (event.key === tui.keys.flatten
1004 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1005 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1006 } else if (event.key === tui.keys.take_thing
1007 && game.tasks.includes('PICK_UP')) {
1008 server.send(["TASK:PICK_UP"]);
1009 } else if (event.key === tui.keys.drop_thing
1010 && game.tasks.includes('DROP')) {
1011 server.send(["TASK:DROP"]);
1012 } else if (event.key in tui.movement_keys
1013 && game.tasks.includes('MOVE')) {
1014 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1015 } else if (event.key === tui.keys.teleport) {
1017 } else if (event.key === tui.keys.switch_to_portal) {
1018 event.preventDefault();
1019 tui.switch_mode('portal');
1020 } else if (event.key === tui.keys.switch_to_annotate) {
1021 event.preventDefault();
1022 tui.switch_mode('annotate');
1024 } else if (tui.mode.name == 'study') {
1025 if (event.key === tui.keys.switch_to_chat) {
1026 event.preventDefault();
1027 tui.switch_mode('chat');
1028 } else if (event.key == tui.keys.switch_to_play) {
1029 tui.switch_mode('play');
1030 } else if (event.key in tui.movement_keys) {
1031 explorer.move(tui.movement_keys[event.key]);
1032 } else if (event.key == tui.keys.toggle_map_mode) {
1033 if (tui.map_mode == 'terrain') {
1034 tui.map_mode = 'annotations';
1035 } else if (tui.map_mode == 'annotations') {
1036 tui.map_mode = 'control';
1038 tui.map_mode = 'terrain';
1045 rows_selector.addEventListener('input', function() {
1046 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1049 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1050 terminal.initialize();
1053 cols_selector.addEventListener('input', function() {
1054 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1057 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1058 terminal.initialize();
1059 tui.window_width = terminal.cols / 2,
1062 for (let key_selector of key_selectors) {
1063 key_selector.addEventListener('input', function() {
1064 window.localStorage.setItem(key_selector.id, key_selector.value);
1068 window.setInterval(function() {
1069 if (server.connected) {
1070 server.send(['PING']);
1072 server.reconnect_to(server.url);
1073 tui.log_msg('@ attempting reconnect …')
1076 document.getElementById("terminal").onclick = function() {
1077 tui.inputEl.focus();
1079 document.getElementById("help").onclick = function() {
1080 tui.show_help = true;
1083 document.getElementById("switch_to_play").onclick = function() {
1084 tui.switch_mode('play');
1087 document.getElementById("switch_to_study").onclick = function() {
1088 tui.switch_mode('study');
1091 document.getElementById("switch_to_chat").onclick = function() {
1092 tui.switch_mode('chat');
1095 document.getElementById("switch_to_password").onclick = function() {
1096 tui.switch_mode('password');
1099 document.getElementById("switch_to_edit").onclick = function() {
1100 tui.switch_mode('edit');
1103 document.getElementById("switch_to_annotate").onclick = function() {
1104 tui.switch_mode('annotate');
1107 document.getElementById("switch_to_portal").onclick = function() {
1108 tui.switch_mode('portal');
1111 document.getElementById("switch_to_admin").onclick = function() {
1112 tui.switch_mode('admin');
1115 document.getElementById("switch_to_control_pw").onclick = function() {
1116 tui.switch_mode('control_pw_type');
1119 document.getElementById("toggle_map_mode").onclick = function() {
1120 if (tui.map_mode == 'terrain') {
1121 tui.map_mode = 'annotations';
1122 } else if (tui.map_mode == 'annotations') {
1123 tui.map_mode = 'control';
1125 tui.map_mode = 'terrain';
1129 document.getElementById("take_thing").onclick = function() {
1130 server.send(['TASK:PICK_UP']);
1132 document.getElementById("drop_thing").onclick = function() {
1133 server.send(['TASK:DROP']);
1135 document.getElementById("flatten").onclick = function() {
1136 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1138 document.getElementById("teleport").onclick = function() {
1141 document.getElementById("move_upleft").onclick = function() {
1142 if (tui.mode.name == 'play') {
1143 server.send(['TASK:MOVE', 'UPLEFT']);
1145 explorer.move('UPLEFT');
1148 document.getElementById("move_left").onclick = function() {
1149 if (tui.mode.name == 'play') {
1150 server.send(['TASK:MOVE', 'LEFT']);
1152 explorer.move('LEFT');
1155 document.getElementById("move_downleft").onclick = function() {
1156 if (tui.mode.name == 'play') {
1157 server.send(['TASK:MOVE', 'DOWNLEFT']);
1159 explorer.move('DOWNLEFT');
1162 document.getElementById("move_down").onclick = function() {
1163 if (tui.mode.name == 'play') {
1164 server.send(['TASK:MOVE', 'DOWN']);
1166 explorer.move('DOWN');
1169 document.getElementById("move_up").onclick = function() {
1170 if (tui.mode.name == 'play') {
1171 server.send(['TASK:MOVE', 'UP']);
1173 explorer.move('UP');
1176 document.getElementById("move_upright").onclick = function() {
1177 if (tui.mode.name == 'play') {
1178 server.send(['TASK:MOVE', 'UPRIGHT']);
1180 explorer.move('UPRIGHT');
1183 document.getElementById("move_right").onclick = function() {
1184 if (tui.mode.name == 'play') {
1185 server.send(['TASK:MOVE', 'RIGHT']);
1187 explorer.move('RIGHT');
1190 document.getElementById("move_downright").onclick = function() {
1191 if (tui.mode.name == 'play') {
1192 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1194 explorer.move('DOWNRIGHT');