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.map_mode = 'terrain + things';
617 this.tile_draw = false;
618 if (mode_name == 'admin_enter' && this.is_admin) {
621 this.mode = this['mode_' + mode_name];
622 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
623 this.inputEl.focus();
625 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
626 explorer.position = game.things[game.player_id].position;
627 if (this.mode.shows_info) {
628 explorer.query_info();
632 this.restore_input_values();
633 for (let el of document.getElementsByTagName("button")) {
636 document.getElementById("help").disabled = false;
637 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
638 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
639 move_key.disabled = false;
642 if (!this.mode.is_intro && this.mode.name != 'play') {
643 document.getElementById("switch_to_play").disabled = false;
645 if (!this.mode.is_intro && this.mode.name != 'study') {
646 document.getElementById("switch_to_study").disabled = false;
648 if (!this.mode.is_intro && this.mode.name != 'chat') {
649 document.getElementById("switch_to_chat").disabled = false;
651 if (!this.mode.is_intro && this.mode.name != 'edit') {
652 document.getElementById("switch_to_edit").disabled = false;
654 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
655 document.getElementById("switch_to_admin_enter").disabled = false;
657 if (this.mode.name == 'login') {
658 if (this.login_name) {
659 server.send(['LOGIN', this.login_name]);
661 this.log_msg("? need login name");
663 } else if (this.mode.name == 'play') {
664 if (game.tasks.includes('PICK_UP')) {
665 document.getElementById("take_thing").disabled = false;
667 if (game.tasks.includes('DROP')) {
668 document.getElementById("drop_thing").disabled = false;
670 if (game.tasks.includes('MOVE')) {
672 document.getElementById("teleport").disabled = false;
673 } else if (this.mode.name == 'edit') {
674 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
675 document.getElementById("flatten").disabled = false;
677 document.getElementById("switch_to_annotate").disabled = false;
678 document.getElementById("switch_to_write").disabled = false;
679 document.getElementById("switch_to_portal").disabled = false;
680 document.getElementById("switch_to_password").disabled = false;
681 } else if (this.mode.name == 'admin') {
682 document.getElementById("switch_to_control_pw_type").disabled = false;
683 document.getElementById("switch_to_control_tile_type").disabled = false;
684 } else if (this.mode.name == 'study') {
685 document.getElementById("toggle_map_mode").disabled = false;
686 } else if (this.mode.is_single_char_entry) {
687 this.show_help = true;
688 } else if (this.mode.name == 'admin_enter') {
689 this.log_msg('@ enter admin password:')
690 } else if (this.mode.name == 'control_pw_type') {
691 this.log_msg('@ enter tile protection character for which you want to change the password:')
692 } else if (this.mode.name == 'control_tile_type') {
693 this.log_msg('@ enter tile protection character which you want to draw:')
694 } else if (this.mode.name == 'control_pw_pw') {
695 this.log_msg('@ enter tile protection password for "' + this.tile_control_char + '":');
696 } else if (this.mode.name == 'control_tile_draw') {
697 document.getElementById("toggle_tile_draw").disabled = false;
698 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 + '].')
702 offset_links: function(offset, links) {
703 for (let y in links) {
704 let real_y = offset[0] + parseInt(y);
705 if (!this.links[real_y]) {
706 this.links[real_y] = [];
708 for (let link of links[y]) {
709 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
710 this.links[real_y].push(offset_link);
714 restore_input_values: function() {
715 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
716 let info = explorer.info_db[explorer.position];
717 if (info != "(none)") {
718 this.inputEl.value = info;
719 this.recalc_input_lines();
721 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
722 let portal = game.portals[explorer.position]
723 this.inputEl.value = portal;
724 this.recalc_input_lines();
725 } else if (this.mode.name == 'password') {
726 this.inputEl.value = this.password;
727 this.recalc_input_lines();
730 empty_input: function(str) {
731 this.inputEl.value = "";
732 if (this.mode.has_input_prompt) {
733 this.recalc_input_lines();
735 this.height_input = 0;
738 recalc_input_lines: function() {
740 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
741 this.height_input = this.input_lines.length;
743 msg_into_lines_of_width: function(msg, width) {
744 function push_inner_link(y, end_x) {
745 if (!inner_links[y]) {
748 inner_links[y].push([url_start_x, end_x, url]);
750 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
753 for (const match of matches) {
754 const url = match[0];
755 const url_start = match.index;
756 const url_end = match.index + match[0].length;
757 link_data[url_start] = url;
758 url_ends.push(url_end);
762 let inner_links = {};
766 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
767 if (x >= width || msg[i] == "\n") {
769 push_inner_link(y, chunk.length);
775 if (msg[i] == "\n") {
780 if (msg[i] != "\n") {
783 if (i in link_data) {
787 } else if (url_ends.includes(i)) {
788 push_inner_link(y, x);
794 push_inner_link(lines.length - 1, chunk.length);
796 return [lines, inner_links];
798 log_msg: function(msg) {
800 while (this.log.length > 100) {
805 draw_map: function() {
806 let map_lines_split = [];
808 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
809 if (j == game.map_size[1]) {
810 map_lines_split.push(line);
814 if (['edit', 'write', 'control_tile_draw',
815 'control_tile_type'].includes(this.mode.name)) {
816 line.push(game.map[i] + game.map_control[i]);
818 line.push(game.map[i] + ' ');
821 map_lines_split.push(line);
822 if (this.map_mode == 'terrain + annotations') {
823 for (const coordinate of explorer.info_hints) {
824 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
826 } else if (this.map_mode == 'terrain + things') {
827 for (const p in game.portals) {
828 let coordinate = p.split(',')
829 let original = map_lines_split[coordinate[0]][coordinate[1]];
830 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
832 let used_positions = [];
833 for (const thing_id in game.things) {
834 let t = game.things[thing_id];
835 let symbol = game.thing_types[t.type_];
838 meta_char = t.player_char;
840 if (used_positions.includes(t.position.toString())) {
843 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
844 used_positions.push(t.position.toString());
847 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
848 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
851 if (game.map_geometry == 'Square') {
852 for (let line_split of map_lines_split) {
853 map_lines.push(line_split.join(''));
855 } else if (game.map_geometry == 'Hex') {
857 for (let line_split of map_lines_split) {
858 map_lines.push(' '.repeat(indent) + line_split.join(''));
866 let window_center = [terminal.rows / 2, this.window_width / 2];
867 let player = game.things[game.player_id];
868 let center_position = [player.position[0], player.position[1]];
869 if (tui.mode.shows_info) {
870 center_position = [explorer.position[0], explorer.position[1]];
872 center_position[1] = center_position[1] * 2;
873 let offset = [center_position[0] - window_center[0],
874 center_position[1] - window_center[1]]
875 if (game.map_geometry == 'Hex' && offset[0] % 2) {
878 let term_y = Math.max(0, -offset[0]);
879 let term_x = Math.max(0, -offset[1]);
880 let map_y = Math.max(0, offset[0]);
881 let map_x = Math.max(0, offset[1]);
882 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
883 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
884 terminal.write(term_y, term_x, to_draw);
887 draw_mode_line: function() {
888 let help = 'hit [' + this.keys.help + '] for help';
889 if (this.mode.has_input_prompt) {
890 help = 'enter /help for help';
892 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
894 draw_turn_line: function(n) {
895 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
897 draw_history: function() {
898 let log_display_lines = [];
900 let y_offset_in_log = 0;
901 for (let line of this.log) {
902 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
904 log_display_lines = log_display_lines.concat(new_lines);
905 for (const y in link_data) {
906 const rel_y = y_offset_in_log + parseInt(y);
907 log_links[rel_y] = [];
908 for (let link of link_data[y]) {
909 log_links[rel_y].push(link);
912 y_offset_in_log += new_lines.length;
914 let i = log_display_lines.length - 1;
915 for (let y = terminal.rows - 1 - this.height_input;
916 y >= this.height_header && i >= 0;
918 terminal.write(y, this.window_width, log_display_lines[i]);
920 for (const key of Object.keys(log_links)) {
921 if (parseInt(key) <= i) {
922 delete log_links[key];
925 let offset = [terminal.rows - this.height_input - log_display_lines.length,
927 this.offset_links(offset, log_links);
929 draw_info: function() {
930 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
932 let offset = [this.height_header, this.window_width];
933 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
934 terminal.write(y, offset[1], lines[i]);
936 this.offset_links(offset, link_data);
938 draw_input: function() {
939 if (this.mode.has_input_prompt) {
940 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
941 terminal.write(y, this.window_width, this.input_lines[i]);
945 draw_help: function() {
946 let movement_keys_desc = '';
947 if (!this.mode.is_intro) {
948 movement_keys_desc = Object.keys(this.movement_keys).join(',');
950 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
951 if (this.mode.name == 'play') {
952 content += "Available actions:\n";
953 if (game.tasks.includes('MOVE')) {
954 content += "[" + movement_keys_desc + "] – move player\n";
956 if (game.tasks.includes('PICK_UP')) {
957 content += "[" + this.keys.take_thing + "] – pick up thing\n";
959 if (game.tasks.includes('DROP')) {
960 content += "[" + this.keys.drop_thing + "] – drop thing\n";
962 content += "[" + tui.keys.teleport + "] – teleport\n";
964 } else if (this.mode.name == 'study') {
965 content += "Available actions:\n";
966 content += '[' + movement_keys_desc + '] – move question mark\n';
967 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
969 } else if (this.mode.name == 'edit') {
970 content += "Available actions:\n";
971 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
972 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
975 } else if (this.mode.name == 'control_tile_draw') {
976 content += "Available actions:\n";
977 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
979 } else if (this.mode.name == 'chat') {
980 content += '/nick NAME – re-name yourself to NAME\n';
981 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
982 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
983 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
984 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
986 content += this.mode.list_available_modes();
988 if (!this.mode.has_input_prompt) {
989 start_x = this.window_width
991 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
992 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
993 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
994 terminal.write(y, start_x, lines[i]);
997 toggle_tile_draw: function() {
999 tui.tile_draw = false;
1001 tui.tile_draw = true;
1004 toggle_map_mode: function() {
1005 if (tui.map_mode == 'terrain only') {
1006 tui.map_mode = 'terrain + annotations';
1007 } else if (tui.map_mode == 'terrain + annotations') {
1008 tui.map_mode = 'terrain + things';
1010 tui.map_mode = 'terrain only';
1013 full_refresh: function() {
1015 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1016 if (this.mode.is_intro) {
1017 this.draw_history();
1020 if (game.turn_complete) {
1022 this.draw_turn_line();
1024 this.draw_mode_line();
1025 if (this.mode.shows_info) {
1028 this.draw_history();
1032 if (this.show_help) {
1044 this.map_control = "";
1045 this.map_size = [0,0];
1046 this.player_id = -1;
1050 get_thing: function(id_, create_if_not_found=false) {
1051 if (id_ in game.things) {
1052 return game.things[id_];
1053 } else if (create_if_not_found) {
1054 let t = new Thing([0,0]);
1055 game.things[id_] = t;
1059 move: function(start_position, direction) {
1060 let target = [start_position[0], start_position[1]];
1061 if (direction == 'LEFT') {
1063 } else if (direction == 'RIGHT') {
1065 } else if (game.map_geometry == 'Square') {
1066 if (direction == 'UP') {
1068 } else if (direction == 'DOWN') {
1071 } else if (game.map_geometry == 'Hex') {
1072 let start_indented = start_position[0] % 2;
1073 if (direction == 'UPLEFT') {
1075 if (!start_indented) {
1078 } else if (direction == 'UPRIGHT') {
1080 if (start_indented) {
1083 } else if (direction == 'DOWNLEFT') {
1085 if (!start_indented) {
1088 } else if (direction == 'DOWNRIGHT') {
1090 if (start_indented) {
1095 if (target[0] < 0 || target[1] < 0 ||
1096 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1101 teleport: function() {
1102 let player = this.get_thing(game.player_id);
1103 if (player.position in this.portals) {
1104 server.reconnect_to(this.portals[player.position]);
1106 terminal.blink_screen();
1107 tui.log_msg('? not standing on portal')
1115 server.init(websocket_location);
1121 move: function(direction) {
1122 let target = game.move(this.position, direction);
1124 this.position = target
1125 if (tui.mode.shows_info) {
1127 } else if (tui.tile_draw) {
1128 this.send_tile_control_command();
1131 terminal.blink_screen();
1134 update_info_db: function(yx, str) {
1135 this.info_db[yx] = str;
1136 if (tui.mode.name == 'study') {
1140 empty_info_db: function() {
1142 this.info_hints = [];
1143 if (tui.mode.name == 'study') {
1147 query_info: function() {
1148 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1150 get_info: function() {
1151 let info = "MAP VIEW: " + tui.map_mode + "\n";
1152 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1153 if (game.fov[position_i] != '.') {
1154 return info + 'outside field of view';
1156 let terrain_char = game.map[position_i]
1157 let terrain_desc = '?'
1158 if (game.terrains[terrain_char]) {
1159 terrain_desc = game.terrains[terrain_char];
1161 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1162 let protection = game.map_control[position_i];
1163 if (protection == '.') {
1164 protection = 'unprotected';
1166 info += 'PROTECTION: ' + protection + '\n';
1167 for (let t_id in game.things) {
1168 let t = game.things[t_id];
1169 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1170 let symbol = game.thing_types[t.type_];
1171 info += "THING: " + t.type_ + " / " + symbol;
1172 if (t.player_char) {
1173 info += t.player_char;
1176 info += " (" + t.name_ + ")";
1181 if (this.position in game.portals) {
1182 info += "PORTAL: " + game.portals[this.position] + "\n";
1184 if (this.position in this.info_db) {
1185 info += "ANNOTATIONS: " + this.info_db[this.position];
1187 info += 'waiting …';
1191 annotate: function(msg) {
1192 if (msg.length == 0) {
1193 msg = " "; // triggers annotation deletion
1195 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1197 set_portal: function(msg) {
1198 if (msg.length == 0) {
1199 msg = " "; // triggers portal deletion
1201 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1203 send_tile_control_command: function() {
1204 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1208 tui.inputEl.addEventListener('input', (event) => {
1209 if (tui.mode.has_input_prompt) {
1210 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1211 if (tui.inputEl.value.length > max_length) {
1212 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1214 tui.recalc_input_lines();
1215 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1216 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1217 tui.switch_mode('edit');
1221 document.onclick = function() {
1222 tui.show_help = false;
1224 tui.inputEl.addEventListener('keydown', (event) => {
1225 tui.show_help = false;
1226 if (event.key == 'Enter') {
1227 event.preventDefault();
1229 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1230 tui.show_help = true;
1232 tui.restore_input_values();
1233 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1234 && !tui.mode.is_single_char_entry) {
1235 tui.show_help = true;
1236 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1237 tui.login_name = tui.inputEl.value;
1238 server.send(['LOGIN', tui.inputEl.value]);
1240 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1241 if (tui.inputEl.value.length == 0) {
1242 tui.log_msg('@ aborted');
1244 server.send(['SET_MAP_CONTROL_PASSWORD',
1245 tui.tile_control_char, tui.inputEl.value]);
1246 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1248 tui.switch_mode('admin');
1249 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1250 explorer.set_portal(tui.inputEl.value);
1251 tui.switch_mode('edit');
1252 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1253 explorer.annotate(tui.inputEl.value);
1254 tui.switch_mode('edit');
1255 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1256 if (tui.inputEl.value.length == 0) {
1257 tui.inputEl.value = " ";
1259 tui.password = tui.inputEl.value
1260 tui.switch_mode('edit');
1261 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1262 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1263 tui.switch_mode('play');
1264 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1265 if (tui.inputEl.value.length != 1) {
1266 tui.log_msg('@ entered non-single-char, therefore aborted');
1267 tui.switch_mode('admin');
1269 tui.tile_control_char = tui.inputEl.value[0];
1270 tui.switch_mode('control_pw_pw');
1272 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1273 if (tui.inputEl.value.length != 1) {
1274 tui.log_msg('@ entered non-single-char, therefore aborted');
1275 tui.switch_mode('admin');
1277 tui.tile_control_char = tui.inputEl.value[0];
1278 tui.switch_mode('control_tile_draw');
1280 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1281 let tokens = parser.tokenize(tui.inputEl.value);
1282 if (tokens.length > 0 && tokens[0].length > 0) {
1283 if (tui.inputEl.value[0][0] == '/') {
1284 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1285 tui.switch_mode('play');
1286 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1287 tui.switch_mode('study');
1288 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1289 tui.switch_mode('edit');
1290 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1291 tui.switch_mode('admin_enter');
1292 } else if (tokens[0].slice(1) == 'nick') {
1293 if (tokens.length > 1) {
1294 server.send(['NICK', tokens[1]]);
1296 tui.log_msg('? need new name');
1299 tui.log_msg('? unknown command');
1302 server.send(['ALL', tui.inputEl.value]);
1304 } else if (tui.inputEl.valuelength > 0) {
1305 server.send(['ALL', tui.inputEl.value]);
1308 } else if (tui.mode.name == 'play') {
1309 if (tui.mode.mode_switch_on_key(event)) {
1311 } else if (event.key === tui.keys.take_thing
1312 && game.tasks.includes('PICK_UP')) {
1313 server.send(["TASK:PICK_UP"]);
1314 } else if (event.key === tui.keys.drop_thing
1315 && game.tasks.includes('DROP')) {
1316 server.send(["TASK:DROP"]);
1317 } else if (event.key in tui.movement_keys
1318 && game.tasks.includes('MOVE')) {
1319 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1320 } else if (event.key === tui.keys.teleport) {
1323 } else if (tui.mode.name == 'study') {
1324 if (tui.mode.mode_switch_on_key(event)) {
1326 } else if (event.key in tui.movement_keys) {
1327 explorer.move(tui.movement_keys[event.key]);
1328 } else if (event.key == tui.keys.toggle_map_mode) {
1329 tui.toggle_map_mode();
1331 } else if (tui.mode.name == 'control_tile_draw') {
1332 if (tui.mode.mode_switch_on_key(event)) {
1334 } else if (event.key in tui.movement_keys) {
1335 explorer.move(tui.movement_keys[event.key]);
1336 } else if (event.key === tui.keys.toggle_tile_draw) {
1337 tui.toggle_tile_draw();
1339 } else if (tui.mode.name == 'admin') {
1340 if (tui.mode.mode_switch_on_key(event)) {
1343 } else if (tui.mode.name == 'edit') {
1344 if (tui.mode.mode_switch_on_key(event)) {
1346 } else if (event.key in tui.movement_keys
1347 && game.tasks.includes('MOVE')) {
1348 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1349 } else if (event.key === tui.keys.flatten
1350 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1351 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1357 rows_selector.addEventListener('input', function() {
1358 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1361 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1362 terminal.initialize();
1365 cols_selector.addEventListener('input', function() {
1366 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1369 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1370 terminal.initialize();
1371 tui.window_width = terminal.cols / 2,
1374 for (let key_selector of key_selectors) {
1375 key_selector.addEventListener('input', function() {
1376 window.localStorage.setItem(key_selector.id, key_selector.value);
1380 window.setInterval(function() {
1381 if (server.connected) {
1382 server.send(['PING']);
1384 server.reconnect_to(server.url);
1385 tui.log_msg('@ attempting reconnect …')
1388 window.setInterval(function() {
1390 if (document.activeElement == tui.inputEl) {
1391 val = "on (click outside terminal to change)";
1393 val = "off (click into terminal to change)";
1395 document.getElementById("keyboard_control").textContent = val;
1397 document.getElementById("terminal").onclick = function() {
1398 tui.inputEl.focus();
1400 document.getElementById("help").onclick = function() {
1401 tui.show_help = true;
1404 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1405 const mode = switchEl.id.slice("switch_to_".length);
1406 switchEl.onclick = function() {
1407 tui.switch_mode(mode);
1411 document.getElementById("toggle_tile_draw").onclick = function() {
1412 tui.toggle_tile_draw();
1414 document.getElementById("toggle_map_mode").onclick = function() {
1415 tui.toggle_map_mode();
1418 document.getElementById("take_thing").onclick = function() {
1419 server.send(['TASK:PICK_UP']);
1421 document.getElementById("drop_thing").onclick = function() {
1422 server.send(['TASK:DROP']);
1424 document.getElementById("flatten").onclick = function() {
1425 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1427 document.getElementById("teleport").onclick = function() {
1430 document.getElementById("move_upleft").onclick = function() {
1431 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1432 server.send(['TASK:MOVE', 'UPLEFT']);
1434 explorer.move('UPLEFT');
1437 document.getElementById("move_left").onclick = function() {
1438 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1439 server.send(['TASK:MOVE', 'LEFT']);
1441 explorer.move('LEFT');
1444 document.getElementById("move_downleft").onclick = function() {
1445 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1446 server.send(['TASK:MOVE', 'DOWNLEFT']);
1448 explorer.move('DOWNLEFT');
1451 document.getElementById("move_down").onclick = function() {
1452 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1453 server.send(['TASK:MOVE', 'DOWN']);
1455 explorer.move('DOWN');
1458 document.getElementById("move_up").onclick = function() {
1459 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1460 server.send(['TASK:MOVE', 'UP']);
1462 explorer.move('UP');
1465 document.getElementById("move_upright").onclick = function() {
1466 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1467 server.send(['TASK:MOVE', 'UPRIGHT']);
1469 explorer.move('UPRIGHT');
1472 document.getElementById("move_right").onclick = function() {
1473 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1474 server.send(['TASK:MOVE', 'RIGHT']);
1476 explorer.move('RIGHT');
1479 document.getElementById("move_downright").onclick = function() {
1480 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1481 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1483 explorer.move('DOWNRIGHT');