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 target="_blank" 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('MOVE')) {
972 content += "[" + movement_keys_desc + "] – move player\n";
974 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
975 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
978 } else if (this.mode.name == 'control_tile_draw') {
979 content += "Available actions:\n";
980 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
982 } else if (this.mode.name == 'chat') {
983 content += '/nick NAME – re-name yourself to NAME\n';
984 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
985 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
986 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
987 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
989 content += this.mode.list_available_modes();
991 if (!this.mode.has_input_prompt) {
992 start_x = this.window_width
994 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
995 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
996 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
997 terminal.write(y, start_x, lines[i]);
1000 toggle_tile_draw: function() {
1001 if (tui.tile_draw) {
1002 tui.tile_draw = false;
1004 tui.tile_draw = true;
1007 toggle_map_mode: function() {
1008 if (tui.map_mode == 'terrain only') {
1009 tui.map_mode = 'terrain + annotations';
1010 } else if (tui.map_mode == 'terrain + annotations') {
1011 tui.map_mode = 'terrain + things';
1013 tui.map_mode = 'terrain only';
1016 full_refresh: function() {
1018 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1019 if (this.mode.is_intro) {
1020 this.draw_history();
1023 if (game.turn_complete) {
1025 this.draw_turn_line();
1027 this.draw_mode_line();
1028 if (this.mode.shows_info) {
1031 this.draw_history();
1035 if (this.show_help) {
1047 this.map_control = "";
1048 this.map_size = [0,0];
1049 this.player_id = -1;
1053 get_thing: function(id_, create_if_not_found=false) {
1054 if (id_ in game.things) {
1055 return game.things[id_];
1056 } else if (create_if_not_found) {
1057 let t = new Thing([0,0]);
1058 game.things[id_] = t;
1062 move: function(start_position, direction) {
1063 let target = [start_position[0], start_position[1]];
1064 if (direction == 'LEFT') {
1066 } else if (direction == 'RIGHT') {
1068 } else if (game.map_geometry == 'Square') {
1069 if (direction == 'UP') {
1071 } else if (direction == 'DOWN') {
1074 } else if (game.map_geometry == 'Hex') {
1075 let start_indented = start_position[0] % 2;
1076 if (direction == 'UPLEFT') {
1078 if (!start_indented) {
1081 } else if (direction == 'UPRIGHT') {
1083 if (start_indented) {
1086 } else if (direction == 'DOWNLEFT') {
1088 if (!start_indented) {
1091 } else if (direction == 'DOWNRIGHT') {
1093 if (start_indented) {
1098 if (target[0] < 0 || target[1] < 0 ||
1099 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1104 teleport: function() {
1105 let player = this.get_thing(game.player_id);
1106 if (player.position in this.portals) {
1107 server.reconnect_to(this.portals[player.position]);
1109 terminal.blink_screen();
1110 tui.log_msg('? not standing on portal')
1118 server.init(websocket_location);
1124 move: function(direction) {
1125 let target = game.move(this.position, direction);
1127 this.position = target
1128 if (tui.mode.shows_info) {
1130 } else if (tui.tile_draw) {
1131 this.send_tile_control_command();
1134 terminal.blink_screen();
1137 update_info_db: function(yx, str) {
1138 this.info_db[yx] = str;
1139 if (tui.mode.name == 'study') {
1143 empty_info_db: function() {
1145 this.info_hints = [];
1146 if (tui.mode.name == 'study') {
1150 query_info: function() {
1151 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1153 get_info: function() {
1154 let info = "MAP VIEW: " + tui.map_mode + "\n";
1155 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1156 if (game.fov[position_i] != '.') {
1157 return info + 'outside field of view';
1159 let terrain_char = game.map[position_i]
1160 let terrain_desc = '?'
1161 if (game.terrains[terrain_char]) {
1162 terrain_desc = game.terrains[terrain_char];
1164 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1165 let protection = game.map_control[position_i];
1166 if (protection == '.') {
1167 protection = 'unprotected';
1169 info += 'PROTECTION: ' + protection + '\n';
1170 for (let t_id in game.things) {
1171 let t = game.things[t_id];
1172 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1173 let symbol = game.thing_types[t.type_];
1174 info += "THING: " + t.type_ + " / " + symbol;
1175 if (t.player_char) {
1176 info += t.player_char;
1179 info += " (" + t.name_ + ")";
1184 if (this.position in game.portals) {
1185 info += "PORTAL: " + game.portals[this.position] + "\n";
1187 if (this.position in this.info_db) {
1188 info += "ANNOTATIONS: " + this.info_db[this.position];
1190 info += 'waiting …';
1194 annotate: function(msg) {
1195 if (msg.length == 0) {
1196 msg = " "; // triggers annotation deletion
1198 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1200 set_portal: function(msg) {
1201 if (msg.length == 0) {
1202 msg = " "; // triggers portal deletion
1204 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1206 send_tile_control_command: function() {
1207 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1211 tui.inputEl.addEventListener('input', (event) => {
1212 if (tui.mode.has_input_prompt) {
1213 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1214 if (tui.inputEl.value.length > max_length) {
1215 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1217 tui.recalc_input_lines();
1218 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1219 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1220 tui.switch_mode('edit');
1224 document.onclick = function() {
1225 tui.show_help = false;
1227 tui.inputEl.addEventListener('keydown', (event) => {
1228 tui.show_help = false;
1229 if (event.key == 'Enter') {
1230 event.preventDefault();
1232 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1233 tui.show_help = true;
1235 tui.restore_input_values();
1236 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1237 && !tui.mode.is_single_char_entry) {
1238 tui.show_help = true;
1239 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1240 tui.login_name = tui.inputEl.value;
1241 server.send(['LOGIN', tui.inputEl.value]);
1243 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1244 if (tui.inputEl.value.length == 0) {
1245 tui.log_msg('@ aborted');
1247 server.send(['SET_MAP_CONTROL_PASSWORD',
1248 tui.tile_control_char, tui.inputEl.value]);
1249 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1251 tui.switch_mode('admin');
1252 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1253 explorer.set_portal(tui.inputEl.value);
1254 tui.switch_mode('edit');
1255 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1256 explorer.annotate(tui.inputEl.value);
1257 tui.switch_mode('edit');
1258 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1259 if (tui.inputEl.value.length == 0) {
1260 tui.inputEl.value = " ";
1262 tui.password = tui.inputEl.value
1263 tui.switch_mode('edit');
1264 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1265 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1266 tui.switch_mode('play');
1267 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1268 if (tui.inputEl.value.length != 1) {
1269 tui.log_msg('@ entered non-single-char, therefore aborted');
1270 tui.switch_mode('admin');
1272 tui.tile_control_char = tui.inputEl.value[0];
1273 tui.switch_mode('control_pw_pw');
1275 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1276 if (tui.inputEl.value.length != 1) {
1277 tui.log_msg('@ entered non-single-char, therefore aborted');
1278 tui.switch_mode('admin');
1280 tui.tile_control_char = tui.inputEl.value[0];
1281 tui.switch_mode('control_tile_draw');
1283 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1284 let tokens = parser.tokenize(tui.inputEl.value);
1285 if (tokens.length > 0 && tokens[0].length > 0) {
1286 if (tui.inputEl.value[0][0] == '/') {
1287 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1288 tui.switch_mode('play');
1289 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1290 tui.switch_mode('study');
1291 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1292 tui.switch_mode('edit');
1293 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1294 tui.switch_mode('admin_enter');
1295 } else if (tokens[0].slice(1) == 'nick') {
1296 if (tokens.length > 1) {
1297 server.send(['NICK', tokens[1]]);
1299 tui.log_msg('? need new name');
1302 tui.log_msg('? unknown command');
1305 server.send(['ALL', tui.inputEl.value]);
1307 } else if (tui.inputEl.valuelength > 0) {
1308 server.send(['ALL', tui.inputEl.value]);
1311 } else if (tui.mode.name == 'play') {
1312 if (tui.mode.mode_switch_on_key(event)) {
1314 } else if (event.key === tui.keys.take_thing
1315 && game.tasks.includes('PICK_UP')) {
1316 server.send(["TASK:PICK_UP"]);
1317 } else if (event.key === tui.keys.drop_thing
1318 && game.tasks.includes('DROP')) {
1319 server.send(["TASK:DROP"]);
1320 } else if (event.key in tui.movement_keys
1321 && game.tasks.includes('MOVE')) {
1322 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1323 } else if (event.key === tui.keys.teleport) {
1326 } else if (tui.mode.name == 'study') {
1327 if (tui.mode.mode_switch_on_key(event)) {
1329 } else if (event.key in tui.movement_keys) {
1330 explorer.move(tui.movement_keys[event.key]);
1331 } else if (event.key == tui.keys.toggle_map_mode) {
1332 tui.toggle_map_mode();
1334 } else if (tui.mode.name == 'control_tile_draw') {
1335 if (tui.mode.mode_switch_on_key(event)) {
1337 } else if (event.key in tui.movement_keys) {
1338 explorer.move(tui.movement_keys[event.key]);
1339 } else if (event.key === tui.keys.toggle_tile_draw) {
1340 tui.toggle_tile_draw();
1342 } else if (tui.mode.name == 'admin') {
1343 if (tui.mode.mode_switch_on_key(event)) {
1346 } else if (tui.mode.name == 'edit') {
1347 if (tui.mode.mode_switch_on_key(event)) {
1349 } else if (event.key in tui.movement_keys
1350 && game.tasks.includes('MOVE')) {
1351 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1352 } else if (event.key === tui.keys.flatten
1353 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1354 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1360 rows_selector.addEventListener('input', function() {
1361 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1364 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1365 terminal.initialize();
1368 cols_selector.addEventListener('input', function() {
1369 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1372 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1373 terminal.initialize();
1374 tui.window_width = terminal.cols / 2,
1377 for (let key_selector of key_selectors) {
1378 key_selector.addEventListener('input', function() {
1379 window.localStorage.setItem(key_selector.id, key_selector.value);
1383 window.setInterval(function() {
1384 if (server.connected) {
1385 server.send(['PING']);
1387 server.reconnect_to(server.url);
1388 tui.log_msg('@ attempting reconnect …')
1391 window.setInterval(function() {
1393 if (document.activeElement == tui.inputEl) {
1394 val = "on (click outside terminal to change)";
1396 val = "off (click into terminal to change)";
1398 document.getElementById("keyboard_control").textContent = val;
1400 document.getElementById("terminal").onclick = function() {
1401 tui.inputEl.focus();
1403 document.getElementById("help").onclick = function() {
1404 tui.show_help = true;
1407 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1408 const mode = switchEl.id.slice("switch_to_".length);
1409 switchEl.onclick = function() {
1410 tui.switch_mode(mode);
1414 document.getElementById("toggle_tile_draw").onclick = function() {
1415 tui.toggle_tile_draw();
1417 document.getElementById("toggle_map_mode").onclick = function() {
1418 tui.toggle_map_mode();
1421 document.getElementById("take_thing").onclick = function() {
1422 server.send(['TASK:PICK_UP']);
1424 document.getElementById("drop_thing").onclick = function() {
1425 server.send(['TASK:DROP']);
1427 document.getElementById("flatten").onclick = function() {
1428 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1430 document.getElementById("teleport").onclick = function() {
1433 document.getElementById("move_upleft").onclick = function() {
1434 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1435 server.send(['TASK:MOVE', 'UPLEFT']);
1437 explorer.move('UPLEFT');
1440 document.getElementById("move_left").onclick = function() {
1441 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1442 server.send(['TASK:MOVE', 'LEFT']);
1444 explorer.move('LEFT');
1447 document.getElementById("move_downleft").onclick = function() {
1448 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1449 server.send(['TASK:MOVE', 'DOWNLEFT']);
1451 explorer.move('DOWNLEFT');
1454 document.getElementById("move_down").onclick = function() {
1455 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1456 server.send(['TASK:MOVE', 'DOWN']);
1458 explorer.move('DOWN');
1461 document.getElementById("move_up").onclick = function() {
1462 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1463 server.send(['TASK:MOVE', 'UP']);
1465 explorer.move('UP');
1468 document.getElementById("move_upright").onclick = function() {
1469 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1470 server.send(['TASK:MOVE', 'UPRIGHT']);
1472 explorer.move('UPRIGHT');
1475 document.getElementById("move_right").onclick = function() {
1476 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1477 server.send(['TASK:MOVE', 'RIGHT']);
1479 explorer.move('RIGHT');
1482 document.getElementById("move_downright").onclick = function() {
1483 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1484 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1486 explorer.move('DOWNRIGHT');