13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
16 <pre id="terminal"></pre>
17 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
19 <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal / curses client)
20 <h3>button controls for mouse players</h3>
21 <table style="float: left">
23 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
24 <td style="text-align: center"><button id="move_up">up</button></td>
25 <td><button id="move_upright">up-right</button></td>
28 <td style="text-align: right;"><button id="move_left">left</button></td>
29 <td stlye="text-align: center;">move</td>
30 <td><button id="move_right">right</button></td>
33 <td><button id="move_downleft">down-left</button></td>
34 <td style="text-align: center"><button id="move_down">down</button></td>
35 <td><button id="move_downright">down-right</button></td>
40 <td><button id="help">help</button></td>
43 <td><button id="switch_to_chat">chat mode</button><br /></td>
46 <td><button id="switch_to_study">study mode</button></td>
47 <td><button id="toggle_map_mode">toggle everything/terrain/annotations view</button>
50 <td><button id="switch_to_play">play mode</button></td>
52 <button id="take_thing">take thing</button>
53 <button id="teleport">teleport</button>
54 <button id="drop_thing">drop thing</button>
58 <td><button id="switch_to_edit">map edit mode</button></td>
60 <button id="switch_to_write">change tile</button>
61 <button id="flatten">flatten surroundings</button>
62 <button id="switch_to_password">change tile editing password</button>
63 <button id="switch_to_annotate">annotate tile</button>
64 <button id="switch_to_portal">edit portal link</button>
68 <td><button id="switch_to_admin_enter">admin mode</button></td>
70 <button id="switch_to_control_pw_type">change tile control password</button>
71 <button id="switch_to_control_tile_type">change tiles control</button>
72 <button id="toggle_tile_draw">toggle tiles control drawing</button>
77 <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 />
79 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
80 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
81 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
82 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
83 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
84 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
85 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
86 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
87 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
88 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
89 <li>help: <input id="key_help" type="text" value="h" />
90 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
91 <li>teleport: <input id="key_teleport" type="text" value="p" />
92 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
93 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
94 <li><input id="key_switch_to_chat" type="text" value="t" />
95 <li><input id="key_switch_to_play" type="text" value="p" />
96 <li><input id="key_switch_to_study" type="text" value="?" />
97 <li><input id="key_switch_to_edit" type="text" value="E" />
98 <li><input id="key_switch_to_write" type="text" value="m" />
99 <li><input id="key_switch_to_password" type="text" value="P" />
100 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
101 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
102 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
103 <li><input id="key_switch_to_annotate" type="text" value="M" />
104 <li><input id="key_switch_to_portal" type="text" value="T" />
105 <li>toggle everything/terrain/annotations view: <input id="key_toggle_map_mode" type="text" value="M" />
106 <li>toggle everything/terrain/annotations view: <input id="key_toggle_tile_draw" type="text" value="m" />
111 let websocket_location = "wss://plomlompom.com/rogue_chat/";
112 //let websocket_location = "ws://localhost:8000/";
117 'long': 'This mode allows you to interact with the map.'
121 '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.'},
124 'long': 'This mode allows you to change the map in various ways.'
127 'short': 'terrain write',
128 '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.'
131 'short': 'change tiles control password',
132 '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!'
135 'short': 'change tiles control password',
136 '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.'
138 'control_tile_type': {
139 'short': 'change tiles control',
140 '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.'
142 'control_tile_draw': {
143 'short': 'change tiles control',
144 'long': 'This mode is the second of two steps to change tile control areas on the map. Toggle tile control drawing on, then move cursor around the map to draw selected tile control character.'
147 'short': 'annotate tile',
148 '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.'
151 'short': 'edit portal',
152 '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.'
156 '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:'
160 'long': 'Pick your player name.'
162 'waiting_for_server': {
163 'short': 'waiting for server response',
164 'long': 'Waiting for a server response.'
167 'short': 'waiting for server response',
168 'long': 'Waiting for a server response.'
171 'short': 'map edit password',
172 '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.'
175 'short': 'become admin',
176 'long': 'This mode allows you to become admin if you know an admin password.'
180 'long': 'This mode allows you access to actions limited to administrators.'
184 let rows_selector = document.getElementById("n_rows");
185 let cols_selector = document.getElementById("n_cols");
186 let key_selectors = document.querySelectorAll('[id^="key_"]');
188 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
189 const action = key_switch_selector.id.slice("key_switch_to_".length);
190 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
193 function restore_selector_value(selector) {
194 let stored_selection = window.localStorage.getItem(selector.id);
195 if (stored_selection) {
196 selector.value = stored_selection;
199 restore_selector_value(rows_selector);
200 restore_selector_value(cols_selector);
201 for (let key_selector of key_selectors) {
202 restore_selector_value(key_selector);
208 initialize: function() {
209 this.rows = rows_selector.value;
210 this.cols = cols_selector.value;
211 this.pre_el = document.getElementById("terminal");
212 this.pre_el.style.color = this.foreground;
213 this.pre_el.style.backgroundColor = this.background;
216 for (let y = 0, x = 0; y <= this.rows; x++) {
217 if (x == this.cols) {
220 this.content.push(line);
222 if (y == this.rows) {
229 blink_screen: function() {
230 this.pre_el.style.color = this.background;
231 this.pre_el.style.backgroundColor = this.foreground;
233 this.pre_el.style.color = this.foreground;
234 this.pre_el.style.backgroundColor = this.background;
237 refresh: function() {
238 function escapeHTML(str) {
240 replace(/&/g, '&').
241 replace(/</g, '<').
242 replace(/>/g, '>').
243 replace(/'/g, ''').
244 replace(/"/g, '"');
246 let pre_content = '';
247 for (let y = 0; y < this.rows; y++) {
248 let line = this.content[y].join('');
250 if (y in tui.links) {
252 for (let span of tui.links[y]) {
253 chunks.push(escapeHTML(line.slice(start_x, span[0])));
254 chunks.push('<a href="');
255 chunks.push(escapeHTML(span[2]));
257 chunks.push(escapeHTML(line.slice(span[0], span[1])));
261 chunks.push(escapeHTML(line.slice(start_x)));
263 chunks = [escapeHTML(line)];
265 for (const chunk of chunks) {
266 pre_content += chunk;
270 this.pre_el.innerHTML = pre_content;
272 write: function(start_y, start_x, msg) {
273 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
274 this.content[start_y][x] = msg[i];
277 drawBox: function(start_y, start_x, height, width) {
278 let end_y = start_y + height;
279 let end_x = start_x + width;
280 for (let y = start_y, x = start_x; y < this.rows; x++) {
288 this.content[y][x] = ' ';
292 terminal.initialize();
295 tokenize: function(str) {
300 for (let i = 0; i < str.length; i++) {
306 } else if (c == '\\') {
308 } else if (c == '"') {
313 } else if (c == '"') {
315 } else if (c === ' ') {
316 if (token.length > 0) {
324 if (token.length > 0) {
329 parse_yx: function(position_string) {
330 let coordinate_strings = position_string.split(',')
331 let position = [0, 0];
332 position[0] = parseInt(coordinate_strings[0].slice(2));
333 position[1] = parseInt(coordinate_strings[1].slice(2));
345 init: function(url) {
347 this.websocket = new WebSocket(this.url);
348 this.websocket.onopen = function(event) {
349 server.connected = true;
350 game.thing_types = {};
352 server.send(['TASKS']);
353 server.send(['TERRAINS']);
354 server.send(['THING_TYPES']);
355 tui.log_msg("@ server connected! :)");
356 tui.switch_mode('login');
358 this.websocket.onclose = function(event) {
359 server.connected = false;
360 tui.switch_mode('waiting_for_server');
361 tui.log_msg("@ server disconnected :(");
363 this.websocket.onmessage = this.handle_event;
365 reconnect_to: function(url) {
366 this.websocket.close();
369 send: function(tokens) {
370 this.websocket.send(unparser.untokenize(tokens));
372 handle_event: function(event) {
373 let tokens = parser.tokenize(event.data);
374 if (tokens[0] === 'TURN') {
375 game.turn_complete = false;
376 explorer.empty_info_db();
379 game.turn = parseInt(tokens[1]);
380 } else if (tokens[0] === 'THING') {
381 let t = game.get_thing(tokens[3], true);
382 t.position = parser.parse_yx(tokens[1]);
384 } else if (tokens[0] === 'THING_NAME') {
385 let t = game.get_thing(tokens[1], false);
389 } else if (tokens[0] === 'THING_CHAR') {
390 let t = game.get_thing(tokens[1], false);
392 t.player_char = tokens[2];
394 } else if (tokens[0] === 'TASKS') {
395 game.tasks = tokens[1].split(',');
396 tui.mode_write.legal = game.tasks.includes('WRITE');
397 } else if (tokens[0] === 'THING_TYPE') {
398 game.thing_types[tokens[1]] = tokens[2]
399 } else if (tokens[0] === 'TERRAIN') {
400 game.terrains[tokens[1]] = tokens[2]
401 } else if (tokens[0] === 'MAP') {
402 game.map_geometry = tokens[1];
404 game.map_size = parser.parse_yx(tokens[2]);
406 } else if (tokens[0] === 'FOV') {
408 } else if (tokens[0] === 'MAP_CONTROL') {
409 game.map_control = tokens[1]
410 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
411 game.turn_complete = true;
412 if (tui.mode.name == 'post_login_wait') {
413 tui.switch_mode('play');
414 } else if (tui.mode.name == 'study') {
415 explorer.query_info();
418 } else if (tokens[0] === 'CHAT') {
419 tui.log_msg('# ' + tokens[1], 1);
420 } else if (tokens[0] === 'PLAYER_ID') {
421 game.player_id = parseInt(tokens[1]);
422 } else if (tokens[0] === 'LOGIN_OK') {
423 this.send(['GET_GAMESTATE']);
424 tui.switch_mode('post_login_wait');
425 } else if (tokens[0] === 'ADMIN_OK') {
427 tui.log_msg('@ you now have admin rights');
428 tui.switch_mode('admin');
429 } else if (tokens[0] === 'PORTAL') {
430 let position = parser.parse_yx(tokens[1]);
431 game.portals[position] = tokens[2];
432 } else if (tokens[0] === 'ANNOTATION_HINT') {
433 let position = parser.parse_yx(tokens[1]);
434 explorer.info_hints = explorer.info_hints.concat([position]);
435 } else if (tokens[0] === 'ANNOTATION') {
436 let position = parser.parse_yx(tokens[1]);
437 explorer.update_info_db(position, tokens[2]);
438 tui.restore_input_values();
440 } else if (tokens[0] === 'UNHANDLED_INPUT') {
441 tui.log_msg('? unknown command');
442 } else if (tokens[0] === 'PLAY_ERROR') {
443 tui.log_msg('? ' + tokens[1]);
444 terminal.blink_screen();
445 } else if (tokens[0] === 'ARGUMENT_ERROR') {
446 tui.log_msg('? syntax error: ' + tokens[1]);
447 } else if (tokens[0] === 'GAME_ERROR') {
448 tui.log_msg('? game error: ' + tokens[1]);
449 } else if (tokens[0] === 'PONG') {
452 tui.log_msg('? unhandled input: ' + event.data);
458 quote: function(str) {
460 for (let i = 0; i < str.length; i++) {
462 if (['"', '\\'].includes(c)) {
468 return quoted.join('');
470 to_yx: function(yx_coordinate) {
471 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
473 untokenize: function(tokens) {
474 let quoted_tokens = [];
475 for (let token of tokens) {
476 quoted_tokens.push(this.quote(token));
478 return quoted_tokens.join(" ");
483 constructor(name, has_input_prompt=false, shows_info=false,
484 is_intro=false, is_single_char_entry=false) {
486 this.short_desc = mode_helps[name].short;
487 this.available_modes = [];
488 this.has_input_prompt = has_input_prompt;
489 this.shows_info= shows_info;
490 this.is_intro = is_intro;
491 this.help_intro = mode_helps[name].long;
492 this.is_single_char_entry = is_single_char_entry;
495 *iter_available_modes() {
496 for (let mode_name of this.available_modes) {
497 let mode = tui['mode_' + mode_name];
501 let key = tui.keys['switch_to_' + mode.name];
505 list_available_modes() {
507 if (this.available_modes.length > 0) {
508 msg += 'Other modes available from here:\n';
509 for (let [mode, key] of this.iter_available_modes()) {
510 msg += '[' + key + '] – ' + mode.short_desc + '\n';
515 mode_switch_on_key(key_event) {
516 for (let [mode, key] of this.iter_available_modes()) {
517 if (key_event.key == key) {
518 event.preventDefault();
519 tui.switch_mode(mode.name);
531 window_width: terminal.cols / 2,
539 mode_waiting_for_server: new Mode('waiting_for_server',
541 mode_login: new Mode('login', true, false, true),
542 mode_post_login_wait: new Mode('post_login_wait'),
543 mode_chat: new Mode('chat', true),
544 mode_annotate: new Mode('annotate', true, true),
545 mode_play: new Mode('play'),
546 mode_study: new Mode('study', false, true),
547 mode_write: new Mode('write', false, false, false, true),
548 mode_edit: new Mode('edit'),
549 mode_control_pw_type: new Mode('control_pw_type',
550 false, false, false, true),
551 mode_portal: new Mode('portal', true, true),
552 mode_password: new Mode('password', true),
553 mode_admin_enter: new Mode('admin_enter', true),
554 mode_admin: new Mode('admin'),
555 mode_control_pw_pw: new Mode('control_pw_pw', true),
556 mode_control_tile_type: new Mode('control_tile_type',
557 false, false, false, true),
558 mode_control_tile_draw: new Mode('control_tile_draw'),
560 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
561 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
562 this.mode_admin.available_modes = ["control_pw_type",
563 "control_tile_type", "chat",
564 "study", "play", "edit"]
565 this.mode_control_tile_draw.available_modes = ["admin_enter"]
566 this.mode_edit.available_modes = ["write", "annotate", "portal",
567 "password", "chat", "study", "play",
569 this.mode = this.mode_waiting_for_server;
570 this.inputEl = document.getElementById("input");
571 this.inputEl.focus();
572 this.recalc_input_lines();
573 this.height_header = this.height_turn_line + this.height_mode_line;
574 this.log_msg("@ waiting for server connection ...");
577 init_keys: function() {
579 for (let key_selector of key_selectors) {
580 this.keys[key_selector.id.slice(4)] = key_selector.value;
582 if (game.map_geometry == 'Square') {
583 this.movement_keys = {
584 [this.keys.square_move_up]: 'UP',
585 [this.keys.square_move_left]: 'LEFT',
586 [this.keys.square_move_down]: 'DOWN',
587 [this.keys.square_move_right]: 'RIGHT'
589 document.getElementById("move_upright").hidden = true;
590 document.getElementById("move_upleft").hidden = true;
591 document.getElementById("move_downright").hidden = true;
592 document.getElementById("move_downleft").hidden = true;
593 document.getElementById("move_up").hidden = false;
594 document.getElementById("move_down").hidden = false;
595 } else if (game.map_geometry == 'Hex') {
596 document.getElementById("move_upright").hidden = false;
597 document.getElementById("move_upleft").hidden = false;
598 document.getElementById("move_downright").hidden = false;
599 document.getElementById("move_downleft").hidden = false;
600 document.getElementById("move_up").hidden = true;
601 document.getElementById("move_down").hidden = true;
602 this.movement_keys = {
603 [this.keys.hex_move_upleft]: 'UPLEFT',
604 [this.keys.hex_move_upright]: 'UPRIGHT',
605 [this.keys.hex_move_right]: 'RIGHT',
606 [this.keys.hex_move_downright]: 'DOWNRIGHT',
607 [this.keys.hex_move_downleft]: 'DOWNLEFT',
608 [this.keys.hex_move_left]: 'LEFT'
612 switch_mode: function(mode_name) {
613 this.inputEl.focus();
614 this.map_mode = 'all';
615 this.tile_draw = false;
616 if (mode_name == 'admin_enter' && this.is_admin) {
619 this.mode = this['mode_' + mode_name];
620 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
621 explorer.position = game.things[game.player_id].position;
622 if (this.mode.shows_info) {
623 explorer.query_info();
627 this.restore_input_values();
628 for (let el of document.getElementsByTagName("button")) {
631 document.getElementById("help").disabled = false;
632 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
633 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
634 move_key.disabled = false;
637 if (!this.mode.is_intro && this.mode.name != 'play') {
638 document.getElementById("switch_to_play").disabled = false;
640 if (!this.mode.is_intro && this.mode.name != 'study') {
641 document.getElementById("switch_to_study").disabled = false;
643 if (!this.mode.is_intro && this.mode.name != 'chat') {
644 document.getElementById("switch_to_chat").disabled = false;
646 if (!this.mode.is_intro && this.mode.name != 'edit') {
647 document.getElementById("switch_to_edit").disabled = false;
649 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
650 document.getElementById("switch_to_admin_enter").disabled = false;
652 if (this.mode.name == 'login') {
653 if (this.login_name) {
654 server.send(['LOGIN', this.login_name]);
656 this.log_msg("? need login name");
658 } else if (this.mode.name == 'play') {
659 if (game.tasks.includes('PICK_UP')) {
660 document.getElementById("take_thing").disabled = false;
662 if (game.tasks.includes('DROP')) {
663 document.getElementById("drop_thing").disabled = false;
665 if (game.tasks.includes('MOVE')) {
667 document.getElementById("teleport").disabled = false;
668 } else if (this.mode.name == 'edit') {
669 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
670 document.getElementById("flatten").disabled = false;
672 document.getElementById("switch_to_annotate").disabled = false;
673 document.getElementById("switch_to_write").disabled = false;
674 document.getElementById("switch_to_portal").disabled = false;
675 document.getElementById("switch_to_password").disabled = false;
676 } else if (this.mode.name == 'admin') {
677 document.getElementById("switch_to_control_pw_type").disabled = false;
678 document.getElementById("switch_to_control_tile_type").disabled = false;
679 } else if (this.mode.name == 'control_tile_draw') {
680 document.getElementById("toggle_tile_draw").disabled = false;
681 } else if (this.mode.name == 'study') {
682 document.getElementById("toggle_map_mode").disabled = false;
683 } else if (this.mode.is_single_char_entry) {
684 this.show_help = true;
685 } else if (this.mode.name == 'admin_enter') {
686 this.log_msg('@ enter admin password:')
687 } else if (this.mode.name == 'control_pw_pw') {
688 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
689 } else if (this.mode.name == 'control_pw_pw') {
690 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
694 offset_links: function(offset, links) {
695 for (let y in links) {
696 let real_y = offset[0] + parseInt(y);
697 if (!this.links[real_y]) {
698 this.links[real_y] = [];
700 for (let link of links[y]) {
701 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
702 this.links[real_y].push(offset_link);
706 restore_input_values: function() {
707 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
708 let info = explorer.info_db[explorer.position];
709 if (info != "(none)") {
710 this.inputEl.value = info;
711 this.recalc_input_lines();
713 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
714 let portal = game.portals[explorer.position]
715 this.inputEl.value = portal;
716 this.recalc_input_lines();
717 } else if (this.mode.name == 'password') {
718 this.inputEl.value = this.password;
719 this.recalc_input_lines();
722 empty_input: function(str) {
723 this.inputEl.value = "";
724 if (this.mode.has_input_prompt) {
725 this.recalc_input_lines();
727 this.height_input = 0;
730 recalc_input_lines: function() {
732 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
733 this.height_input = this.input_lines.length;
735 msg_into_lines_of_width: function(msg, width) {
736 function push_inner_link(y, end_x) {
737 if (!inner_links[y]) {
740 inner_links[y].push([url_start_x, end_x, url]);
742 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
745 for (const match of matches) {
746 const url = match[0];
747 const url_start = match.index;
748 const url_end = match.index + match[0].length;
749 link_data[url_start] = url;
750 url_ends.push(url_end);
754 let inner_links = {};
758 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
759 if (x >= width || msg[i] == "\n") {
761 push_inner_link(y, chunk.length);
767 if (msg[i] == "\n") {
772 if (msg[i] != "\n") {
775 if (i in link_data) {
779 } else if (url_ends.includes(i)) {
780 push_inner_link(y, x);
786 push_inner_link(lines.length - 1, chunk.length);
788 return [lines, inner_links];
790 log_msg: function(msg) {
792 while (this.log.length > 100) {
797 draw_map: function() {
798 let map_lines_split = [];
800 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
801 if (j == game.map_size[1]) {
802 map_lines_split.push(line);
806 if (['edit', 'write', 'control_tile_draw',
807 'control_tile_type'].includes(this.mode.name)) {
808 line.push(game.map[i] + game.map_control[i]);
810 line.push(game.map[i] + ' ');
813 map_lines_split.push(line);
814 if (this.map_mode == 'annotations') {
815 for (const coordinate of explorer.info_hints) {
816 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
818 } else if (this.map_mode == 'all') {
819 for (const p in game.portals) {
820 let coordinate = p.split(',')
821 let original = map_lines_split[coordinate[0]][coordinate[1]];
822 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
824 let used_positions = [];
825 for (const thing_id in game.things) {
826 let t = game.things[thing_id];
827 let symbol = game.thing_types[t.type_];
830 meta_char = t.player_char;
832 if (used_positions.includes(t.position.toString())) {
835 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
836 used_positions.push(t.position.toString());
839 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
840 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
843 if (game.map_geometry == 'Square') {
844 for (let line_split of map_lines_split) {
845 map_lines.push(line_split.join(''));
847 } else if (game.map_geometry == 'Hex') {
849 for (let line_split of map_lines_split) {
850 map_lines.push(' '.repeat(indent) + line_split.join(''));
858 let window_center = [terminal.rows / 2, this.window_width / 2];
859 let player = game.things[game.player_id];
860 let center_position = [player.position[0], player.position[1]];
861 if (tui.mode.shows_info) {
862 center_position = [explorer.position[0], explorer.position[1]];
864 center_position[1] = center_position[1] * 2;
865 let offset = [center_position[0] - window_center[0],
866 center_position[1] - window_center[1]]
867 if (game.map_geometry == 'Hex' && offset[0] % 2) {
870 let term_y = Math.max(0, -offset[0]);
871 let term_x = Math.max(0, -offset[1]);
872 let map_y = Math.max(0, offset[0]);
873 let map_x = Math.max(0, offset[1]);
874 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
875 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
876 terminal.write(term_y, term_x, to_draw);
879 draw_mode_line: function() {
880 let help = 'hit [' + this.keys.help + '] for help';
881 if (this.mode.has_input_prompt) {
882 help = 'enter /help for help';
884 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
886 draw_turn_line: function(n) {
887 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
889 draw_history: function() {
890 let log_display_lines = [];
892 let y_offset_in_log = 0;
893 for (let line of this.log) {
894 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
896 log_display_lines = log_display_lines.concat(new_lines);
897 for (const y in link_data) {
898 const rel_y = y_offset_in_log + parseInt(y);
899 log_links[rel_y] = [];
900 for (let link of link_data[y]) {
901 log_links[rel_y].push(link);
904 y_offset_in_log += new_lines.length;
906 let i = log_display_lines.length - 1;
907 for (let y = terminal.rows - 1 - this.height_input;
908 y >= this.height_header && i >= 0;
910 terminal.write(y, this.window_width, log_display_lines[i]);
912 for (const key of Object.keys(log_links)) {
913 if (parseInt(key) <= i) {
914 delete log_links[key];
917 let offset = [terminal.rows - this.height_input - log_display_lines.length,
919 this.offset_links(offset, log_links);
921 draw_info: function() {
922 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
924 let offset = [this.height_header, this.window_width];
925 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
926 terminal.write(y, offset[1], lines[i]);
928 this.offset_links(offset, link_data);
930 draw_input: function() {
931 if (this.mode.has_input_prompt) {
932 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
933 terminal.write(y, this.window_width, this.input_lines[i]);
937 draw_help: function() {
938 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
939 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
940 if (this.mode.name == 'play') {
941 content += "Available actions:\n";
942 if (game.tasks.includes('MOVE')) {
943 content += "[" + movement_keys_desc + "] – move player\n";
945 if (game.tasks.includes('PICK_UP')) {
946 content += "[" + this.keys.take_thing + "] – pick up thing\n";
948 if (game.tasks.includes('DROP')) {
949 content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
951 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
953 } else if (this.mode.name == 'study') {
954 content += "Available actions:\n";
955 content += '[' + movement_keys_desc + '] – move question mark\n';
956 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
958 } else if (this.mode.name == 'edit') {
959 content += "Available actions:\n";
960 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
961 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
964 } else if (this.mode.name == 'control_tile_draw') {
965 content += "Available actions:\n";
966 content += "[" + tui.keys.toggle_tile_draw + "] – toggle tile control drawing\n";
968 } else if (this.mode.name == 'chat') {
969 content += '/nick NAME – re-name yourself to NAME\n';
970 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
971 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
972 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
973 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
975 content += this.mode.list_available_modes();
977 if (!this.mode.has_input_prompt) {
978 start_x = this.window_width
980 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
981 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
982 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
983 terminal.write(y, start_x, lines[i]);
986 toggle_tile_draw: function() {
988 tui.tile_draw = false;
990 tui.tile_draw = true;
993 toggle_map_mode: function() {
994 if (tui.map_mode == 'terrain') {
995 tui.map_mode = 'annotations';
996 } else if (tui.map_mode == 'annotations') {
997 tui.map_mode = 'all';
999 tui.map_mode = 'terrain';
1002 full_refresh: function() {
1004 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1005 if (this.mode.is_intro) {
1006 this.draw_history();
1009 if (game.turn_complete) {
1011 this.draw_turn_line();
1013 this.draw_mode_line();
1014 if (this.mode.shows_info) {
1017 this.draw_history();
1021 if (this.show_help) {
1033 this.map_control = "";
1034 this.map_size = [0,0];
1035 this.player_id = -1;
1039 get_thing: function(id_, create_if_not_found=false) {
1040 if (id_ in game.things) {
1041 return game.things[id_];
1042 } else if (create_if_not_found) {
1043 let t = new Thing([0,0]);
1044 game.things[id_] = t;
1048 move: function(start_position, direction) {
1049 let target = [start_position[0], start_position[1]];
1050 if (direction == 'LEFT') {
1052 } else if (direction == 'RIGHT') {
1054 } else if (game.map_geometry == 'Square') {
1055 if (direction == 'UP') {
1057 } else if (direction == 'DOWN') {
1060 } else if (game.map_geometry == 'Hex') {
1061 let start_indented = start_position[0] % 2;
1062 if (direction == 'UPLEFT') {
1064 if (!start_indented) {
1067 } else if (direction == 'UPRIGHT') {
1069 if (start_indented) {
1072 } else if (direction == 'DOWNLEFT') {
1074 if (!start_indented) {
1077 } else if (direction == 'DOWNRIGHT') {
1079 if (start_indented) {
1084 if (target[0] < 0 || target[1] < 0 ||
1085 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1090 teleport: function() {
1091 let player = this.get_thing(game.player_id);
1092 if (player.position in this.portals) {
1093 server.reconnect_to(this.portals[player.position]);
1095 terminal.blink_screen();
1096 tui.log_msg('? not standing on portal')
1104 server.init(websocket_location);
1110 move: function(direction) {
1111 let target = game.move(this.position, direction);
1113 this.position = target
1114 if (tui.mode.shows_info) {
1116 } else if (tui.tile_draw) {
1117 this.send_tile_control_command();
1120 terminal.blink_screen();
1123 update_info_db: function(yx, str) {
1124 this.info_db[yx] = str;
1125 if (tui.mode.name == 'study') {
1129 empty_info_db: function() {
1131 this.info_hints = [];
1132 if (tui.mode.name == 'study') {
1136 query_info: function() {
1137 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1139 get_info: function() {
1140 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1141 if (game.fov[position_i] != '.') {
1142 return 'outside field of view';
1145 let terrain_char = game.map[position_i]
1146 let terrain_desc = '?'
1147 if (game.terrains[terrain_char]) {
1148 terrain_desc = game.terrains[terrain_char];
1150 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1151 let protection = game.map_control[position_i];
1152 if (protection == '.') {
1153 protection = 'unprotected';
1155 info += 'PROTECTION: ' + protection + '\n';
1156 for (let t_id in game.things) {
1157 let t = game.things[t_id];
1158 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1159 let symbol = game.thing_types[t.type_];
1160 info += "THING: " + t.type_ + " / " + symbol;
1161 if (t.player_char) {
1162 info += t.player_char;
1165 info += " (" + t.name_ + ")";
1170 if (this.position in game.portals) {
1171 info += "PORTAL: " + game.portals[this.position] + "\n";
1173 if (this.position in this.info_db) {
1174 info += "ANNOTATIONS: " + this.info_db[this.position];
1176 info += 'waiting …';
1180 annotate: function(msg) {
1181 if (msg.length == 0) {
1182 msg = " "; // triggers annotation deletion
1184 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1186 set_portal: function(msg) {
1187 if (msg.length == 0) {
1188 msg = " "; // triggers portal deletion
1190 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1192 send_tile_control_command: function() {
1193 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1197 tui.inputEl.addEventListener('input', (event) => {
1198 if (tui.mode.has_input_prompt) {
1199 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1200 if (tui.inputEl.value.length > max_length) {
1201 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1203 tui.recalc_input_lines();
1204 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1205 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1206 tui.switch_mode('edit');
1207 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1208 tui.tile_control_char = tui.inputEl.value[0];
1209 tui.switch_mode('control_pw_pw');
1210 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1211 tui.tile_control_char = tui.inputEl.value[0];
1212 tui.switch_mode('control_tile_draw');
1216 tui.inputEl.addEventListener('keydown', (event) => {
1217 tui.show_help = false;
1218 if (event.key == 'Enter') {
1219 event.preventDefault();
1221 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1222 tui.show_help = true;
1224 tui.restore_input_values();
1225 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1226 && !tui.mode.is_single_char_entry) {
1227 tui.show_help = true;
1228 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1229 tui.login_name = tui.inputEl.value;
1230 server.send(['LOGIN', tui.inputEl.value]);
1232 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1233 if (tui.inputEl.value.length == 0) {
1234 tui.log_msg('@ aborted');
1236 server.send(['SET_MAP_CONTROL_PASSWORD',
1237 tui.tile_control_char, tui.inputEl.value]);
1239 tui.switch_mode('admin');
1240 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1241 explorer.set_portal(tui.inputEl.value);
1242 tui.switch_mode('edit');
1243 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1244 explorer.annotate(tui.inputEl.value);
1245 tui.switch_mode('edit');
1246 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1247 if (tui.inputEl.value.length == 0) {
1248 tui.inputEl.value = " ";
1250 tui.password = tui.inputEl.value
1251 tui.switch_mode('edit');
1252 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1253 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1254 tui.switch_mode('play');
1255 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1256 let tokens = parser.tokenize(tui.inputEl.value);
1257 if (tokens.length > 0 && tokens[0].length > 0) {
1258 if (tui.inputEl.value[0][0] == '/') {
1259 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1260 tui.switch_mode('play');
1261 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1262 tui.switch_mode('study');
1263 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1264 tui.switch_mode('edit');
1265 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1266 tui.switch_mode('admin_enter');
1267 } else if (tokens[0].slice(1) == 'nick') {
1268 if (tokens.length > 1) {
1269 server.send(['NICK', tokens[1]]);
1271 tui.log_msg('? need new name');
1274 tui.log_msg('? unknown command');
1277 server.send(['ALL', tui.inputEl.value]);
1279 } else if (tui.inputEl.valuelength > 0) {
1280 server.send(['ALL', tui.inputEl.value]);
1283 } else if (tui.mode.name == 'play') {
1284 if (tui.mode.mode_switch_on_key(event)) {
1286 } else if (event.key === tui.keys.take_thing
1287 && game.tasks.includes('PICK_UP')) {
1288 server.send(["TASK:PICK_UP"]);
1289 } else if (event.key === tui.keys.drop_thing
1290 && game.tasks.includes('DROP')) {
1291 server.send(["TASK:DROP"]);
1292 } else if (event.key in tui.movement_keys
1293 && game.tasks.includes('MOVE')) {
1294 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1295 } else if (event.key === tui.keys.teleport) {
1298 } else if (tui.mode.name == 'study') {
1299 if (tui.mode.mode_switch_on_key(event)) {
1301 } else if (event.key in tui.movement_keys) {
1302 explorer.move(tui.movement_keys[event.key]);
1303 } else if (event.key == tui.keys.toggle_map_mode) {
1304 tui.toggle_map_mode();
1306 } else if (tui.mode.name == 'control_tile_draw') {
1307 if (tui.mode.mode_switch_on_key(event)) {
1309 } else if (event.key in tui.movement_keys) {
1310 explorer.move(tui.movement_keys[event.key]);
1311 } else if (event.key === tui.keys.toggle_tile_draw) {
1312 tui.toggle_tile_draw();
1314 } else if (tui.mode.name == 'admin') {
1315 if (tui.mode.mode_switch_on_key(event)) {
1318 } else if (tui.mode.name == 'edit') {
1319 if (tui.mode.mode_switch_on_key(event)) {
1321 } else if (event.key in tui.movement_keys
1322 && game.tasks.includes('MOVE')) {
1323 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1324 } else if (event.key === tui.keys.flatten
1325 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1326 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1332 rows_selector.addEventListener('input', function() {
1333 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1336 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1337 terminal.initialize();
1340 cols_selector.addEventListener('input', function() {
1341 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1344 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1345 terminal.initialize();
1346 tui.window_width = terminal.cols / 2,
1349 for (let key_selector of key_selectors) {
1350 key_selector.addEventListener('input', function() {
1351 window.localStorage.setItem(key_selector.id, key_selector.value);
1355 window.setInterval(function() {
1356 if (server.connected) {
1357 server.send(['PING']);
1359 server.reconnect_to(server.url);
1360 tui.log_msg('@ attempting reconnect …')
1363 document.getElementById("terminal").onclick = function() {
1364 tui.inputEl.focus();
1366 document.getElementById("help").onclick = function() {
1367 tui.show_help = true;
1370 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1371 const mode = switchEl.id.slice("switch_to_".length);
1372 switchEl.onclick = function() {
1373 tui.switch_mode(mode);
1377 document.getElementById("toggle_tile_draw").onclick = function() {
1378 tui.toggle_tile_draw();
1380 document.getElementById("toggle_map_mode").onclick = function() {
1381 tui.toggle_map_mode();
1384 document.getElementById("take_thing").onclick = function() {
1385 server.send(['TASK:PICK_UP']);
1387 document.getElementById("drop_thing").onclick = function() {
1388 server.send(['TASK:DROP']);
1390 document.getElementById("flatten").onclick = function() {
1391 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1393 document.getElementById("teleport").onclick = function() {
1396 document.getElementById("move_upleft").onclick = function() {
1397 if (tui.mode.name == 'play') {
1398 server.send(['TASK:MOVE', 'UPLEFT']);
1400 explorer.move('UPLEFT');
1403 document.getElementById("move_left").onclick = function() {
1404 if (tui.mode.name == 'play') {
1405 server.send(['TASK:MOVE', 'LEFT']);
1407 explorer.move('LEFT');
1410 document.getElementById("move_downleft").onclick = function() {
1411 if (tui.mode.name == 'play') {
1412 server.send(['TASK:MOVE', 'DOWNLEFT']);
1414 explorer.move('DOWNLEFT');
1417 document.getElementById("move_down").onclick = function() {
1418 if (tui.mode.name == 'play') {
1419 server.send(['TASK:MOVE', 'DOWN']);
1421 explorer.move('DOWN');
1424 document.getElementById("move_up").onclick = function() {
1425 if (tui.mode.name == 'play') {
1426 server.send(['TASK:MOVE', 'UP']);
1428 explorer.move('UP');
1431 document.getElementById("move_upright").onclick = function() {
1432 if (tui.mode.name == 'play') {
1433 server.send(['TASK:MOVE', 'UPRIGHT']);
1435 explorer.move('UPRIGHT');
1438 document.getElementById("move_right").onclick = function() {
1439 if (tui.mode.name == 'play') {
1440 server.send(['TASK:MOVE', 'RIGHT']);
1442 explorer.move('RIGHT');
1445 document.getElementById("move_downright").onclick = function() {
1446 if (tui.mode.name == 'play') {
1447 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1449 explorer.move('DOWNRIGHT');