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 <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal / curses client)
14 <h3>button controls for mouse players</h3>
15 <table style="float: left">
17 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
18 <td style="text-align: center"><button id="move_up">up</button></td>
19 <td><button id="move_upright">up-right</button></td>
22 <td style="text-align: right;"><button id="move_left">left</button></td>
23 <td stlye="text-align: center;">move</td>
24 <td><button id="move_right">right</button></td>
27 <td><button id="move_downleft">down-left</button></td>
28 <td style="text-align: center"><button id="move_down">down</button></td>
29 <td><button id="move_downright">down-right</button></td>
34 <td><button id="help">help</button></td>
37 <td><button id="switch_to_chat">chat mode</button><br /></td>
39 <td><button id="switch_to_study">study mode</button></td>
40 <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
42 <td><button id="switch_to_play">play mode</button></td>
46 <td><button id="take_thing">take thing</button></td>
47 <td><button id="switch_to_edit">change tile</button></td>
48 <td><button id="switch_to_admin">become admin</button></td>
51 <td><button id="drop_thing">drop thing</button></td>
52 <td><button id="switch_to_password">change tile editing password</button></td>
53 <td><button id="switch_to_control_pw_type">change tile control password</button></td>
56 <td><button id="flatten">flatten surroundings</button></td>
57 <td><button id="switch_to_annotate">annotate tile</button></td>
58 <td><button id="switch_to_control_tile_type">change tiles control</button></td>
61 <td><button id="teleport">teleport</button></td>
62 <td><button id="switch_to_portal">edit portal link</button></td>
68 <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 />
70 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
71 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
72 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
73 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
74 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
75 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
76 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
77 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
78 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
79 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
80 <li>help: <input id="key_help" type="text" value="h" />
81 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
82 <li>teleport: <input id="key_teleport" type="text" value="p" />
83 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
84 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
85 <li><input id="key_switch_to_chat" type="text" value="t" />
86 <li><input id="key_switch_to_play" type="text" value="p" />
87 <li><input id="key_switch_to_study" type="text" value="?" />
88 <li><input id="key_switch_to_edit" type="text" value="m" />
89 <li><input id="key_switch_to_password" type="text" value="P" />
90 <li><input id="key_switch_to_admin" type="text" value="A" />
91 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
92 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
93 <li><input id="key_switch_to_annotate" type="text" value="M" />
94 <li><input id="key_switch_to_portal" type="text" value="T" />
95 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
100 let websocket_location = "wss://plomlompom.com/rogue_chat/";
101 //let websocket_location = "ws://localhost:8000/";
106 'long': 'This mode allows you to interact with the map.'
110 '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.'},
112 'short': 'terrain edit',
113 '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.'
116 'short': 'change tiles control password',
117 '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!'
120 'short': 'change tiles control password',
121 '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.'
123 'control_tile_type': {
124 'short': 'change tiles control',
125 '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.'
127 'control_tile_draw': {
128 'short': 'change tiles control',
129 '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'
132 'short': 'annotate tile',
133 '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.'
136 'short': 'edit portal',
137 '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.'
141 '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:'
145 'long': 'Pick your player name.'
147 'waiting_for_server': {
148 'short': 'waiting for server response',
149 'long': 'Waiting for a server response.'
152 'short': 'waiting for server response',
153 'long': 'Waiting for a server response.'
156 'short': 'map edit password',
157 '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.'
160 'short': 'become admin',
161 'long': 'This mode allows you to become admin if you know an admin password.'
165 let rows_selector = document.getElementById("n_rows");
166 let cols_selector = document.getElementById("n_cols");
167 let key_selectors = document.querySelectorAll('[id^="key_"]');
169 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
170 const action = key_switch_selector.id.slice("key_switch_to_".length);
171 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
174 function restore_selector_value(selector) {
175 let stored_selection = window.localStorage.getItem(selector.id);
176 if (stored_selection) {
177 selector.value = stored_selection;
180 restore_selector_value(rows_selector);
181 restore_selector_value(cols_selector);
182 for (let key_selector of key_selectors) {
183 restore_selector_value(key_selector);
189 initialize: function() {
190 this.rows = rows_selector.value;
191 this.cols = cols_selector.value;
192 this.pre_el = document.getElementById("terminal");
193 this.pre_el.style.color = this.foreground;
194 this.pre_el.style.backgroundColor = this.background;
197 for (let y = 0, x = 0; y <= this.rows; x++) {
198 if (x == this.cols) {
201 this.content.push(line);
203 if (y == this.rows) {
210 blink_screen: function() {
211 this.pre_el.style.color = this.background;
212 this.pre_el.style.backgroundColor = this.foreground;
214 this.pre_el.style.color = this.foreground;
215 this.pre_el.style.backgroundColor = this.background;
218 refresh: function() {
219 function escapeHTML(str) {
221 replace(/&/g, '&').
222 replace(/</g, '<').
223 replace(/>/g, '>').
224 replace(/'/g, ''').
225 replace(/"/g, '"');
227 let pre_content = '';
228 for (let y = 0; y < this.rows; y++) {
229 let line = this.content[y].join('');
231 if (y in tui.links) {
233 for (let span of tui.links[y]) {
234 chunks.push(escapeHTML(line.slice(start_x, span[0])));
235 chunks.push('<a href="');
236 chunks.push(escapeHTML(span[2]));
238 chunks.push(escapeHTML(line.slice(span[0], span[1])));
242 chunks.push(escapeHTML(line.slice(start_x)));
244 chunks = [escapeHTML(line)];
246 for (const chunk of chunks) {
247 pre_content += chunk;
251 this.pre_el.innerHTML = pre_content;
253 write: function(start_y, start_x, msg) {
254 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
255 this.content[start_y][x] = msg[i];
258 drawBox: function(start_y, start_x, height, width) {
259 let end_y = start_y + height;
260 let end_x = start_x + width;
261 for (let y = start_y, x = start_x; y < this.rows; x++) {
269 this.content[y][x] = ' ';
273 terminal.initialize();
276 tokenize: function(str) {
281 for (let i = 0; i < str.length; i++) {
287 } else if (c == '\\') {
289 } else if (c == '"') {
294 } else if (c == '"') {
296 } else if (c === ' ') {
297 if (token.length > 0) {
305 if (token.length > 0) {
310 parse_yx: function(position_string) {
311 let coordinate_strings = position_string.split(',')
312 let position = [0, 0];
313 position[0] = parseInt(coordinate_strings[0].slice(2));
314 position[1] = parseInt(coordinate_strings[1].slice(2));
326 init: function(url) {
328 this.websocket = new WebSocket(this.url);
329 this.websocket.onopen = function(event) {
330 server.connected = true;
331 game.thing_types = {};
333 server.send(['TASKS']);
334 server.send(['TERRAINS']);
335 server.send(['THING_TYPES']);
336 tui.log_msg("@ server connected! :)");
337 tui.switch_mode('login');
339 this.websocket.onclose = function(event) {
340 server.connected = false;
341 tui.switch_mode('waiting_for_server');
342 tui.log_msg("@ server disconnected :(");
344 this.websocket.onmessage = this.handle_event;
346 reconnect_to: function(url) {
347 this.websocket.close();
350 send: function(tokens) {
351 this.websocket.send(unparser.untokenize(tokens));
353 handle_event: function(event) {
354 let tokens = parser.tokenize(event.data);
355 if (tokens[0] === 'TURN') {
356 game.turn_complete = false;
357 explorer.empty_info_db();
360 game.turn = parseInt(tokens[1]);
361 } else if (tokens[0] === 'THING') {
362 let t = game.get_thing(tokens[3], true);
363 t.position = parser.parse_yx(tokens[1]);
365 } else if (tokens[0] === 'THING_NAME') {
366 let t = game.get_thing(tokens[1], false);
370 } else if (tokens[0] === 'THING_CHAR') {
371 let t = game.get_thing(tokens[1], false);
373 t.player_char = tokens[2];
375 } else if (tokens[0] === 'TASKS') {
376 game.tasks = tokens[1].split(',');
377 tui.mode_edit.legal = game.tasks.includes('WRITE');
378 } else if (tokens[0] === 'THING_TYPE') {
379 game.thing_types[tokens[1]] = tokens[2]
380 } else if (tokens[0] === 'TERRAIN') {
381 game.terrains[tokens[1]] = tokens[2]
382 } else if (tokens[0] === 'MAP') {
383 game.map_geometry = tokens[1];
385 game.map_size = parser.parse_yx(tokens[2]);
387 } else if (tokens[0] === 'FOV') {
389 } else if (tokens[0] === 'MAP_CONTROL') {
390 game.map_control = tokens[1]
391 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
392 game.turn_complete = true;
393 if (tui.mode.name == 'post_login_wait') {
394 tui.switch_mode('play');
395 } else if (tui.mode.name == 'study') {
396 explorer.query_info();
399 } else if (tokens[0] === 'CHAT') {
400 tui.log_msg('# ' + tokens[1], 1);
401 } else if (tokens[0] === 'PLAYER_ID') {
402 game.player_id = parseInt(tokens[1]);
403 } else if (tokens[0] === 'LOGIN_OK') {
404 this.send(['GET_GAMESTATE']);
405 tui.switch_mode('post_login_wait');
406 } else if (tokens[0] === 'PORTAL') {
407 let position = parser.parse_yx(tokens[1]);
408 game.portals[position] = tokens[2];
409 } else if (tokens[0] === 'ANNOTATION_HINT') {
410 let position = parser.parse_yx(tokens[1]);
411 explorer.info_hints = explorer.info_hints.concat([position]);
412 } else if (tokens[0] === 'ANNOTATION') {
413 let position = parser.parse_yx(tokens[1]);
414 explorer.update_info_db(position, tokens[2]);
415 tui.restore_input_values();
417 } else if (tokens[0] === 'UNHANDLED_INPUT') {
418 tui.log_msg('? unknown command');
419 } else if (tokens[0] === 'PLAY_ERROR') {
420 tui.log_msg('? ' + tokens[1]);
421 terminal.blink_screen();
422 } else if (tokens[0] === 'ARGUMENT_ERROR') {
423 tui.log_msg('? syntax error: ' + tokens[1]);
424 } else if (tokens[0] === 'GAME_ERROR') {
425 tui.log_msg('? game error: ' + tokens[1]);
426 } else if (tokens[0] === 'PONG') {
429 tui.log_msg('? unhandled input: ' + event.data);
435 quote: function(str) {
437 for (let i = 0; i < str.length; i++) {
439 if (['"', '\\'].includes(c)) {
445 return quoted.join('');
447 to_yx: function(yx_coordinate) {
448 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
450 untokenize: function(tokens) {
451 let quoted_tokens = [];
452 for (let token of tokens) {
453 quoted_tokens.push(this.quote(token));
455 return quoted_tokens.join(" ");
460 constructor(name, has_input_prompt=false, shows_info=false,
461 is_intro=false, is_single_char_entry=false) {
463 this.short_desc = mode_helps[name].short;
464 this.available_modes = [];
465 this.has_input_prompt = has_input_prompt;
466 this.shows_info= shows_info;
467 this.is_intro = is_intro;
468 this.help_intro = mode_helps[name].long;
469 this.is_single_char_entry = is_single_char_entry;
472 *iter_available_modes() {
473 for (let mode_name of this.available_modes) {
474 let mode = tui['mode_' + mode_name];
478 let key = tui.keys['switch_to_' + mode.name];
482 list_available_modes() {
484 if (this.available_modes.length > 0) {
485 msg += 'Other modes available from here:\n';
486 for (let [mode, key] of this.iter_available_modes()) {
487 msg += '[' + key + '] – ' + mode.short_desc + '\n';
492 mode_switch_on_key(key_event) {
493 for (let [mode, key] of this.iter_available_modes()) {
494 if (key_event.key == key) {
495 event.preventDefault();
496 tui.switch_mode(mode.name);
508 window_width: terminal.cols / 2,
514 mode_waiting_for_server: new Mode('waiting_for_server',
516 mode_login: new Mode('login', true, false, true),
517 mode_post_login_wait: new Mode('post_login_wait'),
518 mode_chat: new Mode('chat', true),
519 mode_annotate: new Mode('annotate', true, true),
520 mode_play: new Mode('play'),
521 mode_study: new Mode('study', false, true),
522 mode_edit: new Mode('edit', false, false, false, true),
523 mode_control_pw_type: new Mode('control_pw_type',
524 false, false, false, true),
525 mode_portal: new Mode('portal', true, true),
526 mode_password: new Mode('password', true),
527 mode_admin: new Mode('admin', true),
528 mode_control_pw_pw: new Mode('control_pw_pw', true),
529 mode_control_tile_type: new Mode('control_tile_type',
530 false, false, false, true),
531 mode_control_tile_draw: new Mode('control_tile_draw'),
533 this.mode_play.available_modes = ["chat", "study", "edit",
534 "annotate", "portal",
538 this.mode_study.available_modes = ["chat", "play"]
539 this.mode_control_tile_draw.available_modes = ["play"]
540 this.mode = this.mode_waiting_for_server;
541 this.inputEl = document.getElementById("input");
542 this.inputEl.focus();
543 this.recalc_input_lines();
544 this.height_header = this.height_turn_line + this.height_mode_line;
545 this.log_msg("@ waiting for server connection ...");
548 init_keys: function() {
550 for (let key_selector of key_selectors) {
551 this.keys[key_selector.id.slice(4)] = key_selector.value;
553 if (game.map_geometry == 'Square') {
554 this.movement_keys = {
555 [this.keys.square_move_up]: 'UP',
556 [this.keys.square_move_left]: 'LEFT',
557 [this.keys.square_move_down]: 'DOWN',
558 [this.keys.square_move_right]: 'RIGHT'
560 document.getElementById("move_upright").hidden = true;
561 document.getElementById("move_upleft").hidden = true;
562 document.getElementById("move_downright").hidden = true;
563 document.getElementById("move_downleft").hidden = true;
564 document.getElementById("move_up").hidden = false;
565 document.getElementById("move_down").hidden = false;
566 } else if (game.map_geometry == 'Hex') {
567 document.getElementById("move_upright").hidden = false;
568 document.getElementById("move_upleft").hidden = false;
569 document.getElementById("move_downright").hidden = false;
570 document.getElementById("move_downleft").hidden = false;
571 document.getElementById("move_up").hidden = true;
572 document.getElementById("move_down").hidden = true;
573 this.movement_keys = {
574 [this.keys.hex_move_upleft]: 'UPLEFT',
575 [this.keys.hex_move_upright]: 'UPRIGHT',
576 [this.keys.hex_move_right]: 'RIGHT',
577 [this.keys.hex_move_downright]: 'DOWNRIGHT',
578 [this.keys.hex_move_downleft]: 'DOWNLEFT',
579 [this.keys.hex_move_left]: 'LEFT'
583 switch_mode: function(mode_name) {
584 this.inputEl.focus();
585 this.map_mode = 'terrain';
586 this.mode = this['mode_' + mode_name];
587 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
588 explorer.position = game.things[game.player_id].position;
589 if (this.mode.shows_info) {
590 explorer.query_info();
591 } else if (this.mode.name == 'control_tile_draw') {
592 explorer.send_tile_control_command();
593 this.map_mode = 'control';
597 this.restore_input_values();
598 for (let el of document.getElementsByTagName("button")) {
601 document.getElementById("help").disabled = false;
602 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
603 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
604 move_key.disabled = false;
607 if (!this.mode.is_intro && this.mode.name != 'play') {
608 document.getElementById("switch_to_play").disabled = false;
610 if (!this.mode.is_intro && this.mode.name != 'study') {
611 document.getElementById("switch_to_study").disabled = false;
613 if (!this.mode.is_intro && this.mode.name != 'chat') {
614 document.getElementById("switch_to_chat").disabled = false;
616 if (this.mode.name == 'login') {
617 if (this.login_name) {
618 server.send(['LOGIN', this.login_name]);
620 this.log_msg("? need login name");
622 } else if (this.mode.name == 'play') {
623 if (game.tasks.includes('PICK_UP')) {
624 document.getElementById("take_thing").disabled = false;
626 if (game.tasks.includes('DROP')) {
627 document.getElementById("drop_thing").disabled = false;
629 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
630 document.getElementById("flatten").disabled = false;
632 if (game.tasks.includes('MOVE')) {
634 document.getElementById("teleport").disabled = false;
635 document.getElementById("switch_to_annotate").disabled = false;
636 document.getElementById("switch_to_edit").disabled = false;
637 document.getElementById("switch_to_portal").disabled = false;
638 document.getElementById("switch_to_password").disabled = false;
639 document.getElementById("switch_to_admin").disabled = false;
640 document.getElementById("switch_to_control_pw_type").disabled = false;
641 document.getElementById("switch_to_control_tile_type").disabled = false;
642 } else if (this.mode.name == 'study') {
643 document.getElementById("toggle_map_mode").disabled = false;
644 } else if (this.mode.is_single_char_entry) {
645 this.show_help = true;
646 } else if (this.mode.name == 'admin') {
647 this.log_msg('@ enter admin password:')
648 } else if (this.mode.name == 'control_pw_pw') {
649 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
650 } else if (this.mode.name == 'control_pw_pw') {
651 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
655 offset_links: function(offset, links) {
656 for (let y in links) {
657 let real_y = offset[0] + parseInt(y);
658 if (!this.links[real_y]) {
659 this.links[real_y] = [];
661 for (let link of links[y]) {
662 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
663 this.links[real_y].push(offset_link);
667 restore_input_values: function() {
668 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
669 let info = explorer.info_db[explorer.position];
670 if (info != "(none)") {
671 this.inputEl.value = info;
672 this.recalc_input_lines();
674 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
675 let portal = game.portals[explorer.position]
676 this.inputEl.value = portal;
677 this.recalc_input_lines();
678 } else if (this.mode.name == 'password') {
679 this.inputEl.value = this.password;
680 this.recalc_input_lines();
683 empty_input: function(str) {
684 this.inputEl.value = "";
685 if (this.mode.has_input_prompt) {
686 this.recalc_input_lines();
688 this.height_input = 0;
691 recalc_input_lines: function() {
693 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
694 this.height_input = this.input_lines.length;
696 msg_into_lines_of_width: function(msg, width) {
697 function push_inner_link(y, end_x) {
698 if (!inner_links[y]) {
701 inner_links[y].push([url_start_x, end_x, url]);
703 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
706 for (const match of matches) {
707 const url = match[0];
708 const url_start = match.index;
709 const url_end = match.index + match[0].length;
710 link_data[url_start] = url;
711 url_ends.push(url_end);
715 let inner_links = {};
719 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
720 if (x >= width || msg[i] == "\n") {
722 push_inner_link(y, chunk.length);
728 if (msg[i] == "\n") {
733 if (msg[i] != "\n") {
736 if (i in link_data) {
740 } else if (url_ends.includes(i)) {
741 push_inner_link(y, x);
747 push_inner_link(lines.length - 1, chunk.length);
749 return [lines, inner_links];
751 log_msg: function(msg) {
753 while (this.log.length > 100) {
758 draw_map: function() {
759 let map_lines_split = [];
761 let map_content = game.map;
762 if (this.map_mode == 'control') {
763 map_content = game.map_control;
765 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
766 if (j == game.map_size[1]) {
767 map_lines_split.push(line);
771 line.push(map_content[i] + ' ');
773 map_lines_split.push(line);
774 if (this.map_mode == 'annotations') {
775 for (const coordinate of explorer.info_hints) {
776 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
778 } else if (this.map_mode == 'terrain') {
779 for (const p in game.portals) {
780 let coordinate = p.split(',')
781 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
783 let used_positions = [];
784 for (const thing_id in game.things) {
785 let t = game.things[thing_id];
786 let symbol = game.thing_types[t.type_];
789 meta_char = t.player_char;
791 if (used_positions.includes(t.position.toString())) {
794 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
795 used_positions.push(t.position.toString());
798 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
799 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
802 if (game.map_geometry == 'Square') {
803 for (let line_split of map_lines_split) {
804 map_lines.push(line_split.join(''));
806 } else if (game.map_geometry == 'Hex') {
808 for (let line_split of map_lines_split) {
809 map_lines.push(' '.repeat(indent) + line_split.join(''));
817 let window_center = [terminal.rows / 2, this.window_width / 2];
818 let player = game.things[game.player_id];
819 let center_position = [player.position[0], player.position[1]];
820 if (tui.mode.shows_info) {
821 center_position = [explorer.position[0], explorer.position[1]];
823 center_position[1] = center_position[1] * 2;
824 let offset = [center_position[0] - window_center[0],
825 center_position[1] - window_center[1]]
826 if (game.map_geometry == 'Hex' && offset[0] % 2) {
829 let term_y = Math.max(0, -offset[0]);
830 let term_x = Math.max(0, -offset[1]);
831 let map_y = Math.max(0, offset[0]);
832 let map_x = Math.max(0, offset[1]);
833 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
834 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
835 terminal.write(term_y, term_x, to_draw);
838 draw_mode_line: function() {
839 let help = 'hit [' + this.keys.help + '] for help';
840 if (this.mode.has_input_prompt) {
841 help = 'enter /help for help';
843 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
845 draw_turn_line: function(n) {
846 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
848 draw_history: function() {
849 let log_display_lines = [];
851 let y_offset_in_log = 0;
852 for (let line of this.log) {
853 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
855 log_display_lines = log_display_lines.concat(new_lines);
856 for (const y in link_data) {
857 const rel_y = y_offset_in_log + parseInt(y);
858 log_links[rel_y] = [];
859 for (let link of link_data[y]) {
860 log_links[rel_y].push(link);
863 y_offset_in_log += new_lines.length;
865 let i = log_display_lines.length - 1;
866 for (let y = terminal.rows - 1 - this.height_input;
867 y >= this.height_header && i >= 0;
869 terminal.write(y, this.window_width, log_display_lines[i]);
871 for (const key of Object.keys(log_links)) {
872 if (parseInt(key) <= i) {
873 delete log_links[key];
876 let offset = [terminal.rows - this.height_input - log_display_lines.length,
878 this.offset_links(offset, log_links);
880 draw_info: function() {
881 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
883 let offset = [this.height_header, this.window_width];
884 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
885 terminal.write(y, offset[1], lines[i]);
887 this.offset_links(offset, link_data);
889 draw_input: function() {
890 if (this.mode.has_input_prompt) {
891 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
892 terminal.write(y, this.window_width, this.input_lines[i]);
896 draw_help: function() {
897 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
898 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
899 if (this.mode.name == 'play') {
900 content += "Available actions:\n";
901 if (game.tasks.includes('MOVE')) {
902 content += "[" + movement_keys_desc + "] – move player\n";
904 if (game.tasks.includes('PICK_UP')) {
905 content += "[" + this.keys.take_thing + "] – take thing under player\n";
907 if (game.tasks.includes('DROP')) {
908 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
910 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
911 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
913 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
915 } else if (this.mode.name == 'study') {
916 content += "Available actions:\n";
917 content += '[' + movement_keys_desc + '] – move question mark\n';
918 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
920 } else if (this.mode.name == 'chat') {
921 content += '/nick NAME – re-name yourself to NAME\n';
922 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
923 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
925 content += this.mode.list_available_modes();
927 if (!this.mode.has_input_prompt) {
928 start_x = this.window_width
930 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
931 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
932 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
933 terminal.write(y, start_x, lines[i]);
936 full_refresh: function() {
938 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
939 if (this.mode.is_intro) {
943 if (game.turn_complete) {
945 this.draw_turn_line();
947 this.draw_mode_line();
948 if (this.mode.shows_info) {
955 if (this.show_help) {
967 this.map_control = "";
968 this.map_size = [0,0];
973 get_thing: function(id_, create_if_not_found=false) {
974 if (id_ in game.things) {
975 return game.things[id_];
976 } else if (create_if_not_found) {
977 let t = new Thing([0,0]);
978 game.things[id_] = t;
982 move: function(start_position, direction) {
983 let target = [start_position[0], start_position[1]];
984 if (direction == 'LEFT') {
986 } else if (direction == 'RIGHT') {
988 } else if (game.map_geometry == 'Square') {
989 if (direction == 'UP') {
991 } else if (direction == 'DOWN') {
994 } else if (game.map_geometry == 'Hex') {
995 let start_indented = start_position[0] % 2;
996 if (direction == 'UPLEFT') {
998 if (!start_indented) {
1001 } else if (direction == 'UPRIGHT') {
1003 if (start_indented) {
1006 } else if (direction == 'DOWNLEFT') {
1008 if (!start_indented) {
1011 } else if (direction == 'DOWNRIGHT') {
1013 if (start_indented) {
1018 if (target[0] < 0 || target[1] < 0 ||
1019 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1024 teleport: function() {
1025 let player = this.get_thing(game.player_id);
1026 if (player.position in this.portals) {
1027 server.reconnect_to(this.portals[player.position]);
1029 terminal.blink_screen();
1030 tui.log_msg('? not standing on portal')
1038 server.init(websocket_location);
1044 move: function(direction) {
1045 let target = game.move(this.position, direction);
1047 this.position = target
1048 if (tui.mode.shows_info) {
1050 } else if (tui.mode.name == 'control_tile_draw') {
1051 this.send_tile_control_command();
1054 terminal.blink_screen();
1057 update_info_db: function(yx, str) {
1058 this.info_db[yx] = str;
1059 if (tui.mode.name == 'study') {
1063 empty_info_db: function() {
1065 this.info_hints = [];
1066 if (tui.mode.name == 'study') {
1070 query_info: function() {
1071 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1073 get_info: function() {
1074 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1075 if (game.fov[position_i] != '.') {
1076 return 'outside field of view';
1079 let terrain_char = game.map[position_i]
1080 let terrain_desc = '?'
1081 if (game.terrains[terrain_char]) {
1082 terrain_desc = game.terrains[terrain_char];
1084 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1085 let protection = game.map_control[position_i];
1086 if (protection == '.') {
1087 protection = 'unprotected';
1089 info += 'PROTECTION: ' + protection + '\n';
1090 for (let t_id in game.things) {
1091 let t = game.things[t_id];
1092 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1093 let symbol = game.thing_types[t.type_];
1094 info += "THING: " + t.type_ + " / " + symbol;
1095 if (t.player_char) {
1096 info += t.player_char;
1099 info += " (" + t.name_ + ")";
1104 if (this.position in game.portals) {
1105 info += "PORTAL: " + game.portals[this.position] + "\n";
1107 if (this.position in this.info_db) {
1108 info += "ANNOTATIONS: " + this.info_db[this.position];
1110 info += 'waiting …';
1114 annotate: function(msg) {
1115 if (msg.length == 0) {
1116 msg = " "; // triggers annotation deletion
1118 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1120 set_portal: function(msg) {
1121 if (msg.length == 0) {
1122 msg = " "; // triggers portal deletion
1124 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1126 send_tile_control_command: function() {
1127 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1131 tui.inputEl.addEventListener('input', (event) => {
1132 if (tui.mode.has_input_prompt) {
1133 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1134 if (tui.inputEl.value.length > max_length) {
1135 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1137 tui.recalc_input_lines();
1138 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1139 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1140 tui.switch_mode('play');
1141 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1142 tui.tile_control_char = tui.inputEl.value[0];
1143 tui.switch_mode('control_pw_pw');
1144 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1145 tui.tile_control_char = tui.inputEl.value[0];
1146 tui.switch_mode('control_tile_draw');
1150 tui.inputEl.addEventListener('keydown', (event) => {
1151 tui.show_help = false;
1152 if (event.key == 'Enter') {
1153 event.preventDefault();
1155 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1156 tui.show_help = true;
1158 tui.restore_input_values();
1159 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1160 && !tui.mode.is_single_char_entry) {
1161 tui.show_help = true;
1162 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1163 tui.login_name = tui.inputEl.value;
1164 server.send(['LOGIN', tui.inputEl.value]);
1166 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1167 if (tui.inputEl.value.length == 0) {
1168 tui.log_msg('@ aborted');
1170 server.send(['SET_MAP_CONTROL_PASSWORD',
1171 tui.tile_control_char, tui.inputEl.value]);
1173 tui.switch_mode('play');
1174 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1175 explorer.set_portal(tui.inputEl.value);
1176 tui.switch_mode('play');
1177 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1178 explorer.annotate(tui.inputEl.value);
1179 tui.switch_mode('play');
1180 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1181 if (tui.inputEl.value.length == 0) {
1182 tui.inputEl.value = " ";
1184 tui.password = tui.inputEl.value
1185 tui.switch_mode('play');
1186 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1187 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1188 tui.switch_mode('play');
1189 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1190 let tokens = parser.tokenize(tui.inputEl.value);
1191 if (tokens.length > 0 && tokens[0].length > 0) {
1192 if (tui.inputEl.value[0][0] == '/') {
1193 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1194 tui.switch_mode('play');
1195 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1196 tui.switch_mode('study');
1197 } else if (tokens[0].slice(1) == 'nick') {
1198 if (tokens.length > 1) {
1199 server.send(['NICK', tokens[1]]);
1201 tui.log_msg('? need new name');
1204 tui.log_msg('? unknown command');
1207 server.send(['ALL', tui.inputEl.value]);
1209 } else if (tui.inputEl.valuelength > 0) {
1210 server.send(['ALL', tui.inputEl.value]);
1213 } else if (tui.mode.name == 'play') {
1214 if (tui.mode.mode_switch_on_key(event)) {
1216 } else if (event.key === tui.keys.flatten
1217 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1218 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1219 } else if (event.key === tui.keys.take_thing
1220 && game.tasks.includes('PICK_UP')) {
1221 server.send(["TASK:PICK_UP"]);
1222 } else if (event.key === tui.keys.drop_thing
1223 && game.tasks.includes('DROP')) {
1224 server.send(["TASK:DROP"]);
1225 } else if (event.key in tui.movement_keys
1226 && game.tasks.includes('MOVE')) {
1227 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1228 } else if (event.key === tui.keys.teleport) {
1230 } else if (event.key === tui.keys.switch_to_portal) {
1231 event.preventDefault();
1232 tui.switch_mode('portal');
1233 } else if (event.key === tui.keys.switch_to_annotate) {
1234 event.preventDefault();
1235 tui.switch_mode('annotate');
1237 } else if (tui.mode.name == 'study') {
1238 if (tui.mode.mode_switch_on_key(event)) {
1240 } else if (event.key in tui.movement_keys) {
1241 explorer.move(tui.movement_keys[event.key]);
1242 } else if (event.key == tui.keys.toggle_map_mode) {
1243 if (tui.map_mode == 'terrain') {
1244 tui.map_mode = 'annotations';
1245 } else if (tui.map_mode == 'annotations') {
1246 tui.map_mode = 'control';
1248 tui.map_mode = 'terrain';
1251 } else if (tui.mode.name == 'control_tile_draw') {
1252 if (tui.mode.mode_switch_on_key(event)) {
1254 } else if (event.key in tui.movement_keys) {
1255 explorer.move(tui.movement_keys[event.key]);
1261 rows_selector.addEventListener('input', function() {
1262 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1265 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1266 terminal.initialize();
1269 cols_selector.addEventListener('input', function() {
1270 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1273 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1274 terminal.initialize();
1275 tui.window_width = terminal.cols / 2,
1278 for (let key_selector of key_selectors) {
1279 key_selector.addEventListener('input', function() {
1280 window.localStorage.setItem(key_selector.id, key_selector.value);
1284 window.setInterval(function() {
1285 if (server.connected) {
1286 server.send(['PING']);
1288 server.reconnect_to(server.url);
1289 tui.log_msg('@ attempting reconnect …')
1292 document.getElementById("terminal").onclick = function() {
1293 tui.inputEl.focus();
1295 document.getElementById("help").onclick = function() {
1296 tui.show_help = true;
1299 document.getElementById("switch_to_play").onclick = function() {
1300 tui.switch_mode('play');
1303 document.getElementById("switch_to_study").onclick = function() {
1304 tui.switch_mode('study');
1307 document.getElementById("switch_to_chat").onclick = function() {
1308 tui.switch_mode('chat');
1311 document.getElementById("switch_to_password").onclick = function() {
1312 tui.switch_mode('password');
1315 document.getElementById("switch_to_edit").onclick = function() {
1316 tui.switch_mode('edit');
1319 document.getElementById("switch_to_annotate").onclick = function() {
1320 tui.switch_mode('annotate');
1323 document.getElementById("switch_to_portal").onclick = function() {
1324 tui.switch_mode('portal');
1327 document.getElementById("switch_to_admin").onclick = function() {
1328 tui.switch_mode('admin');
1331 document.getElementById("switch_to_control_pw_type").onclick = function() {
1332 tui.switch_mode('control_pw_type');
1335 document.getElementById("switch_to_control_tile_type").onclick = function() {
1336 tui.switch_mode('control_tile_type');
1339 document.getElementById("toggle_map_mode").onclick = function() {
1340 if (tui.map_mode == 'terrain') {
1341 tui.map_mode = 'annotations';
1342 } else if (tui.map_mode == 'annotations') {
1343 tui.map_mode = 'control';
1345 tui.map_mode = 'terrain';
1349 document.getElementById("take_thing").onclick = function() {
1350 server.send(['TASK:PICK_UP']);
1352 document.getElementById("drop_thing").onclick = function() {
1353 server.send(['TASK:DROP']);
1355 document.getElementById("flatten").onclick = function() {
1356 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1358 document.getElementById("teleport").onclick = function() {
1361 document.getElementById("move_upleft").onclick = function() {
1362 if (tui.mode.name == 'play') {
1363 server.send(['TASK:MOVE', 'UPLEFT']);
1365 explorer.move('UPLEFT');
1368 document.getElementById("move_left").onclick = function() {
1369 if (tui.mode.name == 'play') {
1370 server.send(['TASK:MOVE', 'LEFT']);
1372 explorer.move('LEFT');
1375 document.getElementById("move_downleft").onclick = function() {
1376 if (tui.mode.name == 'play') {
1377 server.send(['TASK:MOVE', 'DOWNLEFT']);
1379 explorer.move('DOWNLEFT');
1382 document.getElementById("move_down").onclick = function() {
1383 if (tui.mode.name == 'play') {
1384 server.send(['TASK:MOVE', 'DOWN']);
1386 explorer.move('DOWN');
1389 document.getElementById("move_up").onclick = function() {
1390 if (tui.mode.name == 'play') {
1391 server.send(['TASK:MOVE', 'UP']);
1393 explorer.move('UP');
1396 document.getElementById("move_upright").onclick = function() {
1397 if (tui.mode.name == 'play') {
1398 server.send(['TASK:MOVE', 'UPRIGHT']);
1400 explorer.move('UPRIGHT');
1403 document.getElementById("move_right").onclick = function() {
1404 if (tui.mode.name == 'play') {
1405 server.send(['TASK:MOVE', 'RIGHT']);
1407 explorer.move('RIGHT');
1410 document.getElementById("move_downright").onclick = function() {
1411 if (tui.mode.name == 'play') {
1412 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1414 explorer.move('DOWNRIGHT');