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_type">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" 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/";
73 'long': 'This mode allows you to interact with the map.'
77 'long': '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.'},
79 'short': 'terrain edit',
80 'long': '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.'
83 'short': 'change tile control password',
84 'long': '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!'
88 'long': '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.'
91 'short': 'annotation',
92 'long': '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.'
95 'short': 'edit portal',
96 'long': '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.'
100 'long': '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:'
104 'long': 'Pick your player name.'
106 'waiting_for_server': {
108 'long': 'Waiting for a server response.'
112 'long': 'Waiting for a server response.'
115 'short': 'password input',
116 'long': '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.'
119 'short': 'become admin',
120 'long': 'This mode allows you to become admin if you know an admin password.'
124 let rows_selector = document.getElementById("n_rows");
125 let cols_selector = document.getElementById("n_cols");
126 let key_selectors = document.querySelectorAll('[id^="key_"]');
128 function restore_selector_value(selector) {
129 let stored_selection = window.localStorage.getItem(selector.id);
130 if (stored_selection) {
131 selector.value = stored_selection;
134 restore_selector_value(rows_selector);
135 restore_selector_value(cols_selector);
136 for (let key_selector of key_selectors) {
137 restore_selector_value(key_selector);
143 initialize: function() {
144 this.rows = rows_selector.value;
145 this.cols = cols_selector.value;
146 this.pre_el = document.getElementById("terminal");
147 this.pre_el.style.color = this.foreground;
148 this.pre_el.style.backgroundColor = this.background;
151 for (let y = 0, x = 0; y <= this.rows; x++) {
152 if (x == this.cols) {
155 this.content.push(line);
157 if (y == this.rows) {
164 blink_screen: function() {
165 this.pre_el.style.color = this.background;
166 this.pre_el.style.backgroundColor = this.foreground;
168 this.pre_el.style.color = this.foreground;
169 this.pre_el.style.backgroundColor = this.background;
172 refresh: function() {
174 for (let y = 0; y < this.rows; y++) {
175 let line = this.content[y].join('');
176 pre_string += line + '\n';
178 this.pre_el.textContent = pre_string;
180 write: function(start_y, start_x, msg) {
181 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
182 this.content[start_y][x] = msg[i];
185 drawBox: function(start_y, start_x, height, width) {
186 let end_y = start_y + height;
187 let end_x = start_x + width;
188 for (let y = start_y, x = start_x; y < this.rows; x++) {
196 this.content[y][x] = ' ';
200 terminal.initialize();
203 tokenize: function(str) {
208 for (let i = 0; i < str.length; i++) {
214 } else if (c == '\\') {
216 } else if (c == '"') {
221 } else if (c == '"') {
223 } else if (c === ' ') {
224 if (token.length > 0) {
232 if (token.length > 0) {
237 parse_yx: function(position_string) {
238 let coordinate_strings = position_string.split(',')
239 let position = [0, 0];
240 position[0] = parseInt(coordinate_strings[0].slice(2));
241 position[1] = parseInt(coordinate_strings[1].slice(2));
253 init: function(url) {
255 this.websocket = new WebSocket(this.url);
256 this.websocket.onopen = function(event) {
257 server.connected = true;
258 game.thing_types = {};
260 server.send(['TASKS']);
261 server.send(['TERRAINS']);
262 server.send(['THING_TYPES']);
263 tui.log_msg("@ server connected! :)");
264 tui.switch_mode('login');
266 this.websocket.onclose = function(event) {
267 server.connected = false;
268 tui.switch_mode('waiting_for_server');
269 tui.log_msg("@ server disconnected :(");
271 this.websocket.onmessage = this.handle_event;
273 reconnect_to: function(url) {
274 this.websocket.close();
277 send: function(tokens) {
278 this.websocket.send(unparser.untokenize(tokens));
280 handle_event: function(event) {
281 let tokens = parser.tokenize(event.data);
282 if (tokens[0] === 'TURN') {
283 game.turn_complete = false;
284 explorer.empty_info_db();
287 game.turn = parseInt(tokens[1]);
288 } else if (tokens[0] === 'THING') {
289 let t = game.get_thing(tokens[3], true);
290 t.position = parser.parse_yx(tokens[1]);
292 } else if (tokens[0] === 'THING_NAME') {
293 let t = game.get_thing(tokens[1], false);
297 } else if (tokens[0] === 'THING_CHAR') {
298 let t = game.get_thing(tokens[1], false);
300 t.player_char = tokens[2];
302 } else if (tokens[0] === 'TASKS') {
303 game.tasks = tokens[1].split(',');
304 tui.mode_edit.legal = game.tasks.includes('WRITE');
305 } else if (tokens[0] === 'THING_TYPE') {
306 game.thing_types[tokens[1]] = tokens[2]
307 } else if (tokens[0] === 'TERRAIN') {
308 game.terrains[tokens[1]] = tokens[2]
309 } else if (tokens[0] === 'MAP') {
310 game.map_geometry = tokens[1];
312 game.map_size = parser.parse_yx(tokens[2]);
314 } else if (tokens[0] === 'FOV') {
316 } else if (tokens[0] === 'MAP_CONTROL') {
317 game.map_control = tokens[1]
318 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
319 game.turn_complete = true;
320 if (tui.mode.name == 'post_login_wait') {
321 tui.switch_mode('play');
322 } else if (tui.mode.name == 'study') {
323 explorer.query_info();
326 } else if (tokens[0] === 'CHAT') {
327 tui.log_msg('# ' + tokens[1], 1);
328 } else if (tokens[0] === 'PLAYER_ID') {
329 game.player_id = parseInt(tokens[1]);
330 } else if (tokens[0] === 'LOGIN_OK') {
331 this.send(['GET_GAMESTATE']);
332 tui.switch_mode('post_login_wait');
333 } else if (tokens[0] === 'PORTAL') {
334 let position = parser.parse_yx(tokens[1]);
335 game.portals[position] = tokens[2];
336 } else if (tokens[0] === 'ANNOTATION_HINT') {
337 let position = parser.parse_yx(tokens[1]);
338 explorer.info_hints = explorer.info_hints.concat([position]);
339 } else if (tokens[0] === 'ANNOTATION') {
340 let position = parser.parse_yx(tokens[1]);
341 explorer.update_info_db(position, tokens[2]);
342 tui.restore_input_values();
344 } else if (tokens[0] === 'UNHANDLED_INPUT') {
345 tui.log_msg('? unknown command');
346 } else if (tokens[0] === 'PLAY_ERROR') {
347 tui.log_msg('? ' + tokens[1]);
348 terminal.blink_screen();
349 } else if (tokens[0] === 'ARGUMENT_ERROR') {
350 tui.log_msg('? syntax error: ' + tokens[1]);
351 } else if (tokens[0] === 'GAME_ERROR') {
352 tui.log_msg('? game error: ' + tokens[1]);
353 } else if (tokens[0] === 'PONG') {
356 tui.log_msg('? unhandled input: ' + event.data);
362 quote: function(str) {
364 for (let i = 0; i < str.length; i++) {
366 if (['"', '\\'].includes(c)) {
372 return quoted.join('');
374 to_yx: function(yx_coordinate) {
375 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
377 untokenize: function(tokens) {
378 let quoted_tokens = [];
379 for (let token of tokens) {
380 quoted_tokens.push(this.quote(token));
382 return quoted_tokens.join(" ");
387 constructor(name, has_input_prompt=false, shows_info=false,
388 is_intro=false, is_single_char_entry=false) {
390 this.short_desc = mode_helps[name].short;
391 this.available_modes = [];
392 this.has_input_prompt = has_input_prompt;
393 this.shows_info= shows_info;
394 this.is_intro = is_intro;
395 this.help_intro = mode_helps[name].long;
396 this.is_single_char_entry = is_single_char_entry;
399 *iter_available_modes() {
400 for (let mode_name of this.available_modes) {
401 let mode = tui['mode_' + mode_name];
405 let key = tui.keys['switch_to_' + mode.name];
409 list_available_modes() {
411 if (this.available_modes.length > 0) {
412 msg += 'Other modes available from here:\n';
413 for (let [mode, key] of this.iter_available_modes()) {
414 msg += '[' + key + '] – ' + mode.short_desc + '\n';
419 mode_switch_on_key(key_event) {
420 for (let [mode, key] of this.iter_available_modes()) {
421 if (key_event.key == key) {
422 event.preventDefault();
423 tui.switch_mode(mode.name);
434 window_width: terminal.cols / 2,
440 mode_waiting_for_server: new Mode('waiting_for_server',
442 mode_login: new Mode('login', true, false, true),
443 mode_post_login_wait: new Mode('post_login_wait'),
444 mode_chat: new Mode('chat', true),
445 mode_annotate: new Mode('annotate', true, true),
446 mode_play: new Mode('play'),
447 mode_study: new Mode('study', false, true),
448 mode_edit: new Mode('edit', false, false, false, true),
449 mode_control_pw_type: new Mode('control_pw_type',
450 false, false, false, true),
451 mode_portal: new Mode('portal', true, true),
452 mode_password: new Mode('password', true),
453 mode_admin: new Mode('admin', true),
454 mode_control_pw_pw: new Mode('control_pw_pw', true),
456 this.mode_play.available_modes = ["chat", "study", "edit",
457 "annotate", "portal",
460 this.mode_study.available_modes = ["chat", "play"]
461 this.mode = this.mode_waiting_for_server;
462 this.inputEl = document.getElementById("input");
463 this.inputEl.focus();
464 this.recalc_input_lines();
465 this.height_header = this.height_turn_line + this.height_mode_line;
466 this.log_msg("@ waiting for server connection ...");
469 init_keys: function() {
471 for (let key_selector of key_selectors) {
472 this.keys[key_selector.id.slice(4)] = key_selector.value;
474 this.movement_keys = {
475 [this.keys.square_move_up]: 'UP',
476 [this.keys.square_move_left]: 'LEFT',
477 [this.keys.square_move_down]: 'DOWN',
478 [this.keys.square_move_right]: 'RIGHT'
480 if (game.map_geometry == 'Hex') {
481 this.movement_keys = {
482 [this.keys.hex_move_upleft]: 'UPLEFT',
483 [this.keys.hex_move_upright]: 'UPRIGHT',
484 [this.keys.hex_move_right]: 'RIGHT',
485 [this.keys.hex_move_downright]: 'DOWNRIGHT',
486 [this.keys.hex_move_downleft]: 'DOWNLEFT',
487 [this.keys.hex_move_left]: 'LEFT'
491 switch_mode: function(mode_name) {
492 this.inputEl.focus();
493 this.map_mode = 'terrain';
494 this.mode = this['mode_' + mode_name];
495 if (this.mode.shows_info && game.player_id in game.things) {
496 explorer.position = game.things[game.player_id].position;
497 explorer.query_info();
500 this.restore_input_values();
501 document.getElementById("take_thing").disabled = true;
502 document.getElementById("drop_thing").disabled = true;
503 document.getElementById("flatten").disabled = true;
504 document.getElementById("teleport").disabled = true;
505 document.getElementById("toggle_map_mode").disabled = true;
506 document.getElementById("switch_to_chat").disabled = true;
507 document.getElementById("switch_to_play").disabled = true;
508 document.getElementById("switch_to_study").disabled = true;
509 document.getElementById("switch_to_edit").disabled = true;
510 document.getElementById("switch_to_portal").disabled = true;
511 document.getElementById("switch_to_annotate").disabled = true;
512 document.getElementById("switch_to_password").disabled = true;
513 document.getElementById("switch_to_admin").disabled = true;
514 document.getElementById("switch_to_control_pw_type").disabled = true;
515 document.getElementById("move_left").disabled = true;
516 document.getElementById("move_upleft").disabled = true;
517 document.getElementById("move_up").disabled = true;
518 document.getElementById("move_upright").disabled = true;
519 document.getElementById("move_downleft").disabled = true;
520 document.getElementById("move_down").disabled = true;
521 document.getElementById("move_downright").disabled = true;
522 document.getElementById("move_right").disabled = true;
523 if (this.mode.name == 'play' || this.mode.name == 'study') {
524 document.getElementById("move_left").disabled = false;
525 document.getElementById("move_right").disabled = false;
526 if (game.map_geometry == 'Hex') {
527 document.getElementById("move_upleft").disabled = false;
528 document.getElementById("move_upright").disabled = false;
529 document.getElementById("move_downleft").disabled = false;
530 document.getElementById("move_downright").disabled = false;
532 document.getElementById("move_up").disabled = false;
533 document.getElementById("move_down").disabled = false;
536 if (!this.mode.is_intro && this.mode.name != 'play') {
537 document.getElementById("switch_to_play").disabled = false;
539 if (!this.mode.is_intro && this.mode.name != 'study') {
540 document.getElementById("switch_to_study").disabled = false;
542 if (!this.mode.is_intro && this.mode.name != 'chat') {
543 document.getElementById("switch_to_chat").disabled = false;
545 if (this.mode.name == 'login') {
546 if (this.login_name) {
547 server.send(['LOGIN', this.login_name]);
549 this.log_msg("? need login name");
551 } else if (this.mode.name == 'play') {
552 if (game.tasks.includes('PICK_UP')) {
553 document.getElementById("take_thing").disabled = false;
555 if (game.tasks.includes('DROP')) {
556 document.getElementById("drop_thing").disabled = false;
558 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
559 document.getElementById("flatten").disabled = false;
561 if (game.tasks.includes('MOVE')) {
563 document.getElementById("teleport").disabled = false;
564 document.getElementById("switch_to_annotate").disabled = false;
565 document.getElementById("switch_to_edit").disabled = false;
566 document.getElementById("switch_to_portal").disabled = false;
567 document.getElementById("switch_to_password").disabled = false;
568 document.getElementById("switch_to_admin").disabled = false;
569 document.getElementById("switch_to_control_pw_type").disabled = false;
570 } else if (this.mode.name == 'study') {
571 document.getElementById("toggle_map_mode").disabled = false;
572 } else if (this.mode.is_single_char_entry) {
573 this.show_help = true;
574 } else if (this.mode.name == 'admin') {
575 this.log_msg('@ enter admin password:')
576 } else if (this.mode.name == 'control_pw_pw') {
577 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
581 restore_input_values: function() {
582 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
583 let info = explorer.info_db[explorer.position];
584 if (info != "(none)") {
585 this.inputEl.value = info;
586 this.recalc_input_lines();
588 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
589 let portal = game.portals[explorer.position]
590 this.inputEl.value = portal;
591 this.recalc_input_lines();
592 } else if (this.mode.name == 'password') {
593 this.inputEl.value = this.password;
594 this.recalc_input_lines();
597 empty_input: function(str) {
598 this.inputEl.value = "";
599 if (this.mode.has_input_prompt) {
600 this.recalc_input_lines();
602 this.height_input = 0;
605 recalc_input_lines: function() {
606 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
607 this.height_input = this.input_lines.length;
609 msg_into_lines_of_width: function(msg, width) {
612 for (let i = 0, x = 0; i < msg.length; i++, x++) {
613 if (x >= width || msg[i] == "\n") {
618 if (msg[i] != "\n") {
625 log_msg: function(msg) {
627 while (this.log.length > 100) {
632 draw_map: function() {
633 let map_lines_split = [];
635 let map_content = game.map;
636 if (this.map_mode == 'control') {
637 map_content = game.map_control;
639 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
640 if (j == game.map_size[1]) {
641 map_lines_split.push(line);
645 line.push(map_content[i] + ' ');
647 map_lines_split.push(line);
648 if (this.map_mode == 'annotations') {
649 for (const coordinate of explorer.info_hints) {
650 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
652 } else if (this.map_mode == 'terrain') {
653 for (const p in game.portals) {
654 let coordinate = p.split(',')
655 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
657 let used_positions = [];
658 for (const thing_id in game.things) {
659 let t = game.things[thing_id];
660 let symbol = game.thing_types[t.type_];
663 meta_char = t.player_char;
665 if (used_positions.includes(t.position.toString())) {
668 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
669 used_positions.push(t.position.toString());
672 if (tui.mode.shows_info) {
673 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
676 if (game.map_geometry == 'Square') {
677 for (let line_split of map_lines_split) {
678 map_lines.push(line_split.join(''));
680 } else if (game.map_geometry == 'Hex') {
682 for (let line_split of map_lines_split) {
683 map_lines.push(' '.repeat(indent) + line_split.join(''));
691 let window_center = [terminal.rows / 2, this.window_width / 2];
692 let player = game.things[game.player_id];
693 let center_position = [player.position[0], player.position[1]];
694 if (tui.mode.shows_info) {
695 center_position = [explorer.position[0], explorer.position[1]];
697 center_position[1] = center_position[1] * 2;
698 let offset = [center_position[0] - window_center[0],
699 center_position[1] - window_center[1]]
700 if (game.map_geometry == 'Hex' && offset[0] % 2) {
703 let term_y = Math.max(0, -offset[0]);
704 let term_x = Math.max(0, -offset[1]);
705 let map_y = Math.max(0, offset[0]);
706 let map_x = Math.max(0, offset[1]);
707 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
708 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
709 terminal.write(term_y, term_x, to_draw);
712 draw_mode_line: function() {
713 let help = 'hit [' + this.keys.help + '] for help';
714 if (this.mode.has_input_prompt) {
715 help = 'enter /help for help';
717 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
719 draw_turn_line: function(n) {
720 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
722 draw_history: function() {
723 let log_display_lines = [];
724 for (let line of this.log) {
725 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
727 for (let y = terminal.rows - 1 - this.height_input,
728 i = log_display_lines.length - 1;
729 y >= this.height_header && i >= 0;
731 terminal.write(y, this.window_width, log_display_lines[i]);
734 draw_info: function() {
735 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
736 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
737 terminal.write(y, this.window_width, lines[i]);
740 draw_input: function() {
741 if (this.mode.has_input_prompt) {
742 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
743 terminal.write(y, this.window_width, this.input_lines[i]);
747 draw_help: function() {
748 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
749 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
750 if (this.mode.name == 'play') {
751 content += "Available actions:\n";
752 if (game.tasks.includes('MOVE')) {
753 content += "[" + movement_keys_desc + "] – move player\n";
755 if (game.tasks.includes('PICK_UP')) {
756 content += "[" + this.keys.take_thing + "] – take thing under player\n";
758 if (game.tasks.includes('DROP')) {
759 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
761 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
762 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
764 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
766 } else if (this.mode.name == 'study') {
767 content += "Available actions:\n";
768 content += '[' + movement_keys_desc + '] – move question mark\n';
769 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
771 } else if (this.mode.name == 'chat') {
772 content += '/nick NAME – re-name yourself to NAME\n';
773 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
774 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
776 content += this.mode.list_available_modes();
778 if (!this.mode.has_input_prompt) {
779 start_x = this.window_width
781 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
782 let lines = this.msg_into_lines_of_width(content, this.window_width);
783 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
784 terminal.write(y, start_x, lines[i]);
787 full_refresh: function() {
788 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
789 if (this.mode.is_intro) {
793 if (game.turn_complete) {
795 this.draw_turn_line();
797 this.draw_mode_line();
798 if (this.mode.shows_info) {
805 if (this.show_help) {
817 this.map_control = "";
818 this.map_size = [0,0];
823 get_thing: function(id_, create_if_not_found=false) {
824 if (id_ in game.things) {
825 return game.things[id_];
826 } else if (create_if_not_found) {
827 let t = new Thing([0,0]);
828 game.things[id_] = t;
832 move: function(start_position, direction) {
833 let target = [start_position[0], start_position[1]];
834 if (direction == 'LEFT') {
836 } else if (direction == 'RIGHT') {
838 } else if (game.map_geometry == 'Square') {
839 if (direction == 'UP') {
841 } else if (direction == 'DOWN') {
844 } else if (game.map_geometry == 'Hex') {
845 let start_indented = start_position[0] % 2;
846 if (direction == 'UPLEFT') {
848 if (!start_indented) {
851 } else if (direction == 'UPRIGHT') {
853 if (start_indented) {
856 } else if (direction == 'DOWNLEFT') {
858 if (!start_indented) {
861 } else if (direction == 'DOWNRIGHT') {
863 if (start_indented) {
868 if (target[0] < 0 || target[1] < 0 ||
869 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
874 teleport: function() {
875 let player = this.get_thing(game.player_id);
876 if (player.position in this.portals) {
877 server.reconnect_to(this.portals[player.position]);
879 terminal.blink_screen();
880 tui.log_msg('? not standing on portal')
888 server.init(websocket_location);
894 move: function(direction) {
895 let target = game.move(this.position, direction);
897 this.position = target
900 terminal.blink_screen();
903 update_info_db: function(yx, str) {
904 this.info_db[yx] = str;
905 if (tui.mode.name == 'study') {
909 empty_info_db: function() {
911 this.info_hints = [];
912 if (tui.mode.name == 'study') {
916 query_info: function() {
917 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
919 get_info: function() {
920 let position_i = this.position[0] * game.map_size[1] + this.position[1];
921 if (game.fov[position_i] != '.') {
922 return 'outside field of view';
925 let terrain_char = game.map[position_i]
926 let terrain_desc = '?'
927 if (game.terrains[terrain_char]) {
928 terrain_desc = game.terrains[terrain_char];
930 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
931 let protection = game.map_control[position_i];
932 if (protection == '.') {
933 protection = 'unprotected';
935 info += 'PROTECTION: ' + protection + '\n';
936 for (let t_id in game.things) {
937 let t = game.things[t_id];
938 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
939 let symbol = game.thing_types[t.type_];
940 info += "THING: " + t.type_ + " / " + symbol;
942 info += t.player_char;
945 info += " (" + t.name_ + ")";
950 if (this.position in game.portals) {
951 info += "PORTAL: " + game.portals[this.position] + "\n";
953 if (this.position in this.info_db) {
954 info += "ANNOTATIONS: " + this.info_db[this.position];
960 annotate: function(msg) {
961 if (msg.length == 0) {
962 msg = " "; // triggers annotation deletion
964 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
966 set_portal: function(msg) {
967 if (msg.length == 0) {
968 msg = " "; // triggers portal deletion
970 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
974 tui.inputEl.addEventListener('input', (event) => {
975 if (tui.mode.has_input_prompt) {
976 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
977 if (tui.inputEl.value.length > max_length) {
978 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
980 tui.recalc_input_lines();
981 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
982 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
983 tui.switch_mode('play');
984 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
985 tui.tile_control_char = tui.inputEl.value[0];
986 tui.switch_mode('control_pw_pw');
990 tui.inputEl.addEventListener('keydown', (event) => {
991 tui.show_help = false;
992 if (event.key == 'Enter') {
993 event.preventDefault();
995 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
996 tui.show_help = true;
998 tui.restore_input_values();
999 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1000 && !tui.mode.is_single_char_entry) {
1001 tui.show_help = true;
1002 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1003 tui.login_name = tui.inputEl.value;
1004 server.send(['LOGIN', tui.inputEl.value]);
1006 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1007 if (tui.inputEl.value.length == 0) {
1008 tui.log_msg('@ aborted');
1010 server.send(['SET_MAP_CONTROL_PASSWORD',
1011 tui.tile_control_char, tui.inputEl.value]);
1013 tui.switch_mode('play');
1014 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1015 explorer.set_portal(tui.inputEl.value);
1016 tui.switch_mode('play');
1017 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1018 explorer.annotate(tui.inputEl.value);
1019 tui.switch_mode('play');
1020 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1021 if (tui.inputEl.value.length == 0) {
1022 tui.inputEl.value = " ";
1024 tui.password = tui.inputEl.value
1025 tui.switch_mode('play');
1026 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1027 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1028 tui.switch_mode('play');
1029 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1030 let tokens = parser.tokenize(tui.inputEl.value);
1031 if (tokens.length > 0 && tokens[0].length > 0) {
1032 if (tui.inputEl.value[0][0] == '/') {
1033 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1034 tui.switch_mode('play');
1035 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1036 tui.switch_mode('study');
1037 } else if (tokens[0].slice(1) == 'nick') {
1038 if (tokens.length > 1) {
1039 server.send(['NICK', tokens[1]]);
1041 tui.log_msg('? need new name');
1044 tui.log_msg('? unknown command');
1047 server.send(['ALL', tui.inputEl.value]);
1049 } else if (tui.inputEl.valuelength > 0) {
1050 server.send(['ALL', tui.inputEl.value]);
1053 } else if (tui.mode.name == 'play') {
1054 if (tui.mode.mode_switch_on_key(event)) {
1056 } else if (event.key === tui.keys.flatten
1057 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1058 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1059 } else if (event.key === tui.keys.take_thing
1060 && game.tasks.includes('PICK_UP')) {
1061 server.send(["TASK:PICK_UP"]);
1062 } else if (event.key === tui.keys.drop_thing
1063 && game.tasks.includes('DROP')) {
1064 server.send(["TASK:DROP"]);
1065 } else if (event.key in tui.movement_keys
1066 && game.tasks.includes('MOVE')) {
1067 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1068 } else if (event.key === tui.keys.teleport) {
1070 } else if (event.key === tui.keys.switch_to_portal) {
1071 event.preventDefault();
1072 tui.switch_mode('portal');
1073 } else if (event.key === tui.keys.switch_to_annotate) {
1074 event.preventDefault();
1075 tui.switch_mode('annotate');
1077 } else if (tui.mode.name == 'study') {
1078 if (tui.mode.mode_switch_on_key(event)) {
1080 } else if (event.key == tui.keys.switch_to_play) {
1081 tui.switch_mode('play');
1082 } else if (event.key in tui.movement_keys) {
1083 explorer.move(tui.movement_keys[event.key]);
1084 } else if (event.key == tui.keys.toggle_map_mode) {
1085 if (tui.map_mode == 'terrain') {
1086 tui.map_mode = 'annotations';
1087 } else if (tui.map_mode == 'annotations') {
1088 tui.map_mode = 'control';
1090 tui.map_mode = 'terrain';
1097 rows_selector.addEventListener('input', function() {
1098 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1101 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1102 terminal.initialize();
1105 cols_selector.addEventListener('input', function() {
1106 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1109 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1110 terminal.initialize();
1111 tui.window_width = terminal.cols / 2,
1114 for (let key_selector of key_selectors) {
1115 key_selector.addEventListener('input', function() {
1116 window.localStorage.setItem(key_selector.id, key_selector.value);
1120 window.setInterval(function() {
1121 if (server.connected) {
1122 server.send(['PING']);
1124 server.reconnect_to(server.url);
1125 tui.log_msg('@ attempting reconnect …')
1128 document.getElementById("terminal").onclick = function() {
1129 tui.inputEl.focus();
1131 document.getElementById("help").onclick = function() {
1132 tui.show_help = true;
1135 document.getElementById("switch_to_play").onclick = function() {
1136 tui.switch_mode('play');
1139 document.getElementById("switch_to_study").onclick = function() {
1140 tui.switch_mode('study');
1143 document.getElementById("switch_to_chat").onclick = function() {
1144 tui.switch_mode('chat');
1147 document.getElementById("switch_to_password").onclick = function() {
1148 tui.switch_mode('password');
1151 document.getElementById("switch_to_edit").onclick = function() {
1152 tui.switch_mode('edit');
1155 document.getElementById("switch_to_annotate").onclick = function() {
1156 tui.switch_mode('annotate');
1159 document.getElementById("switch_to_portal").onclick = function() {
1160 tui.switch_mode('portal');
1163 document.getElementById("switch_to_admin").onclick = function() {
1164 tui.switch_mode('admin');
1167 document.getElementById("switch_to_control_pw_type").onclick = function() {
1168 tui.switch_mode('control_pw_type');
1171 document.getElementById("toggle_map_mode").onclick = function() {
1172 if (tui.map_mode == 'terrain') {
1173 tui.map_mode = 'annotations';
1174 } else if (tui.map_mode == 'annotations') {
1175 tui.map_mode = 'control';
1177 tui.map_mode = 'terrain';
1181 document.getElementById("take_thing").onclick = function() {
1182 server.send(['TASK:PICK_UP']);
1184 document.getElementById("drop_thing").onclick = function() {
1185 server.send(['TASK:DROP']);
1187 document.getElementById("flatten").onclick = function() {
1188 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1190 document.getElementById("teleport").onclick = function() {
1193 document.getElementById("move_upleft").onclick = function() {
1194 if (tui.mode.name == 'play') {
1195 server.send(['TASK:MOVE', 'UPLEFT']);
1197 explorer.move('UPLEFT');
1200 document.getElementById("move_left").onclick = function() {
1201 if (tui.mode.name == 'play') {
1202 server.send(['TASK:MOVE', 'LEFT']);
1204 explorer.move('LEFT');
1207 document.getElementById("move_downleft").onclick = function() {
1208 if (tui.mode.name == 'play') {
1209 server.send(['TASK:MOVE', 'DOWNLEFT']);
1211 explorer.move('DOWNLEFT');
1214 document.getElementById("move_down").onclick = function() {
1215 if (tui.mode.name == 'play') {
1216 server.send(['TASK:MOVE', 'DOWN']);
1218 explorer.move('DOWN');
1221 document.getElementById("move_up").onclick = function() {
1222 if (tui.mode.name == 'play') {
1223 server.send(['TASK:MOVE', 'UP']);
1225 explorer.move('UP');
1228 document.getElementById("move_upright").onclick = function() {
1229 if (tui.mode.name == 'play') {
1230 server.send(['TASK:MOVE', 'UPRIGHT']);
1232 explorer.move('UPRIGHT');
1235 document.getElementById("move_right").onclick = function() {
1236 if (tui.mode.name == 'play') {
1237 server.send(['TASK:MOVE', 'RIGHT']);
1239 explorer.move('RIGHT');
1242 document.getElementById("move_downright").onclick = function() {
1243 if (tui.mode.name == 'play') {
1244 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1246 explorer.move('DOWNRIGHT');