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="hex_move_upleft">up-left</button></td>
26 <td style="text-align: center"><button id="square_move_up">up</button></td>
27 <td><button id="hex_move_upright">up-right</button></td>
30 <td style="text-align: right;"><button id="square_move_left">left</button><button id="hex_move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="square_move_right">right</button><button id="hex_move_right">right</button></td>
35 <td><button id="hex_move_downleft">down-left</button></td>
36 <td style="text-align: center"><button id="square_move_down">down</button></td>
37 <td><button id="hex_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">world 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_name_thing">name thing</button>
67 <button id="switch_to_password">enter world edit password</button>
71 <td><button id="switch_to_admin_enter">admin mode</button></td>
73 <button id="switch_to_control_pw_type">change protection character password</button>
74 <button id="switch_to_control_tile_type">change protection areas</button>
75 <button id="switch_to_admin_thing_protect">change thing protection</button>
76 <button id="toggle_tile_draw">toggle protection character drawing</button>
81 <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 />
83 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
84 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
85 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
86 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
87 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
88 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
89 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
90 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
91 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
92 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
93 <li>help: <input id="key_help" type="text" value="h" />
94 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
95 <li>teleport: <input id="key_teleport" type="text" value="p" />
96 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
97 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
98 <li><input id="key_switch_to_chat" type="text" value="t" />
99 <li><input id="key_switch_to_play" type="text" value="p" />
100 <li><input id="key_switch_to_study" type="text" value="?" />
101 <li><input id="key_switch_to_edit" type="text" value="E" />
102 <li><input id="key_switch_to_write" type="text" value="m" />
103 <li><input id="key_switch_to_name_thing" type="text" value="N" />
104 <li><input id="key_switch_to_password" type="text" value="P" />
105 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
106 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
107 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
108 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
109 <li><input id="key_switch_to_annotate" type="text" value="M" />
110 <li><input id="key_switch_to_portal" type="text" value="T" />
111 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
112 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
117 let websocket_location = "wss://plomlompom.com/rogue_chat/";
118 //let websocket_location = "ws://localhost:8000/";
123 'long': 'This mode allows you to interact with the map in various ways.'
127 '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.'},
129 'short': 'world edit',
130 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
133 'short': 'name thing',
134 'long': 'Give name to/change name of thing here.'
136 'admin_thing_protect': {
137 'short': 'change thing protection',
138 'long': 'Change protection character for thing here.'
141 'short': 'change terrain',
142 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
145 'short': 'change protection character password',
146 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
149 'short': 'change protection character password',
150 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
152 'control_tile_type': {
153 'short': 'change tiles protection',
154 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
156 'control_tile_draw': {
157 'short': 'change tiles protection',
158 '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 protection character.'
161 'short': 'annotate tile',
162 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
165 'short': 'edit portal',
166 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world 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.'
170 '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:'
174 'long': 'Enter your player name.'
176 'waiting_for_server': {
177 'short': 'waiting for server response',
178 'long': 'Waiting for a server response.'
181 'short': 'waiting for server response',
182 'long': 'Waiting for a server response.'
185 'short': 'set world edit password',
186 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
189 'short': 'become admin',
190 'long': 'This mode allows you to become admin if you know an admin password.'
194 'long': 'This mode allows you access to actions limited to administrators.'
197 let key_descriptions = {
199 'flatten': 'flatten surroundings',
200 'teleport': 'teleport',
201 'take_thing': 'pick up thing',
202 'drop_thing': 'drop thing',
203 'toggle_map_mode': 'toggle map view',
204 'toggle_tile_draw': 'toggle protection character drawing',
205 'hex_move_upleft': 'up-left',
206 'hex_move_upright': 'up-right',
207 'hex_move_right': 'right',
208 'hex_move_left': 'left',
209 'hex_move_downleft': 'down-left',
210 'hex_move_downright': 'down-right',
211 'square_move_up': 'up',
212 'square_move_left': 'left',
213 'square_move_down': 'down',
214 'square_move_right': 'right',
216 for (const mode_name of Object.keys(mode_helps)) {
217 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
220 let rows_selector = document.getElementById("n_rows");
221 let cols_selector = document.getElementById("n_cols");
222 let key_selectors = document.querySelectorAll('[id^="key_"]');
224 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
225 const action = key_switch_selector.id.slice("key_switch_to_".length);
226 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
229 function restore_selector_value(selector) {
230 let stored_selection = window.localStorage.getItem(selector.id);
231 if (stored_selection) {
232 selector.value = stored_selection;
235 restore_selector_value(rows_selector);
236 restore_selector_value(cols_selector);
237 for (let key_selector of key_selectors) {
238 restore_selector_value(key_selector);
241 function escapeHTML(str) {
243 replace(/&/g, '&').
244 replace(/</g, '<').
245 replace(/>/g, '>').
246 replace(/'/g, ''').
247 replace(/"/g, '"');
253 initialize: function() {
254 this.rows = rows_selector.value;
255 this.cols = cols_selector.value;
256 this.pre_el = document.getElementById("terminal");
257 this.pre_el.style.color = this.foreground;
258 this.pre_el.style.backgroundColor = this.background;
261 for (let y = 0, x = 0; y <= this.rows; x++) {
262 if (x == this.cols) {
265 this.content.push(line);
267 if (y == this.rows) {
274 blink_screen: function() {
275 this.pre_el.style.color = this.background;
276 this.pre_el.style.backgroundColor = this.foreground;
278 this.pre_el.style.color = this.foreground;
279 this.pre_el.style.backgroundColor = this.background;
282 refresh: function() {
283 let pre_content = '';
284 for (let y = 0; y < this.rows; y++) {
285 let line = this.content[y].join('');
287 if (y in tui.links) {
289 for (let span of tui.links[y]) {
290 chunks.push(escapeHTML(line.slice(start_x, span[0])));
291 chunks.push('<a target="_blank" href="');
292 chunks.push(escapeHTML(span[2]));
294 chunks.push(escapeHTML(line.slice(span[0], span[1])));
298 chunks.push(escapeHTML(line.slice(start_x)));
300 chunks = [escapeHTML(line)];
302 for (const chunk of chunks) {
303 pre_content += chunk;
307 this.pre_el.innerHTML = pre_content;
309 write: function(start_y, start_x, msg) {
310 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
311 this.content[start_y][x] = msg[i];
314 drawBox: function(start_y, start_x, height, width) {
315 let end_y = start_y + height;
316 let end_x = start_x + width;
317 for (let y = start_y, x = start_x; y < this.rows; x++) {
325 this.content[y][x] = ' ';
329 terminal.initialize();
332 tokenize: function(str) {
337 for (let i = 0; i < str.length; i++) {
343 } else if (c == '\\') {
345 } else if (c == '"') {
350 } else if (c == '"') {
352 } else if (c === ' ') {
353 if (token.length > 0) {
361 if (token.length > 0) {
366 parse_yx: function(position_string) {
367 let coordinate_strings = position_string.split(',')
368 let position = [0, 0];
369 position[0] = parseInt(coordinate_strings[0].slice(2));
370 position[1] = parseInt(coordinate_strings[1].slice(2));
382 init: function(url) {
384 this.websocket = new WebSocket(this.url);
385 this.websocket.onopen = function(event) {
386 server.connected = true;
387 game.thing_types = {};
389 server.send(['TASKS']);
390 server.send(['TERRAINS']);
391 server.send(['THING_TYPES']);
392 tui.log_msg("@ server connected! :)");
393 tui.switch_mode('login');
395 this.websocket.onclose = function(event) {
396 server.connected = false;
397 tui.switch_mode('waiting_for_server');
398 tui.log_msg("@ server disconnected :(");
400 this.websocket.onmessage = this.handle_event;
402 reconnect_to: function(url) {
403 this.websocket.close();
406 send: function(tokens) {
407 this.websocket.send(unparser.untokenize(tokens));
409 handle_event: function(event) {
410 let tokens = parser.tokenize(event.data);
411 if (tokens[0] === 'TURN') {
412 game.turn_complete = false;
413 explorer.empty_info_db();
416 game.turn = parseInt(tokens[1]);
417 } else if (tokens[0] === 'THING') {
418 let t = game.get_thing(tokens[4], true);
419 t.position = parser.parse_yx(tokens[1]);
421 t.protection = tokens[3];
422 } else if (tokens[0] === 'THING_NAME') {
423 let t = game.get_thing(tokens[1], false);
427 } else if (tokens[0] === 'THING_CHAR') {
428 let t = game.get_thing(tokens[1], false);
430 t.player_char = tokens[2];
432 } else if (tokens[0] === 'TASKS') {
433 game.tasks = tokens[1].split(',');
434 tui.mode_write.legal = game.tasks.includes('WRITE');
435 } else if (tokens[0] === 'THING_TYPE') {
436 game.thing_types[tokens[1]] = tokens[2]
437 } else if (tokens[0] === 'TERRAIN') {
438 game.terrains[tokens[1]] = tokens[2]
439 } else if (tokens[0] === 'MAP') {
440 game.map_geometry = tokens[1];
442 game.map_size = parser.parse_yx(tokens[2]);
444 } else if (tokens[0] === 'FOV') {
446 } else if (tokens[0] === 'MAP_CONTROL') {
447 game.map_control = tokens[1]
448 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
449 game.turn_complete = true;
450 if (tui.mode.name == 'post_login_wait') {
451 tui.switch_mode('play');
452 } else if (tui.mode.name == 'study') {
453 explorer.query_info();
456 } else if (tokens[0] === 'CHAT') {
457 tui.log_msg('# ' + tokens[1], 1);
458 } else if (tokens[0] === 'PLAYER_ID') {
459 game.player_id = parseInt(tokens[1]);
460 } else if (tokens[0] === 'LOGIN_OK') {
461 this.send(['GET_GAMESTATE']);
462 tui.switch_mode('post_login_wait');
463 } else if (tokens[0] === 'ADMIN_OK') {
465 tui.log_msg('@ you now have admin rights');
466 tui.switch_mode('admin');
467 } else if (tokens[0] === 'PORTAL') {
468 let position = parser.parse_yx(tokens[1]);
469 game.portals[position] = tokens[2];
470 } else if (tokens[0] === 'ANNOTATION_HINT') {
471 let position = parser.parse_yx(tokens[1]);
472 explorer.info_hints = explorer.info_hints.concat([position]);
473 } else if (tokens[0] === 'ANNOTATION') {
474 let position = parser.parse_yx(tokens[1]);
475 explorer.update_info_db(position, tokens[2]);
477 } else if (tokens[0] === 'UNHANDLED_INPUT') {
478 tui.log_msg('? unknown command');
479 } else if (tokens[0] === 'PLAY_ERROR') {
480 tui.log_msg('? ' + tokens[1]);
481 terminal.blink_screen();
482 } else if (tokens[0] === 'ARGUMENT_ERROR') {
483 tui.log_msg('? syntax error: ' + tokens[1]);
484 } else if (tokens[0] === 'GAME_ERROR') {
485 tui.log_msg('? game error: ' + tokens[1]);
486 } else if (tokens[0] === 'PONG') {
489 tui.log_msg('? unhandled input: ' + event.data);
495 quote: function(str) {
497 for (let i = 0; i < str.length; i++) {
499 if (['"', '\\'].includes(c)) {
505 return quoted.join('');
507 to_yx: function(yx_coordinate) {
508 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
510 untokenize: function(tokens) {
511 let quoted_tokens = [];
512 for (let token of tokens) {
513 quoted_tokens.push(this.quote(token));
515 return quoted_tokens.join(" ");
520 constructor(name, has_input_prompt=false, shows_info=false,
521 is_intro=false, is_single_char_entry=false) {
523 this.short_desc = mode_helps[name].short;
524 this.available_modes = [];
525 this.available_actions = [];
526 this.has_input_prompt = has_input_prompt;
527 this.shows_info= shows_info;
528 this.is_intro = is_intro;
529 this.help_intro = mode_helps[name].long;
530 this.is_single_char_entry = is_single_char_entry;
533 *iter_available_modes() {
534 for (let mode_name of this.available_modes) {
535 let mode = tui['mode_' + mode_name];
539 let key = tui.keys['switch_to_' + mode.name];
543 list_available_modes() {
545 if (this.available_modes.length > 0) {
546 msg += 'Other modes available from here:\n';
547 for (let [mode, key] of this.iter_available_modes()) {
548 msg += '[' + key + '] – ' + mode.short_desc + '\n';
553 mode_switch_on_key(key_event) {
554 for (let [mode, key] of this.iter_available_modes()) {
555 if (key_event.key == key) {
556 event.preventDefault();
557 tui.switch_mode(mode.name);
569 window_width: terminal.cols / 2,
577 mode_waiting_for_server: new Mode('waiting_for_server',
579 mode_login: new Mode('login', true, false, true),
580 mode_post_login_wait: new Mode('post_login_wait'),
581 mode_chat: new Mode('chat', true),
582 mode_annotate: new Mode('annotate', true, true),
583 mode_play: new Mode('play'),
584 mode_study: new Mode('study', false, true),
585 mode_write: new Mode('write', false, false, false, true),
586 mode_edit: new Mode('edit'),
587 mode_control_pw_type: new Mode('control_pw_type', true),
588 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
589 mode_portal: new Mode('portal', true, true),
590 mode_password: new Mode('password', true),
591 mode_name_thing: new Mode('name_thing', true, true),
592 mode_admin_enter: new Mode('admin_enter', true),
593 mode_admin: new Mode('admin'),
594 mode_control_pw_pw: new Mode('control_pw_pw', true),
595 mode_control_tile_type: new Mode('control_tile_type', true),
596 mode_control_tile_draw: new Mode('control_tile_draw'),
598 'flatten': 'FLATTEN_SURROUNDINGS',
599 'take_thing': 'PICK_UP',
600 'drop_thing': 'DROP',
604 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
605 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
606 this.mode_play.available_actions = ["move", "take_thing", "drop_thing", "teleport"];
607 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
608 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
609 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
610 "control_tile_type", "chat",
611 "study", "play", "edit"]
612 this.mode_admin.available_actions = ["move"];
613 this.mode_control_tile_draw.available_modes = ["admin_enter"]
614 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
615 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
616 "password", "chat", "study", "play",
618 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
619 this.mode = this.mode_waiting_for_server;
620 this.inputEl = document.getElementById("input");
621 this.inputEl.focus();
622 this.recalc_input_lines();
623 this.height_header = this.height_turn_line + this.height_mode_line;
624 this.log_msg("@ waiting for server connection ...");
627 init_keys: function() {
629 for (let key_selector of key_selectors) {
630 this.keys[key_selector.id.slice(4)] = key_selector.value;
632 this.movement_keys = {};
633 if (!game.map_geometry) {
636 let geometry_prefix = game.map_geometry.toLowerCase() + '_';
637 for (const key_name of Object.keys(key_descriptions)) {
638 if (key_name.startsWith(geometry_prefix)) {
639 let direction = key_name.split('_')[2].toUpperCase();
640 let key = this.keys[key_name];
641 this.movement_keys[key] = direction;
644 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
645 move_button.hidden = true;
647 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
648 move_button.hidden = false;
650 for (let el of document.getElementsByTagName("button")) {
651 let action_desc = key_descriptions[el.id];
652 let action_key = '[' + this.keys[el.id] + ']';
653 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
656 task_action_on: function(action) {
657 return game.tasks.includes(this.action_tasks[action]);
659 switch_mode: function(mode_name) {
660 if (this.mode.name == 'control_tile_draw') {
661 tui.log_msg('@ finished tile protection drawing.')
663 this.tile_draw = false;
664 if (mode_name == 'admin_enter' && this.is_admin) {
666 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
667 let player_position = game.things[game.player_id].position;
669 for (let t_id in game.things) {
670 if (t_id == game.player_id) {
673 let t = game.things[t_id];
674 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
680 terminal.blink_screen();
681 this.log_msg('? not standing over thing');
684 this.selected_thing_id = thing_id;
687 this.mode = this['mode_' + mode_name];
688 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
689 this.map_mode = 'protections';
690 } else if (this.mode.name != "edit") {
691 this.map_mode = 'terrain + things';
693 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
694 this.inputEl.focus();
696 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
697 explorer.position = game.things[game.player_id].position;
698 if (this.mode.shows_info) {
699 explorer.query_info();
702 this.inputEl.value = "";
703 this.restore_input_values();
704 for (let el of document.getElementsByTagName("button")) {
707 document.getElementById("help").disabled = false;
708 for (const action of this.mode.available_actions) {
709 if (["move", "move_explorer"].includes(action)) {
710 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
711 move_key.disabled = false;
713 } else if (Object.keys(this.action_tasks).includes(action)) {
714 if (this.task_action_on(action)) {
715 document.getElementById(action).disabled = false;
718 document.getElementById(action).disabled = false;
721 for (const mode_name of this.mode.available_modes) {
722 document.getElementById('switch_to_' + mode_name).disabled = false;
724 if (this.mode.name == 'login') {
725 if (this.login_name) {
726 server.send(['LOGIN', this.login_name]);
728 this.log_msg("? need login name");
730 } else if (this.mode.is_single_char_entry) {
731 this.show_help = true;
732 } else if (this.mode.name == 'admin_enter') {
733 this.log_msg('@ enter admin password:')
734 } else if (this.mode.name == 'control_pw_type') {
735 this.log_msg('@ enter protection character for which you want to change the password:')
736 } else if (this.mode.name == 'control_tile_type') {
737 this.log_msg('@ enter protection character which you want to draw:')
738 } else if (this.mode.name == 'admin_thing_protect') {
739 this.log_msg('@ enter thing protection character:')
740 } else if (this.mode.name == 'control_pw_pw') {
741 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
742 } else if (this.mode.name == 'control_tile_draw') {
743 this.log_msg('@ can draw protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
747 offset_links: function(offset, links) {
748 for (let y in links) {
749 let real_y = offset[0] + parseInt(y);
750 if (!this.links[real_y]) {
751 this.links[real_y] = [];
753 for (let link of links[y]) {
754 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
755 this.links[real_y].push(offset_link);
759 restore_input_values: function() {
760 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
761 let info = explorer.info_db[explorer.position];
762 if (info != "(none)") {
763 this.inputEl.value = info;
765 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
766 let portal = game.portals[explorer.position]
767 this.inputEl.value = portal;
768 } else if (this.mode.name == 'password') {
769 this.inputEl.value = this.password;
770 } else if (this.mode.name == 'name_thing') {
771 let t = game.get_thing(this.selected_thing_id);
773 this.inputEl.value = t.name_;
775 } else if (this.mode.name == 'admin_thing_protect') {
776 let t = game.get_thing(this.selected_thing_id);
777 if (t && t.protection) {
778 this.inputEl.value = t.protection;
782 recalc_input_lines: function() {
783 if (this.mode.has_input_prompt) {
785 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
787 this.input_lines = [];
789 this.height_input = this.input_lines.length;
791 msg_into_lines_of_width: function(msg, width) {
792 function push_inner_link(y, end_x) {
793 if (!inner_links[y]) {
796 inner_links[y].push([url_start_x, end_x, url]);
798 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
801 for (const match of matches) {
802 const url = match[0];
803 const url_start = match.index;
804 const url_end = match.index + match[0].length;
805 link_data[url_start] = url;
806 url_ends.push(url_end);
810 let inner_links = {};
814 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
815 if (x >= width || msg[i] == "\n") {
817 push_inner_link(y, chunk.length);
819 if (url_ends[0] == i) {
827 if (msg[i] == "\n") {
832 if (msg[i] != "\n") {
835 if (i in link_data) {
839 } else if (url_ends[0] == i) {
841 push_inner_link(y, x);
847 push_inner_link(lines.length - 1, chunk.length);
849 return [lines, inner_links];
851 log_msg: function(msg) {
853 while (this.log.length > 100) {
858 draw_map: function() {
859 let map_lines_split = [];
861 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
862 if (j == game.map_size[1]) {
863 map_lines_split.push(line);
867 if (this.map_mode == 'protections') {
868 line.push(game.map_control[i] + ' ');
870 line.push(game.map[i] + ' ');
873 map_lines_split.push(line);
874 if (this.map_mode == 'terrain + annotations') {
875 for (const coordinate of explorer.info_hints) {
876 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
878 } else if (this.map_mode == 'terrain + things') {
879 for (const p in game.portals) {
880 let coordinate = p.split(',')
881 let original = map_lines_split[coordinate[0]][coordinate[1]];
882 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
884 let used_positions = [];
885 for (const thing_id in game.things) {
886 let t = game.things[thing_id];
887 let symbol = game.thing_types[t.type_];
890 meta_char = t.player_char;
892 if (used_positions.includes(t.position.toString())) {
895 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
896 used_positions.push(t.position.toString());
899 let player = game.things[game.player_id];
900 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
901 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
902 } else if (tui.map_mode != 'terrain + things') {
903 map_lines_split[player.position[0]][player.position[1]] = '??';
906 if (game.map_geometry == 'Square') {
907 for (let line_split of map_lines_split) {
908 map_lines.push(line_split.join(''));
910 } else if (game.map_geometry == 'Hex') {
912 for (let line_split of map_lines_split) {
913 map_lines.push(' '.repeat(indent) + line_split.join(''));
921 let window_center = [terminal.rows / 2, this.window_width / 2];
922 let center_position = [player.position[0], player.position[1]];
923 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
924 center_position = [explorer.position[0], explorer.position[1]];
926 center_position[1] = center_position[1] * 2;
927 let offset = [center_position[0] - window_center[0],
928 center_position[1] - window_center[1]]
929 if (game.map_geometry == 'Hex' && offset[0] % 2) {
932 let term_y = Math.max(0, -offset[0]);
933 let term_x = Math.max(0, -offset[1]);
934 let map_y = Math.max(0, offset[0]);
935 let map_x = Math.max(0, offset[1]);
936 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
937 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
938 terminal.write(term_y, term_x, to_draw);
941 draw_mode_line: function() {
942 let help = 'hit [' + this.keys.help + '] for help';
943 if (this.mode.has_input_prompt) {
944 help = 'enter /help for help';
946 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
948 draw_turn_line: function(n) {
949 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
951 draw_history: function() {
952 let log_display_lines = [];
954 let y_offset_in_log = 0;
955 for (let line of this.log) {
956 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
958 log_display_lines = log_display_lines.concat(new_lines);
959 for (const y in link_data) {
960 const rel_y = y_offset_in_log + parseInt(y);
961 log_links[rel_y] = [];
962 for (let link of link_data[y]) {
963 log_links[rel_y].push(link);
966 y_offset_in_log += new_lines.length;
968 let i = log_display_lines.length - 1;
969 for (let y = terminal.rows - 1 - this.height_input;
970 y >= this.height_header && i >= 0;
972 terminal.write(y, this.window_width, log_display_lines[i]);
974 for (const key of Object.keys(log_links)) {
975 if (parseInt(key) <= i) {
976 delete log_links[key];
979 let offset = [terminal.rows - this.height_input - log_display_lines.length,
981 this.offset_links(offset, log_links);
983 draw_info: function() {
984 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
986 let offset = [this.height_header, this.window_width];
987 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
988 terminal.write(y, offset[1], lines[i]);
990 this.offset_links(offset, link_data);
992 draw_input: function() {
993 if (this.mode.has_input_prompt) {
994 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
995 terminal.write(y, this.window_width, this.input_lines[i]);
999 draw_help: function() {
1000 let movement_keys_desc = '';
1001 if (!this.mode.is_intro) {
1002 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1004 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1005 if (this.mode.name == 'chat') {
1006 content += '/nick NAME – re-name yourself to NAME\n';
1007 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1008 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1009 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1010 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1011 } else if (this.mode.available_actions.length > 0) {
1012 content += "Available actions:\n";
1013 for (let action of this.mode.available_actions) {
1014 if (Object.keys(this.action_tasks).includes(action)) {
1015 if (!this.task_action_on(action)) {
1019 if (action == 'move_explorer') {
1022 if (action == 'move') {
1023 content += "[" + movement_keys_desc + "] – move\n"
1025 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1030 content += this.mode.list_available_modes();
1032 if (!this.mode.has_input_prompt) {
1033 start_x = this.window_width
1035 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1036 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1037 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1038 terminal.write(y, start_x, lines[i]);
1041 toggle_tile_draw: function() {
1042 if (tui.tile_draw) {
1043 tui.tile_draw = false;
1045 tui.tile_draw = true;
1048 toggle_map_mode: function() {
1049 if (tui.map_mode == 'terrain only') {
1050 tui.map_mode = 'terrain + annotations';
1051 } else if (tui.map_mode == 'terrain + annotations') {
1052 tui.map_mode = 'terrain + things';
1053 } else if (tui.map_mode == 'terrain + things') {
1054 tui.map_mode = 'protections';
1055 } else if (tui.map_mode == 'protections') {
1056 tui.map_mode = 'terrain only';
1059 full_refresh: function() {
1061 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1062 this.recalc_input_lines();
1063 if (this.mode.is_intro) {
1064 this.draw_history();
1067 if (game.turn_complete) {
1069 this.draw_turn_line();
1071 this.draw_mode_line();
1072 if (this.mode.shows_info) {
1075 this.draw_history();
1079 if (this.show_help) {
1091 this.map_control = "";
1092 this.map_size = [0,0];
1093 this.player_id = -1;
1097 get_thing: function(id_, create_if_not_found=false) {
1098 if (id_ in game.things) {
1099 return game.things[id_];
1100 } else if (create_if_not_found) {
1101 let t = new Thing([0,0]);
1102 game.things[id_] = t;
1106 move: function(start_position, direction) {
1107 let target = [start_position[0], start_position[1]];
1108 if (direction == 'LEFT') {
1110 } else if (direction == 'RIGHT') {
1112 } else if (game.map_geometry == 'Square') {
1113 if (direction == 'UP') {
1115 } else if (direction == 'DOWN') {
1118 } else if (game.map_geometry == 'Hex') {
1119 let start_indented = start_position[0] % 2;
1120 if (direction == 'UPLEFT') {
1122 if (!start_indented) {
1125 } else if (direction == 'UPRIGHT') {
1127 if (start_indented) {
1130 } else if (direction == 'DOWNLEFT') {
1132 if (!start_indented) {
1135 } else if (direction == 'DOWNRIGHT') {
1137 if (start_indented) {
1142 if (target[0] < 0 || target[1] < 0 ||
1143 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1148 teleport: function() {
1149 let player = this.get_thing(game.player_id);
1150 if (player.position in this.portals) {
1151 server.reconnect_to(this.portals[player.position]);
1153 terminal.blink_screen();
1154 tui.log_msg('? not standing on portal')
1162 server.init(websocket_location);
1168 move: function(direction) {
1169 let target = game.move(this.position, direction);
1171 this.position = target
1172 if (tui.mode.shows_info) {
1174 } else if (tui.tile_draw) {
1175 this.send_tile_control_command();
1178 terminal.blink_screen();
1181 update_info_db: function(yx, str) {
1182 this.info_db[yx] = str;
1183 if (tui.mode.name == 'study') {
1187 empty_info_db: function() {
1189 this.info_hints = [];
1190 if (tui.mode.name == 'study') {
1194 query_info: function() {
1195 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1197 get_info: function() {
1198 let info = "MAP VIEW: " + tui.map_mode + "\n";
1199 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1200 if (game.fov[position_i] != '.') {
1201 return info + 'outside field of view';
1203 let terrain_char = game.map[position_i]
1204 let terrain_desc = '?'
1205 if (game.terrains[terrain_char]) {
1206 terrain_desc = game.terrains[terrain_char];
1208 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1209 let protection = game.map_control[position_i];
1210 if (protection == '.') {
1211 protection = 'unprotected';
1213 info += 'PROTECTION: ' + protection + '\n';
1214 for (let t_id in game.things) {
1215 let t = game.things[t_id];
1216 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1217 let symbol = game.thing_types[t.type_];
1218 let protection = t.protection;
1219 if (protection == '.') {
1220 protection = 'unprotected';
1222 info += "THING: " + t.type_ + " / protection: " + protection + " / " + symbol;
1223 if (t.player_char) {
1224 info += t.player_char;
1227 info += " (" + t.name_ + ")";
1232 if (this.position in game.portals) {
1233 info += "PORTAL: " + game.portals[this.position] + "\n";
1235 if (this.position in this.info_db) {
1236 info += "ANNOTATIONS: " + this.info_db[this.position];
1238 info += 'waiting …';
1242 annotate: function(msg) {
1243 if (msg.length == 0) {
1244 msg = " "; // triggers annotation deletion
1246 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1248 set_portal: function(msg) {
1249 if (msg.length == 0) {
1250 msg = " "; // triggers portal deletion
1252 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1254 send_tile_control_command: function() {
1255 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1259 tui.inputEl.addEventListener('input', (event) => {
1260 if (tui.mode.has_input_prompt) {
1261 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1262 if (tui.inputEl.value.length > max_length) {
1263 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1265 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1266 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1267 tui.switch_mode('edit');
1271 document.onclick = function() {
1272 tui.show_help = false;
1274 tui.inputEl.addEventListener('keydown', (event) => {
1275 tui.show_help = false;
1276 if (event.key == 'Enter') {
1277 event.preventDefault();
1279 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1280 tui.show_help = true;
1281 tui.inputEl.value = "";
1282 tui.restore_input_values();
1283 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1284 && !tui.mode.is_single_char_entry) {
1285 tui.show_help = true;
1286 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1287 tui.login_name = tui.inputEl.value;
1288 server.send(['LOGIN', tui.inputEl.value]);
1289 tui.inputEl.value = "";
1290 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1291 if (tui.inputEl.value.length == 0) {
1292 tui.log_msg('@ aborted');
1294 server.send(['SET_MAP_CONTROL_PASSWORD',
1295 tui.tile_control_char, tui.inputEl.value]);
1296 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1298 tui.switch_mode('admin');
1299 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1300 explorer.set_portal(tui.inputEl.value);
1301 tui.switch_mode('edit');
1302 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1303 if (tui.inputEl.value.length == 0) {
1304 tui.inputEl.value = " ";
1306 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1308 tui.switch_mode('edit');
1309 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1310 explorer.annotate(tui.inputEl.value);
1311 tui.switch_mode('edit');
1312 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1313 if (tui.inputEl.value.length == 0) {
1314 tui.inputEl.value = " ";
1316 tui.password = tui.inputEl.value
1317 tui.switch_mode('edit');
1318 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1319 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1320 tui.switch_mode('play');
1321 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1322 if (tui.inputEl.value.length != 1) {
1323 tui.log_msg('@ entered non-single-char, therefore aborted');
1324 tui.switch_mode('admin');
1326 tui.tile_control_char = tui.inputEl.value[0];
1327 tui.switch_mode('control_pw_pw');
1329 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1330 if (tui.inputEl.value.length != 1) {
1331 tui.log_msg('@ entered non-single-char, therefore aborted');
1332 tui.switch_mode('admin');
1334 tui.tile_control_char = tui.inputEl.value[0];
1335 tui.switch_mode('control_tile_draw');
1337 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1338 if (tui.inputEl.value.length != 1) {
1339 tui.log_msg('@ entered non-single-char, therefore aborted');
1341 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1342 tui.log_msg('@ sent new protection character for thing');
1344 tui.switch_mode('admin');
1345 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1346 let tokens = parser.tokenize(tui.inputEl.value);
1347 if (tokens.length > 0 && tokens[0].length > 0) {
1348 if (tui.inputEl.value[0][0] == '/') {
1349 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1350 tui.switch_mode('play');
1351 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1352 tui.switch_mode('study');
1353 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1354 tui.switch_mode('edit');
1355 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1356 tui.switch_mode('admin_enter');
1357 } else if (tokens[0].slice(1) == 'nick') {
1358 if (tokens.length > 1) {
1359 server.send(['NICK', tokens[1]]);
1361 tui.log_msg('? need new name');
1364 tui.log_msg('? unknown command');
1367 server.send(['ALL', tui.inputEl.value]);
1369 } else if (tui.inputEl.valuelength > 0) {
1370 server.send(['ALL', tui.inputEl.value]);
1372 tui.inputEl.value = "";
1373 } else if (tui.mode.name == 'play') {
1374 if (tui.mode.mode_switch_on_key(event)) {
1376 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1377 server.send(["TASK:PICK_UP"]);
1378 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1379 server.send(["TASK:DROP"]);
1380 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1381 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1382 } else if (event.key === tui.keys.teleport) {
1385 } else if (tui.mode.name == 'study') {
1386 if (tui.mode.mode_switch_on_key(event)) {
1388 } else if (event.key in tui.movement_keys) {
1389 explorer.move(tui.movement_keys[event.key]);
1390 } else if (event.key == tui.keys.toggle_map_mode) {
1391 tui.toggle_map_mode();
1393 } else if (tui.mode.name == 'control_tile_draw') {
1394 if (tui.mode.mode_switch_on_key(event)) {
1396 } else if (event.key in tui.movement_keys) {
1397 explorer.move(tui.movement_keys[event.key]);
1398 } else if (event.key === tui.keys.toggle_tile_draw) {
1399 tui.toggle_tile_draw();
1401 } else if (tui.mode.name == 'admin') {
1402 if (tui.mode.mode_switch_on_key(event)) {
1404 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1405 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1407 } else if (tui.mode.name == 'edit') {
1408 if (tui.mode.mode_switch_on_key(event)) {
1410 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1411 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1412 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1413 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1414 } else if (event.key == tui.keys.toggle_map_mode) {
1415 tui.toggle_map_mode();
1421 rows_selector.addEventListener('input', function() {
1422 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1425 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1426 terminal.initialize();
1429 cols_selector.addEventListener('input', function() {
1430 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1433 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1434 terminal.initialize();
1435 tui.window_width = terminal.cols / 2,
1438 for (let key_selector of key_selectors) {
1439 key_selector.addEventListener('input', function() {
1440 window.localStorage.setItem(key_selector.id, key_selector.value);
1444 window.setInterval(function() {
1445 if (server.connected) {
1446 server.send(['PING']);
1448 server.reconnect_to(server.url);
1449 tui.log_msg('@ attempting reconnect …')
1452 window.setInterval(function() {
1454 let span_decoration = "none";
1455 if (document.activeElement == tui.inputEl) {
1456 val = "on (click outside terminal to change)";
1458 val = "off (click into terminal to change)";
1459 span_decoration = "line-through";
1461 document.getElementById("keyboard_control").textContent = val;
1462 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1463 span.style.textDecoration = span_decoration;
1466 document.getElementById("terminal").onclick = function() {
1467 tui.inputEl.focus();
1469 document.getElementById("help").onclick = function() {
1470 tui.show_help = true;
1473 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1474 const mode = switchEl.id.slice("switch_to_".length);
1475 switchEl.onclick = function() {
1476 tui.switch_mode(mode);
1480 document.getElementById("toggle_tile_draw").onclick = function() {
1481 tui.toggle_tile_draw();
1483 document.getElementById("toggle_map_mode").onclick = function() {
1484 tui.toggle_map_mode();
1487 document.getElementById("take_thing").onclick = function() {
1488 server.send(['TASK:PICK_UP']);
1490 document.getElementById("drop_thing").onclick = function() {
1491 server.send(['TASK:DROP']);
1493 document.getElementById("flatten").onclick = function() {
1494 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1496 document.getElementById("teleport").onclick = function() {
1499 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1500 let direction = move_button.id.split('_')[2].toUpperCase();
1501 move_button.onclick = function() {
1502 if (tui.mode.available_actions.includes("move")
1503 || tui.mode.available_actions.includes("move_explorer")) {
1504 server.send(['TASK:MOVE', direction]);
1506 explorer.move(direction);