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 control 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 control',
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 control',
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 this.inputEl.focus();
614 this.map_mode = 'terrain + things';
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 == 'study') {
680 document.getElementById("toggle_map_mode").disabled = false;
681 } else if (this.mode.is_single_char_entry) {
682 this.show_help = true;
683 } else if (this.mode.name == 'admin_enter') {
684 this.log_msg('@ enter admin password:')
685 } else if (this.mode.name == 'control_pw_type') {
686 this.log_msg('@ enter tile control character for which you want to change the password:')
687 } else if (this.mode.name == 'control_tile_type') {
688 this.log_msg('@ enter tile control character which you want to draw:')
689 } else if (this.mode.name == 'control_pw_pw') {
690 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
691 } else if (this.mode.name == 'control_tile_draw') {
692 document.getElementById("toggle_tile_draw").disabled = false;
693 this.log_msg('@ can draw tile control character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
697 offset_links: function(offset, links) {
698 for (let y in links) {
699 let real_y = offset[0] + parseInt(y);
700 if (!this.links[real_y]) {
701 this.links[real_y] = [];
703 for (let link of links[y]) {
704 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
705 this.links[real_y].push(offset_link);
709 restore_input_values: function() {
710 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
711 let info = explorer.info_db[explorer.position];
712 if (info != "(none)") {
713 this.inputEl.value = info;
714 this.recalc_input_lines();
716 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
717 let portal = game.portals[explorer.position]
718 this.inputEl.value = portal;
719 this.recalc_input_lines();
720 } else if (this.mode.name == 'password') {
721 this.inputEl.value = this.password;
722 this.recalc_input_lines();
725 empty_input: function(str) {
726 this.inputEl.value = "";
727 if (this.mode.has_input_prompt) {
728 this.recalc_input_lines();
730 this.height_input = 0;
733 recalc_input_lines: function() {
735 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
736 this.height_input = this.input_lines.length;
738 msg_into_lines_of_width: function(msg, width) {
739 function push_inner_link(y, end_x) {
740 if (!inner_links[y]) {
743 inner_links[y].push([url_start_x, end_x, url]);
745 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
748 for (const match of matches) {
749 const url = match[0];
750 const url_start = match.index;
751 const url_end = match.index + match[0].length;
752 link_data[url_start] = url;
753 url_ends.push(url_end);
757 let inner_links = {};
761 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
762 if (x >= width || msg[i] == "\n") {
764 push_inner_link(y, chunk.length);
770 if (msg[i] == "\n") {
775 if (msg[i] != "\n") {
778 if (i in link_data) {
782 } else if (url_ends.includes(i)) {
783 push_inner_link(y, x);
789 push_inner_link(lines.length - 1, chunk.length);
791 return [lines, inner_links];
793 log_msg: function(msg) {
795 while (this.log.length > 100) {
800 draw_map: function() {
801 let map_lines_split = [];
803 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
804 if (j == game.map_size[1]) {
805 map_lines_split.push(line);
809 if (['edit', 'write', 'control_tile_draw',
810 'control_tile_type'].includes(this.mode.name)) {
811 line.push(game.map[i] + game.map_control[i]);
813 line.push(game.map[i] + ' ');
816 map_lines_split.push(line);
817 if (this.map_mode == 'terrain + annotations') {
818 for (const coordinate of explorer.info_hints) {
819 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
821 } else if (this.map_mode == 'terrain + things') {
822 for (const p in game.portals) {
823 let coordinate = p.split(',')
824 let original = map_lines_split[coordinate[0]][coordinate[1]];
825 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
827 let used_positions = [];
828 for (const thing_id in game.things) {
829 let t = game.things[thing_id];
830 let symbol = game.thing_types[t.type_];
833 meta_char = t.player_char;
835 if (used_positions.includes(t.position.toString())) {
838 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
839 used_positions.push(t.position.toString());
842 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
843 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
846 if (game.map_geometry == 'Square') {
847 for (let line_split of map_lines_split) {
848 map_lines.push(line_split.join(''));
850 } else if (game.map_geometry == 'Hex') {
852 for (let line_split of map_lines_split) {
853 map_lines.push(' '.repeat(indent) + line_split.join(''));
861 let window_center = [terminal.rows / 2, this.window_width / 2];
862 let player = game.things[game.player_id];
863 let center_position = [player.position[0], player.position[1]];
864 if (tui.mode.shows_info) {
865 center_position = [explorer.position[0], explorer.position[1]];
867 center_position[1] = center_position[1] * 2;
868 let offset = [center_position[0] - window_center[0],
869 center_position[1] - window_center[1]]
870 if (game.map_geometry == 'Hex' && offset[0] % 2) {
873 let term_y = Math.max(0, -offset[0]);
874 let term_x = Math.max(0, -offset[1]);
875 let map_y = Math.max(0, offset[0]);
876 let map_x = Math.max(0, offset[1]);
877 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
878 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
879 terminal.write(term_y, term_x, to_draw);
882 draw_mode_line: function() {
883 let help = 'hit [' + this.keys.help + '] for help';
884 if (this.mode.has_input_prompt) {
885 help = 'enter /help for help';
887 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
889 draw_turn_line: function(n) {
890 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
892 draw_history: function() {
893 let log_display_lines = [];
895 let y_offset_in_log = 0;
896 for (let line of this.log) {
897 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
899 log_display_lines = log_display_lines.concat(new_lines);
900 for (const y in link_data) {
901 const rel_y = y_offset_in_log + parseInt(y);
902 log_links[rel_y] = [];
903 for (let link of link_data[y]) {
904 log_links[rel_y].push(link);
907 y_offset_in_log += new_lines.length;
909 let i = log_display_lines.length - 1;
910 for (let y = terminal.rows - 1 - this.height_input;
911 y >= this.height_header && i >= 0;
913 terminal.write(y, this.window_width, log_display_lines[i]);
915 for (const key of Object.keys(log_links)) {
916 if (parseInt(key) <= i) {
917 delete log_links[key];
920 let offset = [terminal.rows - this.height_input - log_display_lines.length,
922 this.offset_links(offset, log_links);
924 draw_info: function() {
925 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
927 let offset = [this.height_header, this.window_width];
928 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
929 terminal.write(y, offset[1], lines[i]);
931 this.offset_links(offset, link_data);
933 draw_input: function() {
934 if (this.mode.has_input_prompt) {
935 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
936 terminal.write(y, this.window_width, this.input_lines[i]);
940 draw_help: function() {
941 let movement_keys_desc = '';
942 if (!this.mode.is_intro) {
943 movement_keys_desc = Object.keys(this.movement_keys).join(',');
945 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
946 if (this.mode.name == 'play') {
947 content += "Available actions:\n";
948 if (game.tasks.includes('MOVE')) {
949 content += "[" + movement_keys_desc + "] – move player\n";
951 if (game.tasks.includes('PICK_UP')) {
952 content += "[" + this.keys.take_thing + "] – pick up thing\n";
954 if (game.tasks.includes('DROP')) {
955 content += "[" + this.keys.drop_thing + "] – drop thing\n";
957 content += "[" + tui.keys.teleport + "] – teleport\n";
959 } else if (this.mode.name == 'study') {
960 content += "Available actions:\n";
961 content += '[' + movement_keys_desc + '] – move question mark\n';
962 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
964 } else if (this.mode.name == 'edit') {
965 content += "Available actions:\n";
966 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
967 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
970 } else if (this.mode.name == 'control_tile_draw') {
971 content += "Available actions:\n";
972 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
974 } else if (this.mode.name == 'chat') {
975 content += '/nick NAME – re-name yourself to NAME\n';
976 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
977 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
978 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
979 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
981 content += this.mode.list_available_modes();
983 if (!this.mode.has_input_prompt) {
984 start_x = this.window_width
986 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
987 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
988 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
989 terminal.write(y, start_x, lines[i]);
992 toggle_tile_draw: function() {
994 tui.tile_draw = false;
996 tui.tile_draw = true;
999 toggle_map_mode: function() {
1000 if (tui.map_mode == 'terrain only') {
1001 tui.map_mode = 'terrain + annotations';
1002 } else if (tui.map_mode == 'terrain + annotations') {
1003 tui.map_mode = 'terrain + things';
1005 tui.map_mode = 'terrain only';
1008 full_refresh: function() {
1010 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1011 if (this.mode.is_intro) {
1012 this.draw_history();
1015 if (game.turn_complete) {
1017 this.draw_turn_line();
1019 this.draw_mode_line();
1020 if (this.mode.shows_info) {
1023 this.draw_history();
1027 if (this.show_help) {
1039 this.map_control = "";
1040 this.map_size = [0,0];
1041 this.player_id = -1;
1045 get_thing: function(id_, create_if_not_found=false) {
1046 if (id_ in game.things) {
1047 return game.things[id_];
1048 } else if (create_if_not_found) {
1049 let t = new Thing([0,0]);
1050 game.things[id_] = t;
1054 move: function(start_position, direction) {
1055 let target = [start_position[0], start_position[1]];
1056 if (direction == 'LEFT') {
1058 } else if (direction == 'RIGHT') {
1060 } else if (game.map_geometry == 'Square') {
1061 if (direction == 'UP') {
1063 } else if (direction == 'DOWN') {
1066 } else if (game.map_geometry == 'Hex') {
1067 let start_indented = start_position[0] % 2;
1068 if (direction == 'UPLEFT') {
1070 if (!start_indented) {
1073 } else if (direction == 'UPRIGHT') {
1075 if (start_indented) {
1078 } else if (direction == 'DOWNLEFT') {
1080 if (!start_indented) {
1083 } else if (direction == 'DOWNRIGHT') {
1085 if (start_indented) {
1090 if (target[0] < 0 || target[1] < 0 ||
1091 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1096 teleport: function() {
1097 let player = this.get_thing(game.player_id);
1098 if (player.position in this.portals) {
1099 server.reconnect_to(this.portals[player.position]);
1101 terminal.blink_screen();
1102 tui.log_msg('? not standing on portal')
1110 server.init(websocket_location);
1116 move: function(direction) {
1117 let target = game.move(this.position, direction);
1119 this.position = target
1120 if (tui.mode.shows_info) {
1122 } else if (tui.tile_draw) {
1123 this.send_tile_control_command();
1126 terminal.blink_screen();
1129 update_info_db: function(yx, str) {
1130 this.info_db[yx] = str;
1131 if (tui.mode.name == 'study') {
1135 empty_info_db: function() {
1137 this.info_hints = [];
1138 if (tui.mode.name == 'study') {
1142 query_info: function() {
1143 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1145 get_info: function() {
1146 let info = "MAP VIEW: " + tui.map_mode + "\n";
1147 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1148 if (game.fov[position_i] != '.') {
1149 return info + 'outside field of view';
1151 let terrain_char = game.map[position_i]
1152 let terrain_desc = '?'
1153 if (game.terrains[terrain_char]) {
1154 terrain_desc = game.terrains[terrain_char];
1156 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1157 let protection = game.map_control[position_i];
1158 if (protection == '.') {
1159 protection = 'unprotected';
1161 info += 'PROTECTION: ' + protection + '\n';
1162 for (let t_id in game.things) {
1163 let t = game.things[t_id];
1164 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1165 let symbol = game.thing_types[t.type_];
1166 info += "THING: " + t.type_ + " / " + symbol;
1167 if (t.player_char) {
1168 info += t.player_char;
1171 info += " (" + t.name_ + ")";
1176 if (this.position in game.portals) {
1177 info += "PORTAL: " + game.portals[this.position] + "\n";
1179 if (this.position in this.info_db) {
1180 info += "ANNOTATIONS: " + this.info_db[this.position];
1182 info += 'waiting …';
1186 annotate: function(msg) {
1187 if (msg.length == 0) {
1188 msg = " "; // triggers annotation deletion
1190 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1192 set_portal: function(msg) {
1193 if (msg.length == 0) {
1194 msg = " "; // triggers portal deletion
1196 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1198 send_tile_control_command: function() {
1199 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1203 tui.inputEl.addEventListener('input', (event) => {
1204 if (tui.mode.has_input_prompt) {
1205 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1206 if (tui.inputEl.value.length > max_length) {
1207 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1209 tui.recalc_input_lines();
1210 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1211 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1212 tui.switch_mode('edit');
1216 document.onclick = function() {
1217 tui.show_help = false;
1219 tui.inputEl.addEventListener('keydown', (event) => {
1220 tui.show_help = false;
1221 if (event.key == 'Enter') {
1222 event.preventDefault();
1224 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1225 tui.show_help = true;
1227 tui.restore_input_values();
1228 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1229 && !tui.mode.is_single_char_entry) {
1230 tui.show_help = true;
1231 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1232 tui.login_name = tui.inputEl.value;
1233 server.send(['LOGIN', tui.inputEl.value]);
1235 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1236 if (tui.inputEl.value.length == 0) {
1237 tui.log_msg('@ aborted');
1239 server.send(['SET_MAP_CONTROL_PASSWORD',
1240 tui.tile_control_char, tui.inputEl.value]);
1242 tui.switch_mode('admin');
1243 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1244 explorer.set_portal(tui.inputEl.value);
1245 tui.switch_mode('edit');
1246 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1247 explorer.annotate(tui.inputEl.value);
1248 tui.switch_mode('edit');
1249 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1250 if (tui.inputEl.value.length == 0) {
1251 tui.inputEl.value = " ";
1253 tui.password = tui.inputEl.value
1254 tui.switch_mode('edit');
1255 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1256 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1257 tui.switch_mode('play');
1258 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1259 if (tui.inputEl.value.length != 1) {
1260 tui.log_msg('@ entered non-single-char, therefore aborted');
1261 tui.switch_mode('admin');
1263 tui.tile_control_char = tui.inputEl.value[0];
1264 tui.switch_mode('control_pw_pw');
1266 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1267 if (tui.inputEl.value.length != 1) {
1268 tui.log_msg('@ entered non-single-char, therefore aborted');
1269 tui.switch_mode('admin');
1271 tui.tile_control_char = tui.inputEl.value[0];
1272 tui.switch_mode('control_tile_draw');
1274 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1275 let tokens = parser.tokenize(tui.inputEl.value);
1276 if (tokens.length > 0 && tokens[0].length > 0) {
1277 if (tui.inputEl.value[0][0] == '/') {
1278 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1279 tui.switch_mode('play');
1280 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1281 tui.switch_mode('study');
1282 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1283 tui.switch_mode('edit');
1284 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1285 tui.switch_mode('admin_enter');
1286 } else if (tokens[0].slice(1) == 'nick') {
1287 if (tokens.length > 1) {
1288 server.send(['NICK', tokens[1]]);
1290 tui.log_msg('? need new name');
1293 tui.log_msg('? unknown command');
1296 server.send(['ALL', tui.inputEl.value]);
1298 } else if (tui.inputEl.valuelength > 0) {
1299 server.send(['ALL', tui.inputEl.value]);
1302 } else if (tui.mode.name == 'play') {
1303 if (tui.mode.mode_switch_on_key(event)) {
1305 } else if (event.key === tui.keys.take_thing
1306 && game.tasks.includes('PICK_UP')) {
1307 server.send(["TASK:PICK_UP"]);
1308 } else if (event.key === tui.keys.drop_thing
1309 && game.tasks.includes('DROP')) {
1310 server.send(["TASK:DROP"]);
1311 } else if (event.key in tui.movement_keys
1312 && game.tasks.includes('MOVE')) {
1313 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1314 } else if (event.key === tui.keys.teleport) {
1317 } else if (tui.mode.name == 'study') {
1318 if (tui.mode.mode_switch_on_key(event)) {
1320 } else if (event.key in tui.movement_keys) {
1321 explorer.move(tui.movement_keys[event.key]);
1322 } else if (event.key == tui.keys.toggle_map_mode) {
1323 tui.toggle_map_mode();
1325 } else if (tui.mode.name == 'control_tile_draw') {
1326 if (tui.mode.mode_switch_on_key(event)) {
1328 } else if (event.key in tui.movement_keys) {
1329 explorer.move(tui.movement_keys[event.key]);
1330 } else if (event.key === tui.keys.toggle_tile_draw) {
1331 tui.toggle_tile_draw();
1333 } else if (tui.mode.name == 'admin') {
1334 if (tui.mode.mode_switch_on_key(event)) {
1337 } else if (tui.mode.name == 'edit') {
1338 if (tui.mode.mode_switch_on_key(event)) {
1340 } else if (event.key in tui.movement_keys
1341 && game.tasks.includes('MOVE')) {
1342 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1343 } else if (event.key === tui.keys.flatten
1344 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1345 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1351 rows_selector.addEventListener('input', function() {
1352 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1355 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1356 terminal.initialize();
1359 cols_selector.addEventListener('input', function() {
1360 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1363 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1364 terminal.initialize();
1365 tui.window_width = terminal.cols / 2,
1368 for (let key_selector of key_selectors) {
1369 key_selector.addEventListener('input', function() {
1370 window.localStorage.setItem(key_selector.id, key_selector.value);
1374 window.setInterval(function() {
1375 if (server.connected) {
1376 server.send(['PING']);
1378 server.reconnect_to(server.url);
1379 tui.log_msg('@ attempting reconnect …')
1382 window.setInterval(function() {
1384 if (document.activeElement == tui.inputEl) {
1385 val = "on (click outside terminal to change)";
1387 val = "off (click into terminal to change)";
1389 document.getElementById("keyboard_control").textContent = val;
1391 document.getElementById("terminal").onclick = function() {
1392 tui.inputEl.focus();
1394 document.getElementById("help").onclick = function() {
1395 tui.show_help = true;
1398 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1399 const mode = switchEl.id.slice("switch_to_".length);
1400 switchEl.onclick = function() {
1401 tui.switch_mode(mode);
1405 document.getElementById("toggle_tile_draw").onclick = function() {
1406 tui.toggle_tile_draw();
1408 document.getElementById("toggle_map_mode").onclick = function() {
1409 tui.toggle_map_mode();
1412 document.getElementById("take_thing").onclick = function() {
1413 server.send(['TASK:PICK_UP']);
1415 document.getElementById("drop_thing").onclick = function() {
1416 server.send(['TASK:DROP']);
1418 document.getElementById("flatten").onclick = function() {
1419 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1421 document.getElementById("teleport").onclick = function() {
1424 document.getElementById("move_upleft").onclick = function() {
1425 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1426 server.send(['TASK:MOVE', 'UPLEFT']);
1428 explorer.move('UPLEFT');
1431 document.getElementById("move_left").onclick = function() {
1432 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1433 server.send(['TASK:MOVE', 'LEFT']);
1435 explorer.move('LEFT');
1438 document.getElementById("move_downleft").onclick = function() {
1439 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1440 server.send(['TASK:MOVE', 'DOWNLEFT']);
1442 explorer.move('DOWNLEFT');
1445 document.getElementById("move_down").onclick = function() {
1446 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1447 server.send(['TASK:MOVE', 'DOWN']);
1449 explorer.move('DOWN');
1452 document.getElementById("move_up").onclick = function() {
1453 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1454 server.send(['TASK:MOVE', 'UP']);
1456 explorer.move('UP');
1459 document.getElementById("move_upright").onclick = function() {
1460 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1461 server.send(['TASK:MOVE', 'UPRIGHT']);
1463 explorer.move('UPRIGHT');
1466 document.getElementById("move_right").onclick = function() {
1467 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1468 server.send(['TASK:MOVE', 'RIGHT']);
1470 explorer.move('RIGHT');
1473 document.getElementById("move_downright").onclick = function() {
1474 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1475 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1477 explorer.move('DOWNRIGHT');