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>
35 <button id="switch_to_control_tile_type">change tiles control</button>
37 <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 />
39 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
40 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
41 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
42 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
43 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
44 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
45 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
46 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
47 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
48 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
49 <li>help: <input id="key_help" type="text" value="h" />
50 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
51 <li>teleport: <input id="key_teleport" type="text" value="p" />
52 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
53 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
54 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
55 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
56 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
57 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
58 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
59 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
60 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw_type" type="text" value="C" />
61 <li>change tiles control (from play mode): <input id="key_switch_to_control_tile_type" type="text" value="Q" />
62 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
63 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
64 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
69 let websocket_location = "wss://plomlompom.com/rogue_chat/";
70 //let websocket_location = "ws://localhost:8000/";
75 'long': 'This mode allows you to interact with the map.'
79 '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.'},
81 'short': 'terrain edit',
82 '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.'
85 'short': 'change tile control password',
86 '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!'
89 'short': 'change tile control password',
90 '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.'
92 'control_tile_type': {
93 'short': 'change tiles control',
94 '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.'
96 'control_tile_draw': {
97 'short': 'change tiles control',
98 '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'
101 'short': 'annotation',
102 '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.'
105 'short': 'edit portal',
106 '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.'
109 'short': 'chat mode',
110 '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:'
114 'long': 'Pick your player name.'
116 'waiting_for_server': {
117 'short': 'waiting for server response',
118 'long': 'Waiting for a server response.'
121 'short': 'waiting for server response',
122 'long': 'Waiting for a server response.'
125 'short': 'map edit password',
126 '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.'
129 'short': 'become admin',
130 'long': 'This mode allows you to become admin if you know an admin password.'
134 let rows_selector = document.getElementById("n_rows");
135 let cols_selector = document.getElementById("n_cols");
136 let key_selectors = document.querySelectorAll('[id^="key_"]');
138 function restore_selector_value(selector) {
139 let stored_selection = window.localStorage.getItem(selector.id);
140 if (stored_selection) {
141 selector.value = stored_selection;
144 restore_selector_value(rows_selector);
145 restore_selector_value(cols_selector);
146 for (let key_selector of key_selectors) {
147 restore_selector_value(key_selector);
153 initialize: function() {
154 this.rows = rows_selector.value;
155 this.cols = cols_selector.value;
156 this.pre_el = document.getElementById("terminal");
157 this.pre_el.style.color = this.foreground;
158 this.pre_el.style.backgroundColor = this.background;
161 for (let y = 0, x = 0; y <= this.rows; x++) {
162 if (x == this.cols) {
165 this.content.push(line);
167 if (y == this.rows) {
174 blink_screen: function() {
175 this.pre_el.style.color = this.background;
176 this.pre_el.style.backgroundColor = this.foreground;
178 this.pre_el.style.color = this.foreground;
179 this.pre_el.style.backgroundColor = this.background;
182 refresh: function() {
183 function escapeHTML(str) {
185 replace(/&/g, '&').
186 replace(/</g, '<').
187 replace(/>/g, '>').
188 replace(/'/g, ''').
189 replace(/"/g, '"');
191 let pre_content = '';
192 for (let y = 0; y < this.rows; y++) {
193 let line = this.content[y].join('');
195 if (y in tui.links) {
197 for (let span of tui.links[y]) {
198 chunks.push(escapeHTML(line.slice(start_x, span[0])));
199 chunks.push('<a href="');
200 chunks.push(escapeHTML(span[2]));
202 chunks.push(escapeHTML(line.slice(span[0], span[1])));
206 chunks.push(escapeHTML(line.slice(start_x)));
208 chunks = [escapeHTML(line)];
210 for (const chunk of chunks) {
211 pre_content += chunk;
215 this.pre_el.innerHTML = pre_content;
217 write: function(start_y, start_x, msg) {
218 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
219 this.content[start_y][x] = msg[i];
222 drawBox: function(start_y, start_x, height, width) {
223 let end_y = start_y + height;
224 let end_x = start_x + width;
225 for (let y = start_y, x = start_x; y < this.rows; x++) {
233 this.content[y][x] = ' ';
237 terminal.initialize();
240 tokenize: function(str) {
245 for (let i = 0; i < str.length; i++) {
251 } else if (c == '\\') {
253 } else if (c == '"') {
258 } else if (c == '"') {
260 } else if (c === ' ') {
261 if (token.length > 0) {
269 if (token.length > 0) {
274 parse_yx: function(position_string) {
275 let coordinate_strings = position_string.split(',')
276 let position = [0, 0];
277 position[0] = parseInt(coordinate_strings[0].slice(2));
278 position[1] = parseInt(coordinate_strings[1].slice(2));
290 init: function(url) {
292 this.websocket = new WebSocket(this.url);
293 this.websocket.onopen = function(event) {
294 server.connected = true;
295 game.thing_types = {};
297 server.send(['TASKS']);
298 server.send(['TERRAINS']);
299 server.send(['THING_TYPES']);
300 tui.log_msg("@ server connected! :)");
301 tui.switch_mode('login');
303 this.websocket.onclose = function(event) {
304 server.connected = false;
305 tui.switch_mode('waiting_for_server');
306 tui.log_msg("@ server disconnected :(");
308 this.websocket.onmessage = this.handle_event;
310 reconnect_to: function(url) {
311 this.websocket.close();
314 send: function(tokens) {
315 this.websocket.send(unparser.untokenize(tokens));
317 handle_event: function(event) {
318 let tokens = parser.tokenize(event.data);
319 if (tokens[0] === 'TURN') {
320 game.turn_complete = false;
321 explorer.empty_info_db();
324 game.turn = parseInt(tokens[1]);
325 } else if (tokens[0] === 'THING') {
326 let t = game.get_thing(tokens[3], true);
327 t.position = parser.parse_yx(tokens[1]);
329 } else if (tokens[0] === 'THING_NAME') {
330 let t = game.get_thing(tokens[1], false);
334 } else if (tokens[0] === 'THING_CHAR') {
335 let t = game.get_thing(tokens[1], false);
337 t.player_char = tokens[2];
339 } else if (tokens[0] === 'TASKS') {
340 game.tasks = tokens[1].split(',');
341 tui.mode_edit.legal = game.tasks.includes('WRITE');
342 } else if (tokens[0] === 'THING_TYPE') {
343 game.thing_types[tokens[1]] = tokens[2]
344 } else if (tokens[0] === 'TERRAIN') {
345 game.terrains[tokens[1]] = tokens[2]
346 } else if (tokens[0] === 'MAP') {
347 game.map_geometry = tokens[1];
349 game.map_size = parser.parse_yx(tokens[2]);
351 } else if (tokens[0] === 'FOV') {
353 } else if (tokens[0] === 'MAP_CONTROL') {
354 game.map_control = tokens[1]
355 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
356 game.turn_complete = true;
357 if (tui.mode.name == 'post_login_wait') {
358 tui.switch_mode('play');
359 } else if (tui.mode.name == 'study') {
360 explorer.query_info();
363 } else if (tokens[0] === 'CHAT') {
364 tui.log_msg('# ' + tokens[1], 1);
365 } else if (tokens[0] === 'PLAYER_ID') {
366 game.player_id = parseInt(tokens[1]);
367 } else if (tokens[0] === 'LOGIN_OK') {
368 this.send(['GET_GAMESTATE']);
369 tui.switch_mode('post_login_wait');
370 } else if (tokens[0] === 'PORTAL') {
371 let position = parser.parse_yx(tokens[1]);
372 game.portals[position] = tokens[2];
373 } else if (tokens[0] === 'ANNOTATION_HINT') {
374 let position = parser.parse_yx(tokens[1]);
375 explorer.info_hints = explorer.info_hints.concat([position]);
376 } else if (tokens[0] === 'ANNOTATION') {
377 let position = parser.parse_yx(tokens[1]);
378 explorer.update_info_db(position, tokens[2]);
379 tui.restore_input_values();
381 } else if (tokens[0] === 'UNHANDLED_INPUT') {
382 tui.log_msg('? unknown command');
383 } else if (tokens[0] === 'PLAY_ERROR') {
384 tui.log_msg('? ' + tokens[1]);
385 terminal.blink_screen();
386 } else if (tokens[0] === 'ARGUMENT_ERROR') {
387 tui.log_msg('? syntax error: ' + tokens[1]);
388 } else if (tokens[0] === 'GAME_ERROR') {
389 tui.log_msg('? game error: ' + tokens[1]);
390 } else if (tokens[0] === 'PONG') {
393 tui.log_msg('? unhandled input: ' + event.data);
399 quote: function(str) {
401 for (let i = 0; i < str.length; i++) {
403 if (['"', '\\'].includes(c)) {
409 return quoted.join('');
411 to_yx: function(yx_coordinate) {
412 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
414 untokenize: function(tokens) {
415 let quoted_tokens = [];
416 for (let token of tokens) {
417 quoted_tokens.push(this.quote(token));
419 return quoted_tokens.join(" ");
424 constructor(name, has_input_prompt=false, shows_info=false,
425 is_intro=false, is_single_char_entry=false) {
427 this.short_desc = mode_helps[name].short;
428 this.available_modes = [];
429 this.has_input_prompt = has_input_prompt;
430 this.shows_info= shows_info;
431 this.is_intro = is_intro;
432 this.help_intro = mode_helps[name].long;
433 this.is_single_char_entry = is_single_char_entry;
436 *iter_available_modes() {
437 for (let mode_name of this.available_modes) {
438 let mode = tui['mode_' + mode_name];
442 let key = tui.keys['switch_to_' + mode.name];
446 list_available_modes() {
448 if (this.available_modes.length > 0) {
449 msg += 'Other modes available from here:\n';
450 for (let [mode, key] of this.iter_available_modes()) {
451 msg += '[' + key + '] – ' + mode.short_desc + '\n';
456 mode_switch_on_key(key_event) {
457 for (let [mode, key] of this.iter_available_modes()) {
458 if (key_event.key == key) {
459 event.preventDefault();
460 tui.switch_mode(mode.name);
472 window_width: terminal.cols / 2,
478 mode_waiting_for_server: new Mode('waiting_for_server',
480 mode_login: new Mode('login', true, false, true),
481 mode_post_login_wait: new Mode('post_login_wait'),
482 mode_chat: new Mode('chat', true),
483 mode_annotate: new Mode('annotate', true, true),
484 mode_play: new Mode('play'),
485 mode_study: new Mode('study', false, true),
486 mode_edit: new Mode('edit', false, false, false, true),
487 mode_control_pw_type: new Mode('control_pw_type',
488 false, false, false, true),
489 mode_portal: new Mode('portal', true, true),
490 mode_password: new Mode('password', true),
491 mode_admin: new Mode('admin', true),
492 mode_control_pw_pw: new Mode('control_pw_pw', true),
493 mode_control_tile_type: new Mode('control_tile_type',
494 false, false, false, true),
495 mode_control_tile_draw: new Mode('control_tile_draw'),
497 this.mode_play.available_modes = ["chat", "study", "edit",
498 "annotate", "portal",
502 this.mode_study.available_modes = ["chat", "play"]
503 this.mode_control_tile_draw.available_modes = ["play"]
504 this.mode = this.mode_waiting_for_server;
505 this.inputEl = document.getElementById("input");
506 this.inputEl.focus();
507 this.recalc_input_lines();
508 this.height_header = this.height_turn_line + this.height_mode_line;
509 this.log_msg("@ waiting for server connection ...");
512 init_keys: function() {
514 for (let key_selector of key_selectors) {
515 this.keys[key_selector.id.slice(4)] = key_selector.value;
517 this.movement_keys = {
518 [this.keys.square_move_up]: 'UP',
519 [this.keys.square_move_left]: 'LEFT',
520 [this.keys.square_move_down]: 'DOWN',
521 [this.keys.square_move_right]: 'RIGHT'
523 if (game.map_geometry == 'Hex') {
524 this.movement_keys = {
525 [this.keys.hex_move_upleft]: 'UPLEFT',
526 [this.keys.hex_move_upright]: 'UPRIGHT',
527 [this.keys.hex_move_right]: 'RIGHT',
528 [this.keys.hex_move_downright]: 'DOWNRIGHT',
529 [this.keys.hex_move_downleft]: 'DOWNLEFT',
530 [this.keys.hex_move_left]: 'LEFT'
534 switch_mode: function(mode_name) {
535 this.inputEl.focus();
536 this.map_mode = 'terrain';
537 this.mode = this['mode_' + mode_name];
538 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
539 explorer.position = game.things[game.player_id].position;
540 if (this.mode.shows_info) {
541 explorer.query_info();
542 } else if (this.mode.name == 'control_tile_draw') {
543 explorer.send_tile_control_command();
544 this.map_mode = 'control';
548 this.restore_input_values();
549 document.getElementById("take_thing").disabled = true;
550 document.getElementById("drop_thing").disabled = true;
551 document.getElementById("flatten").disabled = true;
552 document.getElementById("teleport").disabled = true;
553 document.getElementById("toggle_map_mode").disabled = true;
554 document.getElementById("switch_to_chat").disabled = true;
555 document.getElementById("switch_to_play").disabled = true;
556 document.getElementById("switch_to_study").disabled = true;
557 document.getElementById("switch_to_edit").disabled = true;
558 document.getElementById("switch_to_portal").disabled = true;
559 document.getElementById("switch_to_annotate").disabled = true;
560 document.getElementById("switch_to_password").disabled = true;
561 document.getElementById("switch_to_admin").disabled = true;
562 document.getElementById("switch_to_control_pw_type").disabled = true;
563 document.getElementById("switch_to_control_tile_type").disabled = true;
564 document.getElementById("move_left").disabled = true;
565 document.getElementById("move_upleft").disabled = true;
566 document.getElementById("move_up").disabled = true;
567 document.getElementById("move_upright").disabled = true;
568 document.getElementById("move_downleft").disabled = true;
569 document.getElementById("move_down").disabled = true;
570 document.getElementById("move_downright").disabled = true;
571 document.getElementById("move_right").disabled = true;
572 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
573 document.getElementById("move_left").disabled = false;
574 document.getElementById("move_right").disabled = false;
575 if (game.map_geometry == 'Hex') {
576 document.getElementById("move_upleft").disabled = false;
577 document.getElementById("move_upright").disabled = false;
578 document.getElementById("move_downleft").disabled = false;
579 document.getElementById("move_downright").disabled = false;
581 document.getElementById("move_up").disabled = false;
582 document.getElementById("move_down").disabled = false;
585 if (!this.mode.is_intro && this.mode.name != 'play') {
586 document.getElementById("switch_to_play").disabled = false;
588 if (!this.mode.is_intro && this.mode.name != 'study') {
589 document.getElementById("switch_to_study").disabled = false;
591 if (!this.mode.is_intro && this.mode.name != 'chat') {
592 document.getElementById("switch_to_chat").disabled = false;
594 if (this.mode.name == 'login') {
595 if (this.login_name) {
596 server.send(['LOGIN', this.login_name]);
598 this.log_msg("? need login name");
600 } else if (this.mode.name == 'play') {
601 if (game.tasks.includes('PICK_UP')) {
602 document.getElementById("take_thing").disabled = false;
604 if (game.tasks.includes('DROP')) {
605 document.getElementById("drop_thing").disabled = false;
607 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
608 document.getElementById("flatten").disabled = false;
610 if (game.tasks.includes('MOVE')) {
612 document.getElementById("teleport").disabled = false;
613 document.getElementById("switch_to_annotate").disabled = false;
614 document.getElementById("switch_to_edit").disabled = false;
615 document.getElementById("switch_to_portal").disabled = false;
616 document.getElementById("switch_to_password").disabled = false;
617 document.getElementById("switch_to_admin").disabled = false;
618 document.getElementById("switch_to_control_pw_type").disabled = false;
619 document.getElementById("switch_to_control_tile_type").disabled = false;
620 } else if (this.mode.name == 'study') {
621 document.getElementById("toggle_map_mode").disabled = false;
622 } else if (this.mode.is_single_char_entry) {
623 this.show_help = true;
624 } else if (this.mode.name == 'admin') {
625 this.log_msg('@ enter admin password:')
626 } else if (this.mode.name == 'control_pw_pw') {
627 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
628 } else if (this.mode.name == 'control_pw_pw') {
629 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
633 offset_links: function(offset, links) {
634 for (let y in links) {
635 let real_y = offset[0] + parseInt(y);
636 if (!this.links[real_y]) {
637 this.links[real_y] = [];
639 for (let link of links[y]) {
640 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
641 this.links[real_y].push(offset_link);
645 restore_input_values: function() {
646 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
647 let info = explorer.info_db[explorer.position];
648 if (info != "(none)") {
649 this.inputEl.value = info;
650 this.recalc_input_lines();
652 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
653 let portal = game.portals[explorer.position]
654 this.inputEl.value = portal;
655 this.recalc_input_lines();
656 } else if (this.mode.name == 'password') {
657 this.inputEl.value = this.password;
658 this.recalc_input_lines();
661 empty_input: function(str) {
662 this.inputEl.value = "";
663 if (this.mode.has_input_prompt) {
664 this.recalc_input_lines();
666 this.height_input = 0;
669 recalc_input_lines: function() {
671 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
672 this.height_input = this.input_lines.length;
674 msg_into_lines_of_width: function(msg, width) {
675 function push_inner_link(y, end_x) {
676 if (!inner_links[y]) {
679 inner_links[y].push([url_start_x, end_x, url]);
681 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
684 for (const match of matches) {
685 const url = match[0];
686 const url_start = match.index;
687 const url_end = match.index + match[0].length;
688 link_data[url_start] = url;
689 url_ends.push(url_end);
693 let inner_links = {};
697 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
698 if (x >= width || msg[i] == "\n") {
700 push_inner_link(y, chunk.length);
706 if (msg[i] == "\n") {
711 if (msg[i] != "\n") {
714 if (i in link_data) {
718 } else if (url_ends.includes(i)) {
719 push_inner_link(y, x);
725 push_inner_link(lines.length - 1, chunk.length);
727 return [lines, inner_links];
729 log_msg: function(msg) {
731 while (this.log.length > 100) {
736 draw_map: function() {
737 let map_lines_split = [];
739 let map_content = game.map;
740 if (this.map_mode == 'control') {
741 map_content = game.map_control;
743 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
744 if (j == game.map_size[1]) {
745 map_lines_split.push(line);
749 line.push(map_content[i] + ' ');
751 map_lines_split.push(line);
752 if (this.map_mode == 'annotations') {
753 for (const coordinate of explorer.info_hints) {
754 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
756 } else if (this.map_mode == 'terrain') {
757 for (const p in game.portals) {
758 let coordinate = p.split(',')
759 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
761 let used_positions = [];
762 for (const thing_id in game.things) {
763 let t = game.things[thing_id];
764 let symbol = game.thing_types[t.type_];
767 meta_char = t.player_char;
769 if (used_positions.includes(t.position.toString())) {
772 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
773 used_positions.push(t.position.toString());
776 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
777 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
780 if (game.map_geometry == 'Square') {
781 for (let line_split of map_lines_split) {
782 map_lines.push(line_split.join(''));
784 } else if (game.map_geometry == 'Hex') {
786 for (let line_split of map_lines_split) {
787 map_lines.push(' '.repeat(indent) + line_split.join(''));
795 let window_center = [terminal.rows / 2, this.window_width / 2];
796 let player = game.things[game.player_id];
797 let center_position = [player.position[0], player.position[1]];
798 if (tui.mode.shows_info) {
799 center_position = [explorer.position[0], explorer.position[1]];
801 center_position[1] = center_position[1] * 2;
802 let offset = [center_position[0] - window_center[0],
803 center_position[1] - window_center[1]]
804 if (game.map_geometry == 'Hex' && offset[0] % 2) {
807 let term_y = Math.max(0, -offset[0]);
808 let term_x = Math.max(0, -offset[1]);
809 let map_y = Math.max(0, offset[0]);
810 let map_x = Math.max(0, offset[1]);
811 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
812 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
813 terminal.write(term_y, term_x, to_draw);
816 draw_mode_line: function() {
817 let help = 'hit [' + this.keys.help + '] for help';
818 if (this.mode.has_input_prompt) {
819 help = 'enter /help for help';
821 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
823 draw_turn_line: function(n) {
824 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
826 draw_history: function() {
827 let log_display_lines = [];
829 let y_offset_in_log = 0;
830 for (let line of this.log) {
831 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
833 log_display_lines = log_display_lines.concat(new_lines);
834 for (const y in link_data) {
835 const rel_y = y_offset_in_log + parseInt(y);
836 log_links[rel_y] = [];
837 for (let link of link_data[y]) {
838 log_links[rel_y].push(link);
841 y_offset_in_log += new_lines.length;
843 let i = log_display_lines.length - 1;
844 for (let y = terminal.rows - 1 - this.height_input;
845 y >= this.height_header && i >= 0;
847 terminal.write(y, this.window_width, log_display_lines[i]);
849 for (const key of Object.keys(log_links)) {
850 if (parseInt(key) <= i) {
851 delete log_links[key];
854 let offset = [terminal.rows - this.height_input - log_display_lines.length,
856 this.offset_links(offset, log_links);
858 draw_info: function() {
859 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
861 let offset = [this.height_header, this.window_width];
862 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
863 terminal.write(y, offset[1], lines[i]);
865 this.offset_links(offset, link_data);
867 draw_input: function() {
868 if (this.mode.has_input_prompt) {
869 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
870 terminal.write(y, this.window_width, this.input_lines[i]);
874 draw_help: function() {
875 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
876 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
877 if (this.mode.name == 'play') {
878 content += "Available actions:\n";
879 if (game.tasks.includes('MOVE')) {
880 content += "[" + movement_keys_desc + "] – move player\n";
882 if (game.tasks.includes('PICK_UP')) {
883 content += "[" + this.keys.take_thing + "] – take thing under player\n";
885 if (game.tasks.includes('DROP')) {
886 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
888 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
889 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
891 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
893 } else if (this.mode.name == 'study') {
894 content += "Available actions:\n";
895 content += '[' + movement_keys_desc + '] – move question mark\n';
896 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
898 } else if (this.mode.name == 'chat') {
899 content += '/nick NAME – re-name yourself to NAME\n';
900 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
901 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
903 content += this.mode.list_available_modes();
905 if (!this.mode.has_input_prompt) {
906 start_x = this.window_width
908 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
909 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
910 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
911 terminal.write(y, start_x, lines[i]);
914 full_refresh: function() {
916 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
917 if (this.mode.is_intro) {
921 if (game.turn_complete) {
923 this.draw_turn_line();
925 this.draw_mode_line();
926 if (this.mode.shows_info) {
933 if (this.show_help) {
945 this.map_control = "";
946 this.map_size = [0,0];
951 get_thing: function(id_, create_if_not_found=false) {
952 if (id_ in game.things) {
953 return game.things[id_];
954 } else if (create_if_not_found) {
955 let t = new Thing([0,0]);
956 game.things[id_] = t;
960 move: function(start_position, direction) {
961 let target = [start_position[0], start_position[1]];
962 if (direction == 'LEFT') {
964 } else if (direction == 'RIGHT') {
966 } else if (game.map_geometry == 'Square') {
967 if (direction == 'UP') {
969 } else if (direction == 'DOWN') {
972 } else if (game.map_geometry == 'Hex') {
973 let start_indented = start_position[0] % 2;
974 if (direction == 'UPLEFT') {
976 if (!start_indented) {
979 } else if (direction == 'UPRIGHT') {
981 if (start_indented) {
984 } else if (direction == 'DOWNLEFT') {
986 if (!start_indented) {
989 } else if (direction == 'DOWNRIGHT') {
991 if (start_indented) {
996 if (target[0] < 0 || target[1] < 0 ||
997 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1002 teleport: function() {
1003 let player = this.get_thing(game.player_id);
1004 if (player.position in this.portals) {
1005 server.reconnect_to(this.portals[player.position]);
1007 terminal.blink_screen();
1008 tui.log_msg('? not standing on portal')
1016 server.init(websocket_location);
1022 move: function(direction) {
1023 let target = game.move(this.position, direction);
1025 this.position = target
1026 if (tui.mode.shows_info) {
1028 } else if (tui.mode.name == 'control_tile_draw') {
1029 this.send_tile_control_command();
1032 terminal.blink_screen();
1035 update_info_db: function(yx, str) {
1036 this.info_db[yx] = str;
1037 if (tui.mode.name == 'study') {
1041 empty_info_db: function() {
1043 this.info_hints = [];
1044 if (tui.mode.name == 'study') {
1048 query_info: function() {
1049 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1051 get_info: function() {
1052 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1053 if (game.fov[position_i] != '.') {
1054 return 'outside field of view';
1057 let terrain_char = game.map[position_i]
1058 let terrain_desc = '?'
1059 if (game.terrains[terrain_char]) {
1060 terrain_desc = game.terrains[terrain_char];
1062 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1063 let protection = game.map_control[position_i];
1064 if (protection == '.') {
1065 protection = 'unprotected';
1067 info += 'PROTECTION: ' + protection + '\n';
1068 for (let t_id in game.things) {
1069 let t = game.things[t_id];
1070 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1071 let symbol = game.thing_types[t.type_];
1072 info += "THING: " + t.type_ + " / " + symbol;
1073 if (t.player_char) {
1074 info += t.player_char;
1077 info += " (" + t.name_ + ")";
1082 if (this.position in game.portals) {
1083 info += "PORTAL: " + game.portals[this.position] + "\n";
1085 if (this.position in this.info_db) {
1086 info += "ANNOTATIONS: " + this.info_db[this.position];
1088 info += 'waiting …';
1092 annotate: function(msg) {
1093 if (msg.length == 0) {
1094 msg = " "; // triggers annotation deletion
1096 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1098 set_portal: function(msg) {
1099 if (msg.length == 0) {
1100 msg = " "; // triggers portal deletion
1102 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1104 send_tile_control_command: function() {
1105 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1109 tui.inputEl.addEventListener('input', (event) => {
1110 if (tui.mode.has_input_prompt) {
1111 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1112 if (tui.inputEl.value.length > max_length) {
1113 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1115 tui.recalc_input_lines();
1116 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1117 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1118 tui.switch_mode('play');
1119 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1120 tui.tile_control_char = tui.inputEl.value[0];
1121 tui.switch_mode('control_pw_pw');
1122 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1123 tui.tile_control_char = tui.inputEl.value[0];
1124 tui.switch_mode('control_tile_draw');
1128 tui.inputEl.addEventListener('keydown', (event) => {
1129 tui.show_help = false;
1130 if (event.key == 'Enter') {
1131 event.preventDefault();
1133 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1134 tui.show_help = true;
1136 tui.restore_input_values();
1137 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1138 && !tui.mode.is_single_char_entry) {
1139 tui.show_help = true;
1140 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1141 tui.login_name = tui.inputEl.value;
1142 server.send(['LOGIN', tui.inputEl.value]);
1144 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1145 if (tui.inputEl.value.length == 0) {
1146 tui.log_msg('@ aborted');
1148 server.send(['SET_MAP_CONTROL_PASSWORD',
1149 tui.tile_control_char, tui.inputEl.value]);
1151 tui.switch_mode('play');
1152 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1153 explorer.set_portal(tui.inputEl.value);
1154 tui.switch_mode('play');
1155 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1156 explorer.annotate(tui.inputEl.value);
1157 tui.switch_mode('play');
1158 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1159 if (tui.inputEl.value.length == 0) {
1160 tui.inputEl.value = " ";
1162 tui.password = tui.inputEl.value
1163 tui.switch_mode('play');
1164 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1165 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1166 tui.switch_mode('play');
1167 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1168 let tokens = parser.tokenize(tui.inputEl.value);
1169 if (tokens.length > 0 && tokens[0].length > 0) {
1170 if (tui.inputEl.value[0][0] == '/') {
1171 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1172 tui.switch_mode('play');
1173 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1174 tui.switch_mode('study');
1175 } else if (tokens[0].slice(1) == 'nick') {
1176 if (tokens.length > 1) {
1177 server.send(['NICK', tokens[1]]);
1179 tui.log_msg('? need new name');
1182 tui.log_msg('? unknown command');
1185 server.send(['ALL', tui.inputEl.value]);
1187 } else if (tui.inputEl.valuelength > 0) {
1188 server.send(['ALL', tui.inputEl.value]);
1191 } else if (tui.mode.name == 'play') {
1192 if (tui.mode.mode_switch_on_key(event)) {
1194 } else if (event.key === tui.keys.flatten
1195 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1196 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1197 } else if (event.key === tui.keys.take_thing
1198 && game.tasks.includes('PICK_UP')) {
1199 server.send(["TASK:PICK_UP"]);
1200 } else if (event.key === tui.keys.drop_thing
1201 && game.tasks.includes('DROP')) {
1202 server.send(["TASK:DROP"]);
1203 } else if (event.key in tui.movement_keys
1204 && game.tasks.includes('MOVE')) {
1205 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1206 } else if (event.key === tui.keys.teleport) {
1208 } else if (event.key === tui.keys.switch_to_portal) {
1209 event.preventDefault();
1210 tui.switch_mode('portal');
1211 } else if (event.key === tui.keys.switch_to_annotate) {
1212 event.preventDefault();
1213 tui.switch_mode('annotate');
1215 } else if (tui.mode.name == 'study') {
1216 if (tui.mode.mode_switch_on_key(event)) {
1218 } else if (event.key in tui.movement_keys) {
1219 explorer.move(tui.movement_keys[event.key]);
1220 } else if (event.key == tui.keys.toggle_map_mode) {
1221 if (tui.map_mode == 'terrain') {
1222 tui.map_mode = 'annotations';
1223 } else if (tui.map_mode == 'annotations') {
1224 tui.map_mode = 'control';
1226 tui.map_mode = 'terrain';
1229 } else if (tui.mode.name == 'control_tile_draw') {
1230 if (tui.mode.mode_switch_on_key(event)) {
1232 } else if (event.key in tui.movement_keys) {
1233 explorer.move(tui.movement_keys[event.key]);
1239 rows_selector.addEventListener('input', function() {
1240 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1243 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1244 terminal.initialize();
1247 cols_selector.addEventListener('input', function() {
1248 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1251 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1252 terminal.initialize();
1253 tui.window_width = terminal.cols / 2,
1256 for (let key_selector of key_selectors) {
1257 key_selector.addEventListener('input', function() {
1258 window.localStorage.setItem(key_selector.id, key_selector.value);
1262 window.setInterval(function() {
1263 if (server.connected) {
1264 server.send(['PING']);
1266 server.reconnect_to(server.url);
1267 tui.log_msg('@ attempting reconnect …')
1270 document.getElementById("terminal").onclick = function() {
1271 tui.inputEl.focus();
1273 document.getElementById("help").onclick = function() {
1274 tui.show_help = true;
1277 document.getElementById("switch_to_play").onclick = function() {
1278 tui.switch_mode('play');
1281 document.getElementById("switch_to_study").onclick = function() {
1282 tui.switch_mode('study');
1285 document.getElementById("switch_to_chat").onclick = function() {
1286 tui.switch_mode('chat');
1289 document.getElementById("switch_to_password").onclick = function() {
1290 tui.switch_mode('password');
1293 document.getElementById("switch_to_edit").onclick = function() {
1294 tui.switch_mode('edit');
1297 document.getElementById("switch_to_annotate").onclick = function() {
1298 tui.switch_mode('annotate');
1301 document.getElementById("switch_to_portal").onclick = function() {
1302 tui.switch_mode('portal');
1305 document.getElementById("switch_to_admin").onclick = function() {
1306 tui.switch_mode('admin');
1309 document.getElementById("switch_to_control_pw_type").onclick = function() {
1310 tui.switch_mode('control_pw_type');
1313 document.getElementById("switch_to_control_tile_type").onclick = function() {
1314 tui.switch_mode('control_tile_type');
1317 document.getElementById("toggle_map_mode").onclick = function() {
1318 if (tui.map_mode == 'terrain') {
1319 tui.map_mode = 'annotations';
1320 } else if (tui.map_mode == 'annotations') {
1321 tui.map_mode = 'control';
1323 tui.map_mode = 'terrain';
1327 document.getElementById("take_thing").onclick = function() {
1328 server.send(['TASK:PICK_UP']);
1330 document.getElementById("drop_thing").onclick = function() {
1331 server.send(['TASK:DROP']);
1333 document.getElementById("flatten").onclick = function() {
1334 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1336 document.getElementById("teleport").onclick = function() {
1339 document.getElementById("move_upleft").onclick = function() {
1340 if (tui.mode.name == 'play') {
1341 server.send(['TASK:MOVE', 'UPLEFT']);
1343 explorer.move('UPLEFT');
1346 document.getElementById("move_left").onclick = function() {
1347 if (tui.mode.name == 'play') {
1348 server.send(['TASK:MOVE', 'LEFT']);
1350 explorer.move('LEFT');
1353 document.getElementById("move_downleft").onclick = function() {
1354 if (tui.mode.name == 'play') {
1355 server.send(['TASK:MOVE', 'DOWNLEFT']);
1357 explorer.move('DOWNLEFT');
1360 document.getElementById("move_down").onclick = function() {
1361 if (tui.mode.name == 'play') {
1362 server.send(['TASK:MOVE', 'DOWN']);
1364 explorer.move('DOWN');
1367 document.getElementById("move_up").onclick = function() {
1368 if (tui.mode.name == 'play') {
1369 server.send(['TASK:MOVE', 'UP']);
1371 explorer.move('UP');
1374 document.getElementById("move_upright").onclick = function() {
1375 if (tui.mode.name == 'play') {
1376 server.send(['TASK:MOVE', 'UPRIGHT']);
1378 explorer.move('UPRIGHT');
1381 document.getElementById("move_right").onclick = function() {
1382 if (tui.mode.name == 'play') {
1383 server.send(['TASK:MOVE', 'RIGHT']);
1385 explorer.move('RIGHT');
1388 document.getElementById("move_downright").onclick = function() {
1389 if (tui.mode.name == 'play') {
1390 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1392 explorer.move('DOWNRIGHT');