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">
16 <td><button id="move_upleft">up-left</button></td>
17 <td><button id="move_up">up</button></td>
18 <td><button id="move_upright">up-right</button></td>
21 <td><button id="move_left">left</button></td>
23 <td><button id="move_right">right</button></td>
26 <td><button id="move_downleft">down-left</button></td>
27 <td><button id="move_down">down</button></td>
28 <td><button id="move_downright">down-right</button></td>
33 <td><button id="help">help</button></td>
36 <td><button id="switch_to_chat">chat mode</button><br /></td>
38 <td><button id="switch_to_study">study mode</button></td>
39 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
41 <td><button id="switch_to_play">play mode</button></td>
45 <td><button id="take_thing">take thing</button></td>
46 <td><button id="switch_to_edit">change tile</button></td>
47 <td><button id="switch_to_admin">become admin</button></td>
50 <td><button id="drop_thing">drop thing</button></td>
51 <td><button id="switch_to_password">change tile editing password</button></td>
52 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
55 <td><button id="flatten">flatten surroundings</button></td>
56 <td><button id="switch_to_annotate">annotate tile</button></td>
57 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
60 <td><button id="teleport">teleport</button></td>
61 <td><button id="switch_to_portal">edit portal link</button></td>
67 <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 />
69 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
70 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
71 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
72 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
73 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
74 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
75 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
76 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
77 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
78 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
79 <li>help: <input id="key_help" type="text" value="h" />
80 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
81 <li>teleport: <input id="key_teleport" type="text" value="p" />
82 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
83 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
84 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
85 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
86 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
87 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
88 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
89 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
90 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw_type" type="text" value="C" />
91 <li>change tiles control (from play mode): <input id="key_switch_to_control_tile_type" type="text" value="Q" />
92 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
93 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
94 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
99 let websocket_location = "wss://plomlompom.com/rogue_chat/";
100 //let websocket_location = "ws://localhost:8000/";
105 'long': 'This mode allows you to interact with the map.'
109 '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.'},
111 'short': 'terrain edit',
112 '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.'
115 'short': 'change tile control password',
116 '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!'
119 'short': 'change tile control password',
120 '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.'
122 'control_tile_type': {
123 'short': 'change tiles control',
124 'long': 'This mode is the first of two steps to change tile control areas on the map. First enter the tile control character you want to write.'
126 'control_tile_draw': {
127 'short': 'change tiles control',
128 'long': 'This mode is the second of two steps to change tile control areas on the map. Move cursor around the map to draw selected tile control character'
131 'short': 'annotation',
132 '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.'
135 'short': 'edit portal',
136 '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.'
139 'short': 'chat mode',
140 '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:'
144 'long': 'Pick your player name.'
146 'waiting_for_server': {
147 'short': 'waiting for server response',
148 'long': 'Waiting for a server response.'
151 'short': 'waiting for server response',
152 'long': 'Waiting for a server response.'
155 'short': 'map edit password',
156 '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.'
159 'short': 'become admin',
160 'long': 'This mode allows you to become admin if you know an admin password.'
164 let rows_selector = document.getElementById("n_rows");
165 let cols_selector = document.getElementById("n_cols");
166 let key_selectors = document.querySelectorAll('[id^="key_"]');
168 function restore_selector_value(selector) {
169 let stored_selection = window.localStorage.getItem(selector.id);
170 if (stored_selection) {
171 selector.value = stored_selection;
174 restore_selector_value(rows_selector);
175 restore_selector_value(cols_selector);
176 for (let key_selector of key_selectors) {
177 restore_selector_value(key_selector);
183 initialize: function() {
184 this.rows = rows_selector.value;
185 this.cols = cols_selector.value;
186 this.pre_el = document.getElementById("terminal");
187 this.pre_el.style.color = this.foreground;
188 this.pre_el.style.backgroundColor = this.background;
191 for (let y = 0, x = 0; y <= this.rows; x++) {
192 if (x == this.cols) {
195 this.content.push(line);
197 if (y == this.rows) {
204 blink_screen: function() {
205 this.pre_el.style.color = this.background;
206 this.pre_el.style.backgroundColor = this.foreground;
208 this.pre_el.style.color = this.foreground;
209 this.pre_el.style.backgroundColor = this.background;
212 refresh: function() {
213 function escapeHTML(str) {
215 replace(/&/g, '&').
216 replace(/</g, '<').
217 replace(/>/g, '>').
218 replace(/'/g, ''').
219 replace(/"/g, '"');
221 let pre_content = '';
222 for (let y = 0; y < this.rows; y++) {
223 let line = this.content[y].join('');
225 if (y in tui.links) {
227 for (let span of tui.links[y]) {
228 chunks.push(escapeHTML(line.slice(start_x, span[0])));
229 chunks.push('<a href="');
230 chunks.push(escapeHTML(span[2]));
232 chunks.push(escapeHTML(line.slice(span[0], span[1])));
236 chunks.push(escapeHTML(line.slice(start_x)));
238 chunks = [escapeHTML(line)];
240 for (const chunk of chunks) {
241 pre_content += chunk;
245 this.pre_el.innerHTML = pre_content;
247 write: function(start_y, start_x, msg) {
248 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
249 this.content[start_y][x] = msg[i];
252 drawBox: function(start_y, start_x, height, width) {
253 let end_y = start_y + height;
254 let end_x = start_x + width;
255 for (let y = start_y, x = start_x; y < this.rows; x++) {
263 this.content[y][x] = ' ';
267 terminal.initialize();
270 tokenize: function(str) {
275 for (let i = 0; i < str.length; i++) {
281 } else if (c == '\\') {
283 } else if (c == '"') {
288 } else if (c == '"') {
290 } else if (c === ' ') {
291 if (token.length > 0) {
299 if (token.length > 0) {
304 parse_yx: function(position_string) {
305 let coordinate_strings = position_string.split(',')
306 let position = [0, 0];
307 position[0] = parseInt(coordinate_strings[0].slice(2));
308 position[1] = parseInt(coordinate_strings[1].slice(2));
320 init: function(url) {
322 this.websocket = new WebSocket(this.url);
323 this.websocket.onopen = function(event) {
324 server.connected = true;
325 game.thing_types = {};
327 server.send(['TASKS']);
328 server.send(['TERRAINS']);
329 server.send(['THING_TYPES']);
330 tui.log_msg("@ server connected! :)");
331 tui.switch_mode('login');
333 this.websocket.onclose = function(event) {
334 server.connected = false;
335 tui.switch_mode('waiting_for_server');
336 tui.log_msg("@ server disconnected :(");
338 this.websocket.onmessage = this.handle_event;
340 reconnect_to: function(url) {
341 this.websocket.close();
344 send: function(tokens) {
345 this.websocket.send(unparser.untokenize(tokens));
347 handle_event: function(event) {
348 let tokens = parser.tokenize(event.data);
349 if (tokens[0] === 'TURN') {
350 game.turn_complete = false;
351 explorer.empty_info_db();
354 game.turn = parseInt(tokens[1]);
355 } else if (tokens[0] === 'THING') {
356 let t = game.get_thing(tokens[3], true);
357 t.position = parser.parse_yx(tokens[1]);
359 } else if (tokens[0] === 'THING_NAME') {
360 let t = game.get_thing(tokens[1], false);
364 } else if (tokens[0] === 'THING_CHAR') {
365 let t = game.get_thing(tokens[1], false);
367 t.player_char = tokens[2];
369 } else if (tokens[0] === 'TASKS') {
370 game.tasks = tokens[1].split(',');
371 tui.mode_edit.legal = game.tasks.includes('WRITE');
372 } else if (tokens[0] === 'THING_TYPE') {
373 game.thing_types[tokens[1]] = tokens[2]
374 } else if (tokens[0] === 'TERRAIN') {
375 game.terrains[tokens[1]] = tokens[2]
376 } else if (tokens[0] === 'MAP') {
377 game.map_geometry = tokens[1];
379 game.map_size = parser.parse_yx(tokens[2]);
381 } else if (tokens[0] === 'FOV') {
383 } else if (tokens[0] === 'MAP_CONTROL') {
384 game.map_control = tokens[1]
385 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
386 game.turn_complete = true;
387 if (tui.mode.name == 'post_login_wait') {
388 tui.switch_mode('play');
389 } else if (tui.mode.name == 'study') {
390 explorer.query_info();
393 } else if (tokens[0] === 'CHAT') {
394 tui.log_msg('# ' + tokens[1], 1);
395 } else if (tokens[0] === 'PLAYER_ID') {
396 game.player_id = parseInt(tokens[1]);
397 } else if (tokens[0] === 'LOGIN_OK') {
398 this.send(['GET_GAMESTATE']);
399 tui.switch_mode('post_login_wait');
400 } else if (tokens[0] === 'PORTAL') {
401 let position = parser.parse_yx(tokens[1]);
402 game.portals[position] = tokens[2];
403 } else if (tokens[0] === 'ANNOTATION_HINT') {
404 let position = parser.parse_yx(tokens[1]);
405 explorer.info_hints = explorer.info_hints.concat([position]);
406 } else if (tokens[0] === 'ANNOTATION') {
407 let position = parser.parse_yx(tokens[1]);
408 explorer.update_info_db(position, tokens[2]);
409 tui.restore_input_values();
411 } else if (tokens[0] === 'UNHANDLED_INPUT') {
412 tui.log_msg('? unknown command');
413 } else if (tokens[0] === 'PLAY_ERROR') {
414 tui.log_msg('? ' + tokens[1]);
415 terminal.blink_screen();
416 } else if (tokens[0] === 'ARGUMENT_ERROR') {
417 tui.log_msg('? syntax error: ' + tokens[1]);
418 } else if (tokens[0] === 'GAME_ERROR') {
419 tui.log_msg('? game error: ' + tokens[1]);
420 } else if (tokens[0] === 'PONG') {
423 tui.log_msg('? unhandled input: ' + event.data);
429 quote: function(str) {
431 for (let i = 0; i < str.length; i++) {
433 if (['"', '\\'].includes(c)) {
439 return quoted.join('');
441 to_yx: function(yx_coordinate) {
442 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
444 untokenize: function(tokens) {
445 let quoted_tokens = [];
446 for (let token of tokens) {
447 quoted_tokens.push(this.quote(token));
449 return quoted_tokens.join(" ");
454 constructor(name, has_input_prompt=false, shows_info=false,
455 is_intro=false, is_single_char_entry=false) {
457 this.short_desc = mode_helps[name].short;
458 this.available_modes = [];
459 this.has_input_prompt = has_input_prompt;
460 this.shows_info= shows_info;
461 this.is_intro = is_intro;
462 this.help_intro = mode_helps[name].long;
463 this.is_single_char_entry = is_single_char_entry;
466 *iter_available_modes() {
467 for (let mode_name of this.available_modes) {
468 let mode = tui['mode_' + mode_name];
472 let key = tui.keys['switch_to_' + mode.name];
476 list_available_modes() {
478 if (this.available_modes.length > 0) {
479 msg += 'Other modes available from here:\n';
480 for (let [mode, key] of this.iter_available_modes()) {
481 msg += '[' + key + '] – ' + mode.short_desc + '\n';
486 mode_switch_on_key(key_event) {
487 for (let [mode, key] of this.iter_available_modes()) {
488 if (key_event.key == key) {
489 event.preventDefault();
490 tui.switch_mode(mode.name);
502 window_width: terminal.cols / 2,
508 mode_waiting_for_server: new Mode('waiting_for_server',
510 mode_login: new Mode('login', true, false, true),
511 mode_post_login_wait: new Mode('post_login_wait'),
512 mode_chat: new Mode('chat', true),
513 mode_annotate: new Mode('annotate', true, true),
514 mode_play: new Mode('play'),
515 mode_study: new Mode('study', false, true),
516 mode_edit: new Mode('edit', false, false, false, true),
517 mode_control_pw_type: new Mode('control_pw_type',
518 false, false, false, true),
519 mode_portal: new Mode('portal', true, true),
520 mode_password: new Mode('password', true),
521 mode_admin: new Mode('admin', true),
522 mode_control_pw_pw: new Mode('control_pw_pw', true),
523 mode_control_tile_type: new Mode('control_tile_type',
524 false, false, false, true),
525 mode_control_tile_draw: new Mode('control_tile_draw'),
527 this.mode_play.available_modes = ["chat", "study", "edit",
528 "annotate", "portal",
532 this.mode_study.available_modes = ["chat", "play"]
533 this.mode_control_tile_draw.available_modes = ["play"]
534 this.mode = this.mode_waiting_for_server;
535 this.inputEl = document.getElementById("input");
536 this.inputEl.focus();
537 this.recalc_input_lines();
538 this.height_header = this.height_turn_line + this.height_mode_line;
539 this.log_msg("@ waiting for server connection ...");
542 init_keys: function() {
544 for (let key_selector of key_selectors) {
545 this.keys[key_selector.id.slice(4)] = key_selector.value;
547 this.movement_keys = {
548 [this.keys.square_move_up]: 'UP',
549 [this.keys.square_move_left]: 'LEFT',
550 [this.keys.square_move_down]: 'DOWN',
551 [this.keys.square_move_right]: 'RIGHT'
553 if (game.map_geometry == 'Hex') {
554 this.movement_keys = {
555 [this.keys.hex_move_upleft]: 'UPLEFT',
556 [this.keys.hex_move_upright]: 'UPRIGHT',
557 [this.keys.hex_move_right]: 'RIGHT',
558 [this.keys.hex_move_downright]: 'DOWNRIGHT',
559 [this.keys.hex_move_downleft]: 'DOWNLEFT',
560 [this.keys.hex_move_left]: 'LEFT'
564 switch_mode: function(mode_name) {
565 this.inputEl.focus();
566 this.map_mode = 'terrain';
567 this.mode = this['mode_' + mode_name];
568 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
569 explorer.position = game.things[game.player_id].position;
570 if (this.mode.shows_info) {
571 explorer.query_info();
572 } else if (this.mode.name == 'control_tile_draw') {
573 explorer.send_tile_control_command();
574 this.map_mode = 'control';
578 this.restore_input_values();
579 document.getElementById("take_thing").disabled = true;
580 document.getElementById("drop_thing").disabled = true;
581 document.getElementById("flatten").disabled = true;
582 document.getElementById("teleport").disabled = true;
583 document.getElementById("toggle_map_mode").disabled = true;
584 document.getElementById("switch_to_chat").disabled = true;
585 document.getElementById("switch_to_play").disabled = true;
586 document.getElementById("switch_to_study").disabled = true;
587 document.getElementById("switch_to_edit").disabled = true;
588 document.getElementById("switch_to_portal").disabled = true;
589 document.getElementById("switch_to_annotate").disabled = true;
590 document.getElementById("switch_to_password").disabled = true;
591 document.getElementById("switch_to_admin").disabled = true;
592 document.getElementById("switch_to_control_pw_type").disabled = true;
593 document.getElementById("switch_to_control_tile_type").disabled = true;
594 document.getElementById("move_left").disabled = true;
595 document.getElementById("move_upleft").disabled = true;
596 document.getElementById("move_up").disabled = true;
597 document.getElementById("move_upright").disabled = true;
598 document.getElementById("move_downleft").disabled = true;
599 document.getElementById("move_down").disabled = true;
600 document.getElementById("move_downright").disabled = true;
601 document.getElementById("move_right").disabled = true;
602 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
603 document.getElementById("move_left").disabled = false;
604 document.getElementById("move_right").disabled = false;
605 if (game.map_geometry == 'Hex') {
606 document.getElementById("move_upleft").disabled = false;
607 document.getElementById("move_upright").disabled = false;
608 document.getElementById("move_downleft").disabled = false;
609 document.getElementById("move_downright").disabled = false;
611 document.getElementById("move_up").disabled = false;
612 document.getElementById("move_down").disabled = false;
615 if (!this.mode.is_intro && this.mode.name != 'play') {
616 document.getElementById("switch_to_play").disabled = false;
618 if (!this.mode.is_intro && this.mode.name != 'study') {
619 document.getElementById("switch_to_study").disabled = false;
621 if (!this.mode.is_intro && this.mode.name != 'chat') {
622 document.getElementById("switch_to_chat").disabled = false;
624 if (this.mode.name == 'login') {
625 if (this.login_name) {
626 server.send(['LOGIN', this.login_name]);
628 this.log_msg("? need login name");
630 } else if (this.mode.name == 'play') {
631 if (game.tasks.includes('PICK_UP')) {
632 document.getElementById("take_thing").disabled = false;
634 if (game.tasks.includes('DROP')) {
635 document.getElementById("drop_thing").disabled = false;
637 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
638 document.getElementById("flatten").disabled = false;
640 if (game.tasks.includes('MOVE')) {
642 document.getElementById("teleport").disabled = false;
643 document.getElementById("switch_to_annotate").disabled = false;
644 document.getElementById("switch_to_edit").disabled = false;
645 document.getElementById("switch_to_portal").disabled = false;
646 document.getElementById("switch_to_password").disabled = false;
647 document.getElementById("switch_to_admin").disabled = false;
648 document.getElementById("switch_to_control_pw_type").disabled = false;
649 document.getElementById("switch_to_control_tile_type").disabled = false;
650 } else if (this.mode.name == 'study') {
651 document.getElementById("toggle_map_mode").disabled = false;
652 } else if (this.mode.is_single_char_entry) {
653 this.show_help = true;
654 } else if (this.mode.name == 'admin') {
655 this.log_msg('@ enter admin password:')
656 } else if (this.mode.name == 'control_pw_pw') {
657 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
658 } else if (this.mode.name == 'control_pw_pw') {
659 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
663 offset_links: function(offset, links) {
664 for (let y in links) {
665 let real_y = offset[0] + parseInt(y);
666 if (!this.links[real_y]) {
667 this.links[real_y] = [];
669 for (let link of links[y]) {
670 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
671 this.links[real_y].push(offset_link);
675 restore_input_values: function() {
676 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
677 let info = explorer.info_db[explorer.position];
678 if (info != "(none)") {
679 this.inputEl.value = info;
680 this.recalc_input_lines();
682 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
683 let portal = game.portals[explorer.position]
684 this.inputEl.value = portal;
685 this.recalc_input_lines();
686 } else if (this.mode.name == 'password') {
687 this.inputEl.value = this.password;
688 this.recalc_input_lines();
691 empty_input: function(str) {
692 this.inputEl.value = "";
693 if (this.mode.has_input_prompt) {
694 this.recalc_input_lines();
696 this.height_input = 0;
699 recalc_input_lines: function() {
701 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
702 this.height_input = this.input_lines.length;
704 msg_into_lines_of_width: function(msg, width) {
705 function push_inner_link(y, end_x) {
706 if (!inner_links[y]) {
709 inner_links[y].push([url_start_x, end_x, url]);
711 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
714 for (const match of matches) {
715 const url = match[0];
716 const url_start = match.index;
717 const url_end = match.index + match[0].length;
718 link_data[url_start] = url;
719 url_ends.push(url_end);
723 let inner_links = {};
727 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
728 if (x >= width || msg[i] == "\n") {
730 push_inner_link(y, chunk.length);
736 if (msg[i] == "\n") {
741 if (msg[i] != "\n") {
744 if (i in link_data) {
748 } else if (url_ends.includes(i)) {
749 push_inner_link(y, x);
755 push_inner_link(lines.length - 1, chunk.length);
757 return [lines, inner_links];
759 log_msg: function(msg) {
761 while (this.log.length > 100) {
766 draw_map: function() {
767 let map_lines_split = [];
769 let map_content = game.map;
770 if (this.map_mode == 'control') {
771 map_content = game.map_control;
773 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
774 if (j == game.map_size[1]) {
775 map_lines_split.push(line);
779 line.push(map_content[i] + ' ');
781 map_lines_split.push(line);
782 if (this.map_mode == 'annotations') {
783 for (const coordinate of explorer.info_hints) {
784 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
786 } else if (this.map_mode == 'terrain') {
787 for (const p in game.portals) {
788 let coordinate = p.split(',')
789 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
791 let used_positions = [];
792 for (const thing_id in game.things) {
793 let t = game.things[thing_id];
794 let symbol = game.thing_types[t.type_];
797 meta_char = t.player_char;
799 if (used_positions.includes(t.position.toString())) {
802 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
803 used_positions.push(t.position.toString());
806 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
807 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
810 if (game.map_geometry == 'Square') {
811 for (let line_split of map_lines_split) {
812 map_lines.push(line_split.join(''));
814 } else if (game.map_geometry == 'Hex') {
816 for (let line_split of map_lines_split) {
817 map_lines.push(' '.repeat(indent) + line_split.join(''));
825 let window_center = [terminal.rows / 2, this.window_width / 2];
826 let player = game.things[game.player_id];
827 let center_position = [player.position[0], player.position[1]];
828 if (tui.mode.shows_info) {
829 center_position = [explorer.position[0], explorer.position[1]];
831 center_position[1] = center_position[1] * 2;
832 let offset = [center_position[0] - window_center[0],
833 center_position[1] - window_center[1]]
834 if (game.map_geometry == 'Hex' && offset[0] % 2) {
837 let term_y = Math.max(0, -offset[0]);
838 let term_x = Math.max(0, -offset[1]);
839 let map_y = Math.max(0, offset[0]);
840 let map_x = Math.max(0, offset[1]);
841 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
842 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
843 terminal.write(term_y, term_x, to_draw);
846 draw_mode_line: function() {
847 let help = 'hit [' + this.keys.help + '] for help';
848 if (this.mode.has_input_prompt) {
849 help = 'enter /help for help';
851 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
853 draw_turn_line: function(n) {
854 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
856 draw_history: function() {
857 let log_display_lines = [];
859 let y_offset_in_log = 0;
860 for (let line of this.log) {
861 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
863 log_display_lines = log_display_lines.concat(new_lines);
864 for (const y in link_data) {
865 const rel_y = y_offset_in_log + parseInt(y);
866 log_links[rel_y] = [];
867 for (let link of link_data[y]) {
868 log_links[rel_y].push(link);
871 y_offset_in_log += new_lines.length;
873 let i = log_display_lines.length - 1;
874 for (let y = terminal.rows - 1 - this.height_input;
875 y >= this.height_header && i >= 0;
877 terminal.write(y, this.window_width, log_display_lines[i]);
879 for (const key of Object.keys(log_links)) {
880 if (parseInt(key) <= i) {
881 delete log_links[key];
884 let offset = [terminal.rows - this.height_input - log_display_lines.length,
886 this.offset_links(offset, log_links);
888 draw_info: function() {
889 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
891 let offset = [this.height_header, this.window_width];
892 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
893 terminal.write(y, offset[1], lines[i]);
895 this.offset_links(offset, link_data);
897 draw_input: function() {
898 if (this.mode.has_input_prompt) {
899 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
900 terminal.write(y, this.window_width, this.input_lines[i]);
904 draw_help: function() {
905 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
906 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
907 if (this.mode.name == 'play') {
908 content += "Available actions:\n";
909 if (game.tasks.includes('MOVE')) {
910 content += "[" + movement_keys_desc + "] – move player\n";
912 if (game.tasks.includes('PICK_UP')) {
913 content += "[" + this.keys.take_thing + "] – take thing under player\n";
915 if (game.tasks.includes('DROP')) {
916 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
918 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
919 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
921 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
923 } else if (this.mode.name == 'study') {
924 content += "Available actions:\n";
925 content += '[' + movement_keys_desc + '] – move question mark\n';
926 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
928 } else if (this.mode.name == 'chat') {
929 content += '/nick NAME – re-name yourself to NAME\n';
930 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
931 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
933 content += this.mode.list_available_modes();
935 if (!this.mode.has_input_prompt) {
936 start_x = this.window_width
938 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
939 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
940 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
941 terminal.write(y, start_x, lines[i]);
944 full_refresh: function() {
946 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
947 if (this.mode.is_intro) {
951 if (game.turn_complete) {
953 this.draw_turn_line();
955 this.draw_mode_line();
956 if (this.mode.shows_info) {
963 if (this.show_help) {
975 this.map_control = "";
976 this.map_size = [0,0];
981 get_thing: function(id_, create_if_not_found=false) {
982 if (id_ in game.things) {
983 return game.things[id_];
984 } else if (create_if_not_found) {
985 let t = new Thing([0,0]);
986 game.things[id_] = t;
990 move: function(start_position, direction) {
991 let target = [start_position[0], start_position[1]];
992 if (direction == 'LEFT') {
994 } else if (direction == 'RIGHT') {
996 } else if (game.map_geometry == 'Square') {
997 if (direction == 'UP') {
999 } else if (direction == 'DOWN') {
1002 } else if (game.map_geometry == 'Hex') {
1003 let start_indented = start_position[0] % 2;
1004 if (direction == 'UPLEFT') {
1006 if (!start_indented) {
1009 } else if (direction == 'UPRIGHT') {
1011 if (start_indented) {
1014 } else if (direction == 'DOWNLEFT') {
1016 if (!start_indented) {
1019 } else if (direction == 'DOWNRIGHT') {
1021 if (start_indented) {
1026 if (target[0] < 0 || target[1] < 0 ||
1027 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1032 teleport: function() {
1033 let player = this.get_thing(game.player_id);
1034 if (player.position in this.portals) {
1035 server.reconnect_to(this.portals[player.position]);
1037 terminal.blink_screen();
1038 tui.log_msg('? not standing on portal')
1046 server.init(websocket_location);
1052 move: function(direction) {
1053 let target = game.move(this.position, direction);
1055 this.position = target
1056 if (tui.mode.shows_info) {
1058 } else if (tui.mode.name == 'control_tile_draw') {
1059 this.send_tile_control_command();
1062 terminal.blink_screen();
1065 update_info_db: function(yx, str) {
1066 this.info_db[yx] = str;
1067 if (tui.mode.name == 'study') {
1071 empty_info_db: function() {
1073 this.info_hints = [];
1074 if (tui.mode.name == 'study') {
1078 query_info: function() {
1079 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1081 get_info: function() {
1082 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1083 if (game.fov[position_i] != '.') {
1084 return 'outside field of view';
1087 let terrain_char = game.map[position_i]
1088 let terrain_desc = '?'
1089 if (game.terrains[terrain_char]) {
1090 terrain_desc = game.terrains[terrain_char];
1092 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1093 let protection = game.map_control[position_i];
1094 if (protection == '.') {
1095 protection = 'unprotected';
1097 info += 'PROTECTION: ' + protection + '\n';
1098 for (let t_id in game.things) {
1099 let t = game.things[t_id];
1100 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1101 let symbol = game.thing_types[t.type_];
1102 info += "THING: " + t.type_ + " / " + symbol;
1103 if (t.player_char) {
1104 info += t.player_char;
1107 info += " (" + t.name_ + ")";
1112 if (this.position in game.portals) {
1113 info += "PORTAL: " + game.portals[this.position] + "\n";
1115 if (this.position in this.info_db) {
1116 info += "ANNOTATIONS: " + this.info_db[this.position];
1118 info += 'waiting …';
1122 annotate: function(msg) {
1123 if (msg.length == 0) {
1124 msg = " "; // triggers annotation deletion
1126 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1128 set_portal: function(msg) {
1129 if (msg.length == 0) {
1130 msg = " "; // triggers portal deletion
1132 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1134 send_tile_control_command: function() {
1135 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1139 tui.inputEl.addEventListener('input', (event) => {
1140 if (tui.mode.has_input_prompt) {
1141 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1142 if (tui.inputEl.value.length > max_length) {
1143 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1145 tui.recalc_input_lines();
1146 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1147 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1148 tui.switch_mode('play');
1149 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1150 tui.tile_control_char = tui.inputEl.value[0];
1151 tui.switch_mode('control_pw_pw');
1152 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1153 tui.tile_control_char = tui.inputEl.value[0];
1154 tui.switch_mode('control_tile_draw');
1158 tui.inputEl.addEventListener('keydown', (event) => {
1159 tui.show_help = false;
1160 if (event.key == 'Enter') {
1161 event.preventDefault();
1163 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1164 tui.show_help = true;
1166 tui.restore_input_values();
1167 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1168 && !tui.mode.is_single_char_entry) {
1169 tui.show_help = true;
1170 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1171 tui.login_name = tui.inputEl.value;
1172 server.send(['LOGIN', tui.inputEl.value]);
1174 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1175 if (tui.inputEl.value.length == 0) {
1176 tui.log_msg('@ aborted');
1178 server.send(['SET_MAP_CONTROL_PASSWORD',
1179 tui.tile_control_char, tui.inputEl.value]);
1181 tui.switch_mode('play');
1182 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1183 explorer.set_portal(tui.inputEl.value);
1184 tui.switch_mode('play');
1185 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1186 explorer.annotate(tui.inputEl.value);
1187 tui.switch_mode('play');
1188 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1189 if (tui.inputEl.value.length == 0) {
1190 tui.inputEl.value = " ";
1192 tui.password = tui.inputEl.value
1193 tui.switch_mode('play');
1194 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1195 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1196 tui.switch_mode('play');
1197 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1198 let tokens = parser.tokenize(tui.inputEl.value);
1199 if (tokens.length > 0 && tokens[0].length > 0) {
1200 if (tui.inputEl.value[0][0] == '/') {
1201 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1202 tui.switch_mode('play');
1203 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1204 tui.switch_mode('study');
1205 } else if (tokens[0].slice(1) == 'nick') {
1206 if (tokens.length > 1) {
1207 server.send(['NICK', tokens[1]]);
1209 tui.log_msg('? need new name');
1212 tui.log_msg('? unknown command');
1215 server.send(['ALL', tui.inputEl.value]);
1217 } else if (tui.inputEl.valuelength > 0) {
1218 server.send(['ALL', tui.inputEl.value]);
1221 } else if (tui.mode.name == 'play') {
1222 if (tui.mode.mode_switch_on_key(event)) {
1224 } else if (event.key === tui.keys.flatten
1225 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1226 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1227 } else if (event.key === tui.keys.take_thing
1228 && game.tasks.includes('PICK_UP')) {
1229 server.send(["TASK:PICK_UP"]);
1230 } else if (event.key === tui.keys.drop_thing
1231 && game.tasks.includes('DROP')) {
1232 server.send(["TASK:DROP"]);
1233 } else if (event.key in tui.movement_keys
1234 && game.tasks.includes('MOVE')) {
1235 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1236 } else if (event.key === tui.keys.teleport) {
1238 } else if (event.key === tui.keys.switch_to_portal) {
1239 event.preventDefault();
1240 tui.switch_mode('portal');
1241 } else if (event.key === tui.keys.switch_to_annotate) {
1242 event.preventDefault();
1243 tui.switch_mode('annotate');
1245 } else if (tui.mode.name == 'study') {
1246 if (tui.mode.mode_switch_on_key(event)) {
1248 } else if (event.key in tui.movement_keys) {
1249 explorer.move(tui.movement_keys[event.key]);
1250 } else if (event.key == tui.keys.toggle_map_mode) {
1251 if (tui.map_mode == 'terrain') {
1252 tui.map_mode = 'annotations';
1253 } else if (tui.map_mode == 'annotations') {
1254 tui.map_mode = 'control';
1256 tui.map_mode = 'terrain';
1259 } else if (tui.mode.name == 'control_tile_draw') {
1260 if (tui.mode.mode_switch_on_key(event)) {
1262 } else if (event.key in tui.movement_keys) {
1263 explorer.move(tui.movement_keys[event.key]);
1269 rows_selector.addEventListener('input', function() {
1270 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1273 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1274 terminal.initialize();
1277 cols_selector.addEventListener('input', function() {
1278 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1281 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1282 terminal.initialize();
1283 tui.window_width = terminal.cols / 2,
1286 for (let key_selector of key_selectors) {
1287 key_selector.addEventListener('input', function() {
1288 window.localStorage.setItem(key_selector.id, key_selector.value);
1292 window.setInterval(function() {
1293 if (server.connected) {
1294 server.send(['PING']);
1296 server.reconnect_to(server.url);
1297 tui.log_msg('@ attempting reconnect …')
1300 document.getElementById("terminal").onclick = function() {
1301 tui.inputEl.focus();
1303 document.getElementById("help").onclick = function() {
1304 tui.show_help = true;
1307 document.getElementById("switch_to_play").onclick = function() {
1308 tui.switch_mode('play');
1311 document.getElementById("switch_to_study").onclick = function() {
1312 tui.switch_mode('study');
1315 document.getElementById("switch_to_chat").onclick = function() {
1316 tui.switch_mode('chat');
1319 document.getElementById("switch_to_password").onclick = function() {
1320 tui.switch_mode('password');
1323 document.getElementById("switch_to_edit").onclick = function() {
1324 tui.switch_mode('edit');
1327 document.getElementById("switch_to_annotate").onclick = function() {
1328 tui.switch_mode('annotate');
1331 document.getElementById("switch_to_portal").onclick = function() {
1332 tui.switch_mode('portal');
1335 document.getElementById("switch_to_admin").onclick = function() {
1336 tui.switch_mode('admin');
1339 document.getElementById("switch_to_control_pw_type").onclick = function() {
1340 tui.switch_mode('control_pw_type');
1343 document.getElementById("switch_to_control_tile_type").onclick = function() {
1344 tui.switch_mode('control_tile_type');
1347 document.getElementById("toggle_map_mode").onclick = function() {
1348 if (tui.map_mode == 'terrain') {
1349 tui.map_mode = 'annotations';
1350 } else if (tui.map_mode == 'annotations') {
1351 tui.map_mode = 'control';
1353 tui.map_mode = 'terrain';
1357 document.getElementById("take_thing").onclick = function() {
1358 server.send(['TASK:PICK_UP']);
1360 document.getElementById("drop_thing").onclick = function() {
1361 server.send(['TASK:DROP']);
1363 document.getElementById("flatten").onclick = function() {
1364 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1366 document.getElementById("teleport").onclick = function() {
1369 document.getElementById("move_upleft").onclick = function() {
1370 if (tui.mode.name == 'play') {
1371 server.send(['TASK:MOVE', 'UPLEFT']);
1373 explorer.move('UPLEFT');
1376 document.getElementById("move_left").onclick = function() {
1377 if (tui.mode.name == 'play') {
1378 server.send(['TASK:MOVE', 'LEFT']);
1380 explorer.move('LEFT');
1383 document.getElementById("move_downleft").onclick = function() {
1384 if (tui.mode.name == 'play') {
1385 server.send(['TASK:MOVE', 'DOWNLEFT']);
1387 explorer.move('DOWNLEFT');
1390 document.getElementById("move_down").onclick = function() {
1391 if (tui.mode.name == 'play') {
1392 server.send(['TASK:MOVE', 'DOWN']);
1394 explorer.move('DOWN');
1397 document.getElementById("move_up").onclick = function() {
1398 if (tui.mode.name == 'play') {
1399 server.send(['TASK:MOVE', 'UP']);
1401 explorer.move('UP');
1404 document.getElementById("move_upright").onclick = function() {
1405 if (tui.mode.name == 'play') {
1406 server.send(['TASK:MOVE', 'UPRIGHT']);
1408 explorer.move('UPRIGHT');
1411 document.getElementById("move_right").onclick = function() {
1412 if (tui.mode.name == 'play') {
1413 server.send(['TASK:MOVE', 'RIGHT']);
1415 explorer.move('RIGHT');
1418 document.getElementById("move_downright").onclick = function() {
1419 if (tui.mode.name == 'play') {
1420 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1422 explorer.move('DOWNRIGHT');