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 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table style="float: left">
25 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
26 <td style="text-align: center"><button id="move_up">up</button></td>
27 <td><button id="move_upright">up-right</button></td>
30 <td style="text-align: right;"><button id="move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="move_right">right</button></td>
35 <td><button id="move_downleft">down-left</button></td>
36 <td style="text-align: center"><button id="move_down">down</button></td>
37 <td><button id="move_downright">down-right</button></td>
42 <td><button id="help">help</button></td>
45 <td><button id="switch_to_chat">chat mode</button><br /></td>
48 <td><button id="switch_to_study">study mode</button></td>
49 <td><button id="toggle_map_mode">toggle map view</button>
52 <td><button id="switch_to_play">play mode</button></td>
54 <button id="take_thing">pick up thing</button>
55 <button id="drop_thing">drop thing</button>
56 <button id="teleport">teleport</button>
60 <td><button id="switch_to_edit">map edit mode</button></td>
62 <button id="switch_to_write">change terrain</button>
63 <button id="flatten">flatten surroundings</button>
64 <button id="switch_to_annotate">annotate tile</button>
65 <button id="switch_to_portal">edit portal</button>
66 <button id="switch_to_password">enter map edit password</button>
70 <td><button id="switch_to_admin_enter">admin mode</button></td>
72 <button id="switch_to_control_pw_type">change protection character password</button>
73 <button id="switch_to_control_tile_type">change protection areas</button>
74 <button id="toggle_tile_draw">toggle protection character drawing</button>
79 <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 />
81 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
82 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
83 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
84 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
85 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
86 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
87 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
88 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
89 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
90 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
91 <li>help: <input id="key_help" type="text" value="h" />
92 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
93 <li>teleport: <input id="key_teleport" type="text" value="p" />
94 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
95 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
96 <li><input id="key_switch_to_chat" type="text" value="t" />
97 <li><input id="key_switch_to_play" type="text" value="p" />
98 <li><input id="key_switch_to_study" type="text" value="?" />
99 <li><input id="key_switch_to_edit" type="text" value="E" />
100 <li><input id="key_switch_to_write" type="text" value="m" />
101 <li><input id="key_switch_to_password" type="text" value="P" />
102 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
103 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
104 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
105 <li><input id="key_switch_to_annotate" type="text" value="M" />
106 <li><input id="key_switch_to_portal" type="text" value="T" />
107 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="M" />
108 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
113 let websocket_location = "wss://plomlompom.com/rogue_chat/";
114 //let websocket_location = "ws://localhost:8000/";
119 'long': 'This mode allows you to interact with the map in various ways.'
123 '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. Toggle the map view to show or hide different information layers.'},
126 'long': 'This mode allows you to change the map in various ways. Individual map tiles are shown together with their "protection characters". You can edit a tile if you set the map edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
129 'short': 'change terrain',
130 '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.'
133 'short': 'change protection character password',
134 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
137 'short': 'change tiles protection password',
138 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
140 'control_tile_type': {
141 'short': 'change tiles protection',
142 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
144 'control_tile_draw': {
145 'short': 'change tiles protection',
146 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected tile protection character.'
149 'short': 'annotate tile',
150 '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.'
153 'short': 'edit portal',
154 '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.'
158 '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:'
162 'long': 'Enter your player name.'
164 'waiting_for_server': {
165 'short': 'waiting for server response',
166 'long': 'Waiting for a server response.'
169 'short': 'waiting for server response',
170 'long': 'Waiting for a server response.'
173 'short': 'set map edit password',
174 '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.'
177 'short': 'become admin',
178 'long': 'This mode allows you to become admin if you know an admin password.'
182 'long': 'This mode allows you access to actions limited to administrators.'
186 let rows_selector = document.getElementById("n_rows");
187 let cols_selector = document.getElementById("n_cols");
188 let key_selectors = document.querySelectorAll('[id^="key_"]');
190 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
191 const action = key_switch_selector.id.slice("key_switch_to_".length);
192 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
195 function restore_selector_value(selector) {
196 let stored_selection = window.localStorage.getItem(selector.id);
197 if (stored_selection) {
198 selector.value = stored_selection;
201 restore_selector_value(rows_selector);
202 restore_selector_value(cols_selector);
203 for (let key_selector of key_selectors) {
204 restore_selector_value(key_selector);
210 initialize: function() {
211 this.rows = rows_selector.value;
212 this.cols = cols_selector.value;
213 this.pre_el = document.getElementById("terminal");
214 this.pre_el.style.color = this.foreground;
215 this.pre_el.style.backgroundColor = this.background;
218 for (let y = 0, x = 0; y <= this.rows; x++) {
219 if (x == this.cols) {
222 this.content.push(line);
224 if (y == this.rows) {
231 blink_screen: function() {
232 this.pre_el.style.color = this.background;
233 this.pre_el.style.backgroundColor = this.foreground;
235 this.pre_el.style.color = this.foreground;
236 this.pre_el.style.backgroundColor = this.background;
239 refresh: function() {
240 function escapeHTML(str) {
242 replace(/&/g, '&').
243 replace(/</g, '<').
244 replace(/>/g, '>').
245 replace(/'/g, ''').
246 replace(/"/g, '"');
248 let pre_content = '';
249 for (let y = 0; y < this.rows; y++) {
250 let line = this.content[y].join('');
252 if (y in tui.links) {
254 for (let span of tui.links[y]) {
255 chunks.push(escapeHTML(line.slice(start_x, span[0])));
256 chunks.push('<a href="');
257 chunks.push(escapeHTML(span[2]));
259 chunks.push(escapeHTML(line.slice(span[0], span[1])));
263 chunks.push(escapeHTML(line.slice(start_x)));
265 chunks = [escapeHTML(line)];
267 for (const chunk of chunks) {
268 pre_content += chunk;
272 this.pre_el.innerHTML = pre_content;
274 write: function(start_y, start_x, msg) {
275 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
276 this.content[start_y][x] = msg[i];
279 drawBox: function(start_y, start_x, height, width) {
280 let end_y = start_y + height;
281 let end_x = start_x + width;
282 for (let y = start_y, x = start_x; y < this.rows; x++) {
290 this.content[y][x] = ' ';
294 terminal.initialize();
297 tokenize: function(str) {
302 for (let i = 0; i < str.length; i++) {
308 } else if (c == '\\') {
310 } else if (c == '"') {
315 } else if (c == '"') {
317 } else if (c === ' ') {
318 if (token.length > 0) {
326 if (token.length > 0) {
331 parse_yx: function(position_string) {
332 let coordinate_strings = position_string.split(',')
333 let position = [0, 0];
334 position[0] = parseInt(coordinate_strings[0].slice(2));
335 position[1] = parseInt(coordinate_strings[1].slice(2));
347 init: function(url) {
349 this.websocket = new WebSocket(this.url);
350 this.websocket.onopen = function(event) {
351 server.connected = true;
352 game.thing_types = {};
354 server.send(['TASKS']);
355 server.send(['TERRAINS']);
356 server.send(['THING_TYPES']);
357 tui.log_msg("@ server connected! :)");
358 tui.switch_mode('login');
360 this.websocket.onclose = function(event) {
361 server.connected = false;
362 tui.switch_mode('waiting_for_server');
363 tui.log_msg("@ server disconnected :(");
365 this.websocket.onmessage = this.handle_event;
367 reconnect_to: function(url) {
368 this.websocket.close();
371 send: function(tokens) {
372 this.websocket.send(unparser.untokenize(tokens));
374 handle_event: function(event) {
375 let tokens = parser.tokenize(event.data);
376 if (tokens[0] === 'TURN') {
377 game.turn_complete = false;
378 explorer.empty_info_db();
381 game.turn = parseInt(tokens[1]);
382 } else if (tokens[0] === 'THING') {
383 let t = game.get_thing(tokens[3], true);
384 t.position = parser.parse_yx(tokens[1]);
386 } else if (tokens[0] === 'THING_NAME') {
387 let t = game.get_thing(tokens[1], false);
391 } else if (tokens[0] === 'THING_CHAR') {
392 let t = game.get_thing(tokens[1], false);
394 t.player_char = tokens[2];
396 } else if (tokens[0] === 'TASKS') {
397 game.tasks = tokens[1].split(',');
398 tui.mode_write.legal = game.tasks.includes('WRITE');
399 } else if (tokens[0] === 'THING_TYPE') {
400 game.thing_types[tokens[1]] = tokens[2]
401 } else if (tokens[0] === 'TERRAIN') {
402 game.terrains[tokens[1]] = tokens[2]
403 } else if (tokens[0] === 'MAP') {
404 game.map_geometry = tokens[1];
406 game.map_size = parser.parse_yx(tokens[2]);
408 } else if (tokens[0] === 'FOV') {
410 } else if (tokens[0] === 'MAP_CONTROL') {
411 game.map_control = tokens[1]
412 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
413 game.turn_complete = true;
414 if (tui.mode.name == 'post_login_wait') {
415 tui.switch_mode('play');
416 } else if (tui.mode.name == 'study') {
417 explorer.query_info();
420 } else if (tokens[0] === 'CHAT') {
421 tui.log_msg('# ' + tokens[1], 1);
422 } else if (tokens[0] === 'PLAYER_ID') {
423 game.player_id = parseInt(tokens[1]);
424 } else if (tokens[0] === 'LOGIN_OK') {
425 this.send(['GET_GAMESTATE']);
426 tui.switch_mode('post_login_wait');
427 } else if (tokens[0] === 'ADMIN_OK') {
429 tui.log_msg('@ you now have admin rights');
430 tui.switch_mode('admin');
431 } else if (tokens[0] === 'PORTAL') {
432 let position = parser.parse_yx(tokens[1]);
433 game.portals[position] = tokens[2];
434 } else if (tokens[0] === 'ANNOTATION_HINT') {
435 let position = parser.parse_yx(tokens[1]);
436 explorer.info_hints = explorer.info_hints.concat([position]);
437 } else if (tokens[0] === 'ANNOTATION') {
438 let position = parser.parse_yx(tokens[1]);
439 explorer.update_info_db(position, tokens[2]);
440 tui.restore_input_values();
442 } else if (tokens[0] === 'UNHANDLED_INPUT') {
443 tui.log_msg('? unknown command');
444 } else if (tokens[0] === 'PLAY_ERROR') {
445 tui.log_msg('? ' + tokens[1]);
446 terminal.blink_screen();
447 } else if (tokens[0] === 'ARGUMENT_ERROR') {
448 tui.log_msg('? syntax error: ' + tokens[1]);
449 } else if (tokens[0] === 'GAME_ERROR') {
450 tui.log_msg('? game error: ' + tokens[1]);
451 } else if (tokens[0] === 'PONG') {
454 tui.log_msg('? unhandled input: ' + event.data);
460 quote: function(str) {
462 for (let i = 0; i < str.length; i++) {
464 if (['"', '\\'].includes(c)) {
470 return quoted.join('');
472 to_yx: function(yx_coordinate) {
473 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
475 untokenize: function(tokens) {
476 let quoted_tokens = [];
477 for (let token of tokens) {
478 quoted_tokens.push(this.quote(token));
480 return quoted_tokens.join(" ");
485 constructor(name, has_input_prompt=false, shows_info=false,
486 is_intro=false, is_single_char_entry=false) {
488 this.short_desc = mode_helps[name].short;
489 this.available_modes = [];
490 this.has_input_prompt = has_input_prompt;
491 this.shows_info= shows_info;
492 this.is_intro = is_intro;
493 this.help_intro = mode_helps[name].long;
494 this.is_single_char_entry = is_single_char_entry;
497 *iter_available_modes() {
498 for (let mode_name of this.available_modes) {
499 let mode = tui['mode_' + mode_name];
503 let key = tui.keys['switch_to_' + mode.name];
507 list_available_modes() {
509 if (this.available_modes.length > 0) {
510 msg += 'Other modes available from here:\n';
511 for (let [mode, key] of this.iter_available_modes()) {
512 msg += '[' + key + '] – ' + mode.short_desc + '\n';
517 mode_switch_on_key(key_event) {
518 for (let [mode, key] of this.iter_available_modes()) {
519 if (key_event.key == key) {
520 event.preventDefault();
521 tui.switch_mode(mode.name);
533 window_width: terminal.cols / 2,
541 mode_waiting_for_server: new Mode('waiting_for_server',
543 mode_login: new Mode('login', true, false, true),
544 mode_post_login_wait: new Mode('post_login_wait'),
545 mode_chat: new Mode('chat', true),
546 mode_annotate: new Mode('annotate', true, true),
547 mode_play: new Mode('play'),
548 mode_study: new Mode('study', false, true),
549 mode_write: new Mode('write', false, false, false, true),
550 mode_edit: new Mode('edit'),
551 mode_control_pw_type: new Mode('control_pw_type', true),
552 mode_portal: new Mode('portal', true, true),
553 mode_password: new Mode('password', true),
554 mode_admin_enter: new Mode('admin_enter', true),
555 mode_admin: new Mode('admin'),
556 mode_control_pw_pw: new Mode('control_pw_pw', true),
557 mode_control_tile_type: new Mode('control_tile_type', 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 if (this.mode.name == 'control_tile_draw') {
614 tui.log_msg('@ finished tile protection drawing.')
616 this.inputEl.focus();
617 this.map_mode = 'terrain + things';
618 this.tile_draw = false;
619 if (mode_name == 'admin_enter' && this.is_admin) {
622 this.mode = this['mode_' + mode_name];
623 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
624 explorer.position = game.things[game.player_id].position;
625 if (this.mode.shows_info) {
626 explorer.query_info();
630 this.restore_input_values();
631 for (let el of document.getElementsByTagName("button")) {
634 document.getElementById("help").disabled = false;
635 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
636 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
637 move_key.disabled = false;
640 if (!this.mode.is_intro && this.mode.name != 'play') {
641 document.getElementById("switch_to_play").disabled = false;
643 if (!this.mode.is_intro && this.mode.name != 'study') {
644 document.getElementById("switch_to_study").disabled = false;
646 if (!this.mode.is_intro && this.mode.name != 'chat') {
647 document.getElementById("switch_to_chat").disabled = false;
649 if (!this.mode.is_intro && this.mode.name != 'edit') {
650 document.getElementById("switch_to_edit").disabled = false;
652 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
653 document.getElementById("switch_to_admin_enter").disabled = false;
655 if (this.mode.name == 'login') {
656 if (this.login_name) {
657 server.send(['LOGIN', this.login_name]);
659 this.log_msg("? need login name");
661 } else if (this.mode.name == 'play') {
662 if (game.tasks.includes('PICK_UP')) {
663 document.getElementById("take_thing").disabled = false;
665 if (game.tasks.includes('DROP')) {
666 document.getElementById("drop_thing").disabled = false;
668 if (game.tasks.includes('MOVE')) {
670 document.getElementById("teleport").disabled = false;
671 } else if (this.mode.name == 'edit') {
672 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
673 document.getElementById("flatten").disabled = false;
675 document.getElementById("switch_to_annotate").disabled = false;
676 document.getElementById("switch_to_write").disabled = false;
677 document.getElementById("switch_to_portal").disabled = false;
678 document.getElementById("switch_to_password").disabled = false;
679 } else if (this.mode.name == 'admin') {
680 document.getElementById("switch_to_control_pw_type").disabled = false;
681 document.getElementById("switch_to_control_tile_type").disabled = false;
682 } else if (this.mode.name == 'study') {
683 document.getElementById("toggle_map_mode").disabled = false;
684 } else if (this.mode.is_single_char_entry) {
685 this.show_help = true;
686 } else if (this.mode.name == 'admin_enter') {
687 this.log_msg('@ enter admin password:')
688 } else if (this.mode.name == 'control_pw_type') {
689 this.log_msg('@ enter tile protection character for which you want to change the password:')
690 } else if (this.mode.name == 'control_tile_type') {
691 this.log_msg('@ enter tile protection character which you want to draw:')
692 } else if (this.mode.name == 'control_pw_pw') {
693 this.log_msg('@ enter tile protection password for "' + this.tile_control_char + '":');
694 } else if (this.mode.name == 'control_tile_draw') {
695 document.getElementById("toggle_tile_draw").disabled = false;
696 this.log_msg('@ can draw tile protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
700 offset_links: function(offset, links) {
701 for (let y in links) {
702 let real_y = offset[0] + parseInt(y);
703 if (!this.links[real_y]) {
704 this.links[real_y] = [];
706 for (let link of links[y]) {
707 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
708 this.links[real_y].push(offset_link);
712 restore_input_values: function() {
713 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
714 let info = explorer.info_db[explorer.position];
715 if (info != "(none)") {
716 this.inputEl.value = info;
717 this.recalc_input_lines();
719 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
720 let portal = game.portals[explorer.position]
721 this.inputEl.value = portal;
722 this.recalc_input_lines();
723 } else if (this.mode.name == 'password') {
724 this.inputEl.value = this.password;
725 this.recalc_input_lines();
728 empty_input: function(str) {
729 this.inputEl.value = "";
730 if (this.mode.has_input_prompt) {
731 this.recalc_input_lines();
733 this.height_input = 0;
736 recalc_input_lines: function() {
738 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
739 this.height_input = this.input_lines.length;
741 msg_into_lines_of_width: function(msg, width) {
742 function push_inner_link(y, end_x) {
743 if (!inner_links[y]) {
746 inner_links[y].push([url_start_x, end_x, url]);
748 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
751 for (const match of matches) {
752 const url = match[0];
753 const url_start = match.index;
754 const url_end = match.index + match[0].length;
755 link_data[url_start] = url;
756 url_ends.push(url_end);
760 let inner_links = {};
764 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
765 if (x >= width || msg[i] == "\n") {
767 push_inner_link(y, chunk.length);
773 if (msg[i] == "\n") {
778 if (msg[i] != "\n") {
781 if (i in link_data) {
785 } else if (url_ends.includes(i)) {
786 push_inner_link(y, x);
792 push_inner_link(lines.length - 1, chunk.length);
794 return [lines, inner_links];
796 log_msg: function(msg) {
798 while (this.log.length > 100) {
803 draw_map: function() {
804 let map_lines_split = [];
806 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
807 if (j == game.map_size[1]) {
808 map_lines_split.push(line);
812 if (['edit', 'write', 'control_tile_draw',
813 'control_tile_type'].includes(this.mode.name)) {
814 line.push(game.map[i] + game.map_control[i]);
816 line.push(game.map[i] + ' ');
819 map_lines_split.push(line);
820 if (this.map_mode == 'terrain + annotations') {
821 for (const coordinate of explorer.info_hints) {
822 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
824 } else if (this.map_mode == 'terrain + things') {
825 for (const p in game.portals) {
826 let coordinate = p.split(',')
827 let original = map_lines_split[coordinate[0]][coordinate[1]];
828 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
830 let used_positions = [];
831 for (const thing_id in game.things) {
832 let t = game.things[thing_id];
833 let symbol = game.thing_types[t.type_];
836 meta_char = t.player_char;
838 if (used_positions.includes(t.position.toString())) {
841 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
842 used_positions.push(t.position.toString());
845 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
846 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
849 if (game.map_geometry == 'Square') {
850 for (let line_split of map_lines_split) {
851 map_lines.push(line_split.join(''));
853 } else if (game.map_geometry == 'Hex') {
855 for (let line_split of map_lines_split) {
856 map_lines.push(' '.repeat(indent) + line_split.join(''));
864 let window_center = [terminal.rows / 2, this.window_width / 2];
865 let player = game.things[game.player_id];
866 let center_position = [player.position[0], player.position[1]];
867 if (tui.mode.shows_info) {
868 center_position = [explorer.position[0], explorer.position[1]];
870 center_position[1] = center_position[1] * 2;
871 let offset = [center_position[0] - window_center[0],
872 center_position[1] - window_center[1]]
873 if (game.map_geometry == 'Hex' && offset[0] % 2) {
876 let term_y = Math.max(0, -offset[0]);
877 let term_x = Math.max(0, -offset[1]);
878 let map_y = Math.max(0, offset[0]);
879 let map_x = Math.max(0, offset[1]);
880 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
881 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
882 terminal.write(term_y, term_x, to_draw);
885 draw_mode_line: function() {
886 let help = 'hit [' + this.keys.help + '] for help';
887 if (this.mode.has_input_prompt) {
888 help = 'enter /help for help';
890 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
892 draw_turn_line: function(n) {
893 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
895 draw_history: function() {
896 let log_display_lines = [];
898 let y_offset_in_log = 0;
899 for (let line of this.log) {
900 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
902 log_display_lines = log_display_lines.concat(new_lines);
903 for (const y in link_data) {
904 const rel_y = y_offset_in_log + parseInt(y);
905 log_links[rel_y] = [];
906 for (let link of link_data[y]) {
907 log_links[rel_y].push(link);
910 y_offset_in_log += new_lines.length;
912 let i = log_display_lines.length - 1;
913 for (let y = terminal.rows - 1 - this.height_input;
914 y >= this.height_header && i >= 0;
916 terminal.write(y, this.window_width, log_display_lines[i]);
918 for (const key of Object.keys(log_links)) {
919 if (parseInt(key) <= i) {
920 delete log_links[key];
923 let offset = [terminal.rows - this.height_input - log_display_lines.length,
925 this.offset_links(offset, log_links);
927 draw_info: function() {
928 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
930 let offset = [this.height_header, this.window_width];
931 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
932 terminal.write(y, offset[1], lines[i]);
934 this.offset_links(offset, link_data);
936 draw_input: function() {
937 if (this.mode.has_input_prompt) {
938 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
939 terminal.write(y, this.window_width, this.input_lines[i]);
943 draw_help: function() {
944 let movement_keys_desc = '';
945 if (!this.mode.is_intro) {
946 movement_keys_desc = Object.keys(this.movement_keys).join(',');
948 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
949 if (this.mode.name == 'play') {
950 content += "Available actions:\n";
951 if (game.tasks.includes('MOVE')) {
952 content += "[" + movement_keys_desc + "] – move player\n";
954 if (game.tasks.includes('PICK_UP')) {
955 content += "[" + this.keys.take_thing + "] – pick up thing\n";
957 if (game.tasks.includes('DROP')) {
958 content += "[" + this.keys.drop_thing + "] – drop thing\n";
960 content += "[" + tui.keys.teleport + "] – teleport\n";
962 } else if (this.mode.name == 'study') {
963 content += "Available actions:\n";
964 content += '[' + movement_keys_desc + '] – move question mark\n';
965 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
967 } else if (this.mode.name == 'edit') {
968 content += "Available actions:\n";
969 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
970 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
973 } else if (this.mode.name == 'control_tile_draw') {
974 content += "Available actions:\n";
975 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
977 } else if (this.mode.name == 'chat') {
978 content += '/nick NAME – re-name yourself to NAME\n';
979 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
980 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
981 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
982 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
984 content += this.mode.list_available_modes();
986 if (!this.mode.has_input_prompt) {
987 start_x = this.window_width
989 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
990 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
991 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
992 terminal.write(y, start_x, lines[i]);
995 toggle_tile_draw: function() {
997 tui.tile_draw = false;
999 tui.tile_draw = true;
1002 toggle_map_mode: function() {
1003 if (tui.map_mode == 'terrain only') {
1004 tui.map_mode = 'terrain + annotations';
1005 } else if (tui.map_mode == 'terrain + annotations') {
1006 tui.map_mode = 'terrain + things';
1008 tui.map_mode = 'terrain only';
1011 full_refresh: function() {
1013 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1014 if (this.mode.is_intro) {
1015 this.draw_history();
1018 if (game.turn_complete) {
1020 this.draw_turn_line();
1022 this.draw_mode_line();
1023 if (this.mode.shows_info) {
1026 this.draw_history();
1030 if (this.show_help) {
1042 this.map_control = "";
1043 this.map_size = [0,0];
1044 this.player_id = -1;
1048 get_thing: function(id_, create_if_not_found=false) {
1049 if (id_ in game.things) {
1050 return game.things[id_];
1051 } else if (create_if_not_found) {
1052 let t = new Thing([0,0]);
1053 game.things[id_] = t;
1057 move: function(start_position, direction) {
1058 let target = [start_position[0], start_position[1]];
1059 if (direction == 'LEFT') {
1061 } else if (direction == 'RIGHT') {
1063 } else if (game.map_geometry == 'Square') {
1064 if (direction == 'UP') {
1066 } else if (direction == 'DOWN') {
1069 } else if (game.map_geometry == 'Hex') {
1070 let start_indented = start_position[0] % 2;
1071 if (direction == 'UPLEFT') {
1073 if (!start_indented) {
1076 } else if (direction == 'UPRIGHT') {
1078 if (start_indented) {
1081 } else if (direction == 'DOWNLEFT') {
1083 if (!start_indented) {
1086 } else if (direction == 'DOWNRIGHT') {
1088 if (start_indented) {
1093 if (target[0] < 0 || target[1] < 0 ||
1094 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1099 teleport: function() {
1100 let player = this.get_thing(game.player_id);
1101 if (player.position in this.portals) {
1102 server.reconnect_to(this.portals[player.position]);
1104 terminal.blink_screen();
1105 tui.log_msg('? not standing on portal')
1113 server.init(websocket_location);
1119 move: function(direction) {
1120 let target = game.move(this.position, direction);
1122 this.position = target
1123 if (tui.mode.shows_info) {
1125 } else if (tui.tile_draw) {
1126 this.send_tile_control_command();
1129 terminal.blink_screen();
1132 update_info_db: function(yx, str) {
1133 this.info_db[yx] = str;
1134 if (tui.mode.name == 'study') {
1138 empty_info_db: function() {
1140 this.info_hints = [];
1141 if (tui.mode.name == 'study') {
1145 query_info: function() {
1146 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1148 get_info: function() {
1149 let info = "MAP VIEW: " + tui.map_mode + "\n";
1150 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1151 if (game.fov[position_i] != '.') {
1152 return info + 'outside field of view';
1154 let terrain_char = game.map[position_i]
1155 let terrain_desc = '?'
1156 if (game.terrains[terrain_char]) {
1157 terrain_desc = game.terrains[terrain_char];
1159 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1160 let protection = game.map_control[position_i];
1161 if (protection == '.') {
1162 protection = 'unprotected';
1164 info += 'PROTECTION: ' + protection + '\n';
1165 for (let t_id in game.things) {
1166 let t = game.things[t_id];
1167 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1168 let symbol = game.thing_types[t.type_];
1169 info += "THING: " + t.type_ + " / " + symbol;
1170 if (t.player_char) {
1171 info += t.player_char;
1174 info += " (" + t.name_ + ")";
1179 if (this.position in game.portals) {
1180 info += "PORTAL: " + game.portals[this.position] + "\n";
1182 if (this.position in this.info_db) {
1183 info += "ANNOTATIONS: " + this.info_db[this.position];
1185 info += 'waiting …';
1189 annotate: function(msg) {
1190 if (msg.length == 0) {
1191 msg = " "; // triggers annotation deletion
1193 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1195 set_portal: function(msg) {
1196 if (msg.length == 0) {
1197 msg = " "; // triggers portal deletion
1199 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1201 send_tile_control_command: function() {
1202 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1206 tui.inputEl.addEventListener('input', (event) => {
1207 if (tui.mode.has_input_prompt) {
1208 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1209 if (tui.inputEl.value.length > max_length) {
1210 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1212 tui.recalc_input_lines();
1213 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1214 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1215 tui.switch_mode('edit');
1219 document.onclick = function() {
1220 tui.show_help = false;
1222 tui.inputEl.addEventListener('keydown', (event) => {
1223 tui.show_help = false;
1224 if (event.key == 'Enter') {
1225 event.preventDefault();
1227 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1228 tui.show_help = true;
1230 tui.restore_input_values();
1231 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1232 && !tui.mode.is_single_char_entry) {
1233 tui.show_help = true;
1234 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1235 tui.login_name = tui.inputEl.value;
1236 server.send(['LOGIN', tui.inputEl.value]);
1238 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1239 if (tui.inputEl.value.length == 0) {
1240 tui.log_msg('@ aborted');
1242 server.send(['SET_MAP_CONTROL_PASSWORD',
1243 tui.tile_control_char, tui.inputEl.value]);
1244 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1246 tui.switch_mode('admin');
1247 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1248 explorer.set_portal(tui.inputEl.value);
1249 tui.switch_mode('edit');
1250 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1251 explorer.annotate(tui.inputEl.value);
1252 tui.switch_mode('edit');
1253 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1254 if (tui.inputEl.value.length == 0) {
1255 tui.inputEl.value = " ";
1257 tui.password = tui.inputEl.value
1258 tui.switch_mode('edit');
1259 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1260 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1261 tui.switch_mode('play');
1262 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1263 if (tui.inputEl.value.length != 1) {
1264 tui.log_msg('@ entered non-single-char, therefore aborted');
1265 tui.switch_mode('admin');
1267 tui.tile_control_char = tui.inputEl.value[0];
1268 tui.switch_mode('control_pw_pw');
1270 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1271 if (tui.inputEl.value.length != 1) {
1272 tui.log_msg('@ entered non-single-char, therefore aborted');
1273 tui.switch_mode('admin');
1275 tui.tile_control_char = tui.inputEl.value[0];
1276 tui.switch_mode('control_tile_draw');
1278 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1279 let tokens = parser.tokenize(tui.inputEl.value);
1280 if (tokens.length > 0 && tokens[0].length > 0) {
1281 if (tui.inputEl.value[0][0] == '/') {
1282 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1283 tui.switch_mode('play');
1284 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1285 tui.switch_mode('study');
1286 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1287 tui.switch_mode('edit');
1288 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1289 tui.switch_mode('admin_enter');
1290 } else if (tokens[0].slice(1) == 'nick') {
1291 if (tokens.length > 1) {
1292 server.send(['NICK', tokens[1]]);
1294 tui.log_msg('? need new name');
1297 tui.log_msg('? unknown command');
1300 server.send(['ALL', tui.inputEl.value]);
1302 } else if (tui.inputEl.valuelength > 0) {
1303 server.send(['ALL', tui.inputEl.value]);
1306 } else if (tui.mode.name == 'play') {
1307 if (tui.mode.mode_switch_on_key(event)) {
1309 } else if (event.key === tui.keys.take_thing
1310 && game.tasks.includes('PICK_UP')) {
1311 server.send(["TASK:PICK_UP"]);
1312 } else if (event.key === tui.keys.drop_thing
1313 && game.tasks.includes('DROP')) {
1314 server.send(["TASK:DROP"]);
1315 } else if (event.key in tui.movement_keys
1316 && game.tasks.includes('MOVE')) {
1317 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1318 } else if (event.key === tui.keys.teleport) {
1321 } else if (tui.mode.name == 'study') {
1322 if (tui.mode.mode_switch_on_key(event)) {
1324 } else if (event.key in tui.movement_keys) {
1325 explorer.move(tui.movement_keys[event.key]);
1326 } else if (event.key == tui.keys.toggle_map_mode) {
1327 tui.toggle_map_mode();
1329 } else if (tui.mode.name == 'control_tile_draw') {
1330 if (tui.mode.mode_switch_on_key(event)) {
1332 } else if (event.key in tui.movement_keys) {
1333 explorer.move(tui.movement_keys[event.key]);
1334 } else if (event.key === tui.keys.toggle_tile_draw) {
1335 tui.toggle_tile_draw();
1337 } else if (tui.mode.name == 'admin') {
1338 if (tui.mode.mode_switch_on_key(event)) {
1341 } else if (tui.mode.name == 'edit') {
1342 if (tui.mode.mode_switch_on_key(event)) {
1344 } else if (event.key in tui.movement_keys
1345 && game.tasks.includes('MOVE')) {
1346 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1347 } else if (event.key === tui.keys.flatten
1348 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1349 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1355 rows_selector.addEventListener('input', function() {
1356 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1359 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1360 terminal.initialize();
1363 cols_selector.addEventListener('input', function() {
1364 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1367 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1368 terminal.initialize();
1369 tui.window_width = terminal.cols / 2,
1372 for (let key_selector of key_selectors) {
1373 key_selector.addEventListener('input', function() {
1374 window.localStorage.setItem(key_selector.id, key_selector.value);
1378 window.setInterval(function() {
1379 if (server.connected) {
1380 server.send(['PING']);
1382 server.reconnect_to(server.url);
1383 tui.log_msg('@ attempting reconnect …')
1386 window.setInterval(function() {
1388 if (document.activeElement == tui.inputEl) {
1389 val = "on (click outside terminal to change)";
1391 val = "off (click into terminal to change)";
1393 document.getElementById("keyboard_control").textContent = val;
1395 document.getElementById("terminal").onclick = function() {
1396 tui.inputEl.focus();
1398 document.getElementById("help").onclick = function() {
1399 tui.show_help = true;
1402 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1403 const mode = switchEl.id.slice("switch_to_".length);
1404 switchEl.onclick = function() {
1405 tui.switch_mode(mode);
1409 document.getElementById("toggle_tile_draw").onclick = function() {
1410 tui.toggle_tile_draw();
1412 document.getElementById("toggle_map_mode").onclick = function() {
1413 tui.toggle_map_mode();
1416 document.getElementById("take_thing").onclick = function() {
1417 server.send(['TASK:PICK_UP']);
1419 document.getElementById("drop_thing").onclick = function() {
1420 server.send(['TASK:DROP']);
1422 document.getElementById("flatten").onclick = function() {
1423 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1425 document.getElementById("teleport").onclick = function() {
1428 document.getElementById("move_upleft").onclick = function() {
1429 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1430 server.send(['TASK:MOVE', 'UPLEFT']);
1432 explorer.move('UPLEFT');
1435 document.getElementById("move_left").onclick = function() {
1436 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1437 server.send(['TASK:MOVE', 'LEFT']);
1439 explorer.move('LEFT');
1442 document.getElementById("move_downleft").onclick = function() {
1443 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1444 server.send(['TASK:MOVE', 'DOWNLEFT']);
1446 explorer.move('DOWNLEFT');
1449 document.getElementById("move_down").onclick = function() {
1450 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1451 server.send(['TASK:MOVE', 'DOWN']);
1453 explorer.move('DOWN');
1456 document.getElementById("move_up").onclick = function() {
1457 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1458 server.send(['TASK:MOVE', 'UP']);
1460 explorer.move('UP');
1463 document.getElementById("move_upright").onclick = function() {
1464 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1465 server.send(['TASK:MOVE', 'UPRIGHT']);
1467 explorer.move('UPRIGHT');
1470 document.getElementById("move_right").onclick = function() {
1471 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1472 server.send(['TASK:MOVE', 'RIGHT']);
1474 explorer.move('RIGHT');
1477 document.getElementById("move_downright").onclick = function() {
1478 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1479 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1481 explorer.move('DOWNRIGHT');