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 id="move_table" style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
26 <td style="text-align: center"><button id="square_move_up"></button></td>
27 <td><button id="hex_move_upright"></button></td>
30 <td style="text-align: right;"><button id="square_move_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"></button><button id="hex_move_right"></button></td>
35 <td><button id="hex_move_downleft"></button></td>
36 <td style="text-align: center"><button id="square_move_down"></button></td>
37 <td><button id="hex_move_downright"></button></td>
42 <td><button id="help"></button></td>
45 <td><button id="switch_to_chat"></button><br /></td>
48 <td><button id="switch_to_study"></button></td>
49 <td><button id="toggle_map_mode"></button>
52 <td><button id="switch_to_play"></button></td>
54 <button id="take_thing"></button>
55 <button id="drop_thing"></button>
56 <button id="teleport"></button>
60 <td><button id="switch_to_edit"></button></td>
62 <button id="switch_to_write"></button>
63 <button id="flatten"></button>
64 <button id="switch_to_annotate"></button>
65 <button id="switch_to_portal"></button>
66 <button id="switch_to_name_thing"></button>
67 <button id="switch_to_password"></button>
71 <td><button id="switch_to_admin_enter"></button></td>
73 <button id="switch_to_control_pw_type"></button>
74 <button id="switch_to_control_tile_type"></button>
75 <button id="switch_to_admin_thing_protect"></button>
76 <button id="toggle_tile_draw"></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() {
628 document.getElementById("move_table").hidden = true;
630 for (let key_selector of key_selectors) {
631 this.keys[key_selector.id.slice(4)] = key_selector.value;
633 this.movement_keys = {};
634 let geometry_prefix = 'undefinedMapGeometry_';
635 if (game.map_geometry) {
636 geometry_prefix = game.map_geometry.toLowerCase() + '_';
638 for (const key_name of Object.keys(key_descriptions)) {
639 if (key_name.startsWith(geometry_prefix)) {
640 let direction = key_name.split('_')[2].toUpperCase();
641 let key = this.keys[key_name];
642 this.movement_keys[key] = direction;
645 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
646 if (move_button.id.startsWith('key_')) {
649 move_button.hidden = true;
651 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
652 document.getElementById("move_table").hidden = false;
653 move_button.hidden = false;
655 for (let el of document.getElementsByTagName("button")) {
656 let action_desc = key_descriptions[el.id];
657 let action_key = '[' + this.keys[el.id] + ']';
658 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
661 task_action_on: function(action) {
662 return game.tasks.includes(this.action_tasks[action]);
664 switch_mode: function(mode_name) {
665 if (this.mode.name == 'control_tile_draw') {
666 tui.log_msg('@ finished tile protection drawing.')
668 this.tile_draw = false;
669 if (mode_name == 'admin_enter' && this.is_admin) {
671 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
672 let player_position = game.things[game.player_id].position;
674 for (let t_id in game.things) {
675 if (t_id == game.player_id) {
678 let t = game.things[t_id];
679 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
685 terminal.blink_screen();
686 this.log_msg('? not standing over thing');
689 this.selected_thing_id = thing_id;
692 this.mode = this['mode_' + mode_name];
693 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
694 this.map_mode = 'protections';
695 } else if (this.mode.name != "edit") {
696 this.map_mode = 'terrain + things';
698 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
699 this.inputEl.focus();
701 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
702 explorer.position = game.things[game.player_id].position;
703 if (this.mode.shows_info) {
704 explorer.query_info();
707 this.inputEl.value = "";
708 this.restore_input_values();
709 for (let el of document.getElementsByTagName("button")) {
712 document.getElementById("help").disabled = false;
713 for (const action of this.mode.available_actions) {
714 if (["move", "move_explorer"].includes(action)) {
715 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
716 move_key.disabled = false;
718 } else if (Object.keys(this.action_tasks).includes(action)) {
719 if (this.task_action_on(action)) {
720 document.getElementById(action).disabled = false;
723 document.getElementById(action).disabled = false;
726 for (const mode_name of this.mode.available_modes) {
727 document.getElementById('switch_to_' + mode_name).disabled = false;
729 if (this.mode.name == 'login') {
730 if (this.login_name) {
731 server.send(['LOGIN', this.login_name]);
733 this.log_msg("? need login name");
735 } else if (this.mode.is_single_char_entry) {
736 this.show_help = true;
737 } else if (this.mode.name == 'admin_enter') {
738 this.log_msg('@ enter admin password:')
739 } else if (this.mode.name == 'control_pw_type') {
740 this.log_msg('@ enter protection character for which you want to change the password:')
741 } else if (this.mode.name == 'control_tile_type') {
742 this.log_msg('@ enter protection character which you want to draw:')
743 } else if (this.mode.name == 'admin_thing_protect') {
744 this.log_msg('@ enter thing protection character:')
745 } else if (this.mode.name == 'control_pw_pw') {
746 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
747 } else if (this.mode.name == 'control_tile_draw') {
748 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 + '].')
752 offset_links: function(offset, links) {
753 for (let y in links) {
754 let real_y = offset[0] + parseInt(y);
755 if (!this.links[real_y]) {
756 this.links[real_y] = [];
758 for (let link of links[y]) {
759 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
760 this.links[real_y].push(offset_link);
764 restore_input_values: function() {
765 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
766 let info = explorer.info_db[explorer.position];
767 if (info != "(none)") {
768 this.inputEl.value = info;
770 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
771 let portal = game.portals[explorer.position]
772 this.inputEl.value = portal;
773 } else if (this.mode.name == 'password') {
774 this.inputEl.value = this.password;
775 } else if (this.mode.name == 'name_thing') {
776 let t = game.get_thing(this.selected_thing_id);
778 this.inputEl.value = t.name_;
780 } else if (this.mode.name == 'admin_thing_protect') {
781 let t = game.get_thing(this.selected_thing_id);
782 if (t && t.protection) {
783 this.inputEl.value = t.protection;
787 recalc_input_lines: function() {
788 if (this.mode.has_input_prompt) {
790 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
792 this.input_lines = [];
794 this.height_input = this.input_lines.length;
796 msg_into_lines_of_width: function(msg, width) {
797 function push_inner_link(y, end_x) {
798 if (!inner_links[y]) {
801 inner_links[y].push([url_start_x, end_x, url]);
803 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
806 for (const match of matches) {
807 const url = match[0];
808 const url_start = match.index;
809 const url_end = match.index + match[0].length;
810 link_data[url_start] = url;
811 url_ends.push(url_end);
815 let inner_links = {};
819 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
820 if (x >= width || msg[i] == "\n") {
822 push_inner_link(y, chunk.length);
824 if (url_ends[0] == i) {
832 if (msg[i] == "\n") {
837 if (msg[i] != "\n") {
840 if (i in link_data) {
844 } else if (url_ends[0] == i) {
846 push_inner_link(y, x);
852 push_inner_link(lines.length - 1, chunk.length);
854 return [lines, inner_links];
856 log_msg: function(msg) {
858 while (this.log.length > 100) {
863 draw_map: function() {
864 let map_lines_split = [];
866 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
867 if (j == game.map_size[1]) {
868 map_lines_split.push(line);
872 if (this.map_mode == 'protections') {
873 line.push(game.map_control[i] + ' ');
875 line.push(game.map[i] + ' ');
878 map_lines_split.push(line);
879 if (this.map_mode == 'terrain + annotations') {
880 for (const coordinate of explorer.info_hints) {
881 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
883 } else if (this.map_mode == 'terrain + things') {
884 for (const p in game.portals) {
885 let coordinate = p.split(',')
886 let original = map_lines_split[coordinate[0]][coordinate[1]];
887 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
889 let used_positions = [];
890 for (const thing_id in game.things) {
891 let t = game.things[thing_id];
892 let symbol = game.thing_types[t.type_];
895 meta_char = t.player_char;
897 if (used_positions.includes(t.position.toString())) {
900 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
901 used_positions.push(t.position.toString());
904 let player = game.things[game.player_id];
905 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
906 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
907 } else if (tui.map_mode != 'terrain + things') {
908 map_lines_split[player.position[0]][player.position[1]] = '??';
911 if (game.map_geometry == 'Square') {
912 for (let line_split of map_lines_split) {
913 map_lines.push(line_split.join(''));
915 } else if (game.map_geometry == 'Hex') {
917 for (let line_split of map_lines_split) {
918 map_lines.push(' '.repeat(indent) + line_split.join(''));
926 let window_center = [terminal.rows / 2, this.window_width / 2];
927 let center_position = [player.position[0], player.position[1]];
928 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
929 center_position = [explorer.position[0], explorer.position[1]];
931 center_position[1] = center_position[1] * 2;
932 let offset = [center_position[0] - window_center[0],
933 center_position[1] - window_center[1]]
934 if (game.map_geometry == 'Hex' && offset[0] % 2) {
937 let term_y = Math.max(0, -offset[0]);
938 let term_x = Math.max(0, -offset[1]);
939 let map_y = Math.max(0, offset[0]);
940 let map_x = Math.max(0, offset[1]);
941 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
942 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
943 terminal.write(term_y, term_x, to_draw);
946 draw_mode_line: function() {
947 let help = 'hit [' + this.keys.help + '] for help';
948 if (this.mode.has_input_prompt) {
949 help = 'enter /help for help';
951 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
953 draw_turn_line: function(n) {
954 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
956 draw_history: function() {
957 let log_display_lines = [];
959 let y_offset_in_log = 0;
960 for (let line of this.log) {
961 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
963 log_display_lines = log_display_lines.concat(new_lines);
964 for (const y in link_data) {
965 const rel_y = y_offset_in_log + parseInt(y);
966 log_links[rel_y] = [];
967 for (let link of link_data[y]) {
968 log_links[rel_y].push(link);
971 y_offset_in_log += new_lines.length;
973 let i = log_display_lines.length - 1;
974 for (let y = terminal.rows - 1 - this.height_input;
975 y >= this.height_header && i >= 0;
977 terminal.write(y, this.window_width, log_display_lines[i]);
979 for (const key of Object.keys(log_links)) {
980 if (parseInt(key) <= i) {
981 delete log_links[key];
984 let offset = [terminal.rows - this.height_input - log_display_lines.length,
986 this.offset_links(offset, log_links);
988 draw_info: function() {
989 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
991 let offset = [this.height_header, this.window_width];
992 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
993 terminal.write(y, offset[1], lines[i]);
995 this.offset_links(offset, link_data);
997 draw_input: function() {
998 if (this.mode.has_input_prompt) {
999 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1000 terminal.write(y, this.window_width, this.input_lines[i]);
1004 draw_help: function() {
1005 let movement_keys_desc = '';
1006 if (!this.mode.is_intro) {
1007 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1009 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1010 if (this.mode.name == 'chat') {
1011 content += '/nick NAME – re-name yourself to NAME\n';
1012 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1013 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1014 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1015 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1016 } else if (this.mode.available_actions.length > 0) {
1017 content += "Available actions:\n";
1018 for (let action of this.mode.available_actions) {
1019 if (Object.keys(this.action_tasks).includes(action)) {
1020 if (!this.task_action_on(action)) {
1024 if (action == 'move_explorer') {
1027 if (action == 'move') {
1028 content += "[" + movement_keys_desc + "] – move\n"
1030 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1035 content += this.mode.list_available_modes();
1037 if (!this.mode.has_input_prompt) {
1038 start_x = this.window_width
1040 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1041 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1042 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1043 terminal.write(y, start_x, lines[i]);
1046 toggle_tile_draw: function() {
1047 if (tui.tile_draw) {
1048 tui.tile_draw = false;
1050 tui.tile_draw = true;
1053 toggle_map_mode: function() {
1054 if (tui.map_mode == 'terrain only') {
1055 tui.map_mode = 'terrain + annotations';
1056 } else if (tui.map_mode == 'terrain + annotations') {
1057 tui.map_mode = 'terrain + things';
1058 } else if (tui.map_mode == 'terrain + things') {
1059 tui.map_mode = 'protections';
1060 } else if (tui.map_mode == 'protections') {
1061 tui.map_mode = 'terrain only';
1064 full_refresh: function() {
1066 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1067 this.recalc_input_lines();
1068 if (this.mode.is_intro) {
1069 this.draw_history();
1072 if (game.turn_complete) {
1074 this.draw_turn_line();
1076 this.draw_mode_line();
1077 if (this.mode.shows_info) {
1080 this.draw_history();
1084 if (this.show_help) {
1096 this.map_control = "";
1097 this.map_size = [0,0];
1098 this.player_id = -1;
1102 get_thing: function(id_, create_if_not_found=false) {
1103 if (id_ in game.things) {
1104 return game.things[id_];
1105 } else if (create_if_not_found) {
1106 let t = new Thing([0,0]);
1107 game.things[id_] = t;
1111 move: function(start_position, direction) {
1112 let target = [start_position[0], start_position[1]];
1113 if (direction == 'LEFT') {
1115 } else if (direction == 'RIGHT') {
1117 } else if (game.map_geometry == 'Square') {
1118 if (direction == 'UP') {
1120 } else if (direction == 'DOWN') {
1123 } else if (game.map_geometry == 'Hex') {
1124 let start_indented = start_position[0] % 2;
1125 if (direction == 'UPLEFT') {
1127 if (!start_indented) {
1130 } else if (direction == 'UPRIGHT') {
1132 if (start_indented) {
1135 } else if (direction == 'DOWNLEFT') {
1137 if (!start_indented) {
1140 } else if (direction == 'DOWNRIGHT') {
1142 if (start_indented) {
1147 if (target[0] < 0 || target[1] < 0 ||
1148 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1153 teleport: function() {
1154 let player = this.get_thing(game.player_id);
1155 if (player.position in this.portals) {
1156 server.reconnect_to(this.portals[player.position]);
1158 terminal.blink_screen();
1159 tui.log_msg('? not standing on portal')
1167 server.init(websocket_location);
1173 move: function(direction) {
1174 let target = game.move(this.position, direction);
1176 this.position = target
1177 if (tui.mode.shows_info) {
1179 } else if (tui.tile_draw) {
1180 this.send_tile_control_command();
1183 terminal.blink_screen();
1186 update_info_db: function(yx, str) {
1187 this.info_db[yx] = str;
1188 if (tui.mode.name == 'study') {
1192 empty_info_db: function() {
1194 this.info_hints = [];
1195 if (tui.mode.name == 'study') {
1199 query_info: function() {
1200 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1202 get_info: function() {
1203 let info = "MAP VIEW: " + tui.map_mode + "\n";
1204 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1205 if (game.fov[position_i] != '.') {
1206 return info + 'outside field of view';
1208 let terrain_char = game.map[position_i]
1209 let terrain_desc = '?'
1210 if (game.terrains[terrain_char]) {
1211 terrain_desc = game.terrains[terrain_char];
1213 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1214 let protection = game.map_control[position_i];
1215 if (protection == '.') {
1216 protection = 'unprotected';
1218 info += 'PROTECTION: ' + protection + '\n';
1219 for (let t_id in game.things) {
1220 let t = game.things[t_id];
1221 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1222 let symbol = game.thing_types[t.type_];
1223 let protection = t.protection;
1224 if (protection == '.') {
1225 protection = 'unprotected';
1227 info += "THING: " + t.type_ + " / protection: " + protection + " / " + symbol;
1228 if (t.player_char) {
1229 info += t.player_char;
1232 info += " (" + t.name_ + ")";
1237 if (this.position in game.portals) {
1238 info += "PORTAL: " + game.portals[this.position] + "\n";
1240 if (this.position in this.info_db) {
1241 info += "ANNOTATIONS: " + this.info_db[this.position];
1243 info += 'waiting …';
1247 annotate: function(msg) {
1248 if (msg.length == 0) {
1249 msg = " "; // triggers annotation deletion
1251 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1253 set_portal: function(msg) {
1254 if (msg.length == 0) {
1255 msg = " "; // triggers portal deletion
1257 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1259 send_tile_control_command: function() {
1260 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1264 tui.inputEl.addEventListener('input', (event) => {
1265 if (tui.mode.has_input_prompt) {
1266 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1267 if (tui.inputEl.value.length > max_length) {
1268 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1270 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1271 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1272 tui.switch_mode('edit');
1276 document.onclick = function() {
1277 tui.show_help = false;
1279 tui.inputEl.addEventListener('keydown', (event) => {
1280 tui.show_help = false;
1281 if (event.key == 'Enter') {
1282 event.preventDefault();
1284 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1285 tui.show_help = true;
1286 tui.inputEl.value = "";
1287 tui.restore_input_values();
1288 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1289 && !tui.mode.is_single_char_entry) {
1290 tui.show_help = true;
1291 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1292 tui.login_name = tui.inputEl.value;
1293 server.send(['LOGIN', tui.inputEl.value]);
1294 tui.inputEl.value = "";
1295 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1296 if (tui.inputEl.value.length == 0) {
1297 tui.log_msg('@ aborted');
1299 server.send(['SET_MAP_CONTROL_PASSWORD',
1300 tui.tile_control_char, tui.inputEl.value]);
1301 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1303 tui.switch_mode('admin');
1304 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1305 explorer.set_portal(tui.inputEl.value);
1306 tui.switch_mode('edit');
1307 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1308 if (tui.inputEl.value.length == 0) {
1309 tui.inputEl.value = " ";
1311 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1313 tui.switch_mode('edit');
1314 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1315 explorer.annotate(tui.inputEl.value);
1316 tui.switch_mode('edit');
1317 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1318 if (tui.inputEl.value.length == 0) {
1319 tui.inputEl.value = " ";
1321 tui.password = tui.inputEl.value
1322 tui.switch_mode('edit');
1323 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1324 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1325 tui.switch_mode('play');
1326 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1327 if (tui.inputEl.value.length != 1) {
1328 tui.log_msg('@ entered non-single-char, therefore aborted');
1329 tui.switch_mode('admin');
1331 tui.tile_control_char = tui.inputEl.value[0];
1332 tui.switch_mode('control_pw_pw');
1334 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1335 if (tui.inputEl.value.length != 1) {
1336 tui.log_msg('@ entered non-single-char, therefore aborted');
1337 tui.switch_mode('admin');
1339 tui.tile_control_char = tui.inputEl.value[0];
1340 tui.switch_mode('control_tile_draw');
1342 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1343 if (tui.inputEl.value.length != 1) {
1344 tui.log_msg('@ entered non-single-char, therefore aborted');
1346 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1347 tui.log_msg('@ sent new protection character for thing');
1349 tui.switch_mode('admin');
1350 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1351 let tokens = parser.tokenize(tui.inputEl.value);
1352 if (tokens.length > 0 && tokens[0].length > 0) {
1353 if (tui.inputEl.value[0][0] == '/') {
1354 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1355 tui.switch_mode('play');
1356 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1357 tui.switch_mode('study');
1358 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1359 tui.switch_mode('edit');
1360 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1361 tui.switch_mode('admin_enter');
1362 } else if (tokens[0].slice(1) == 'nick') {
1363 if (tokens.length > 1) {
1364 server.send(['NICK', tokens[1]]);
1366 tui.log_msg('? need new name');
1369 tui.log_msg('? unknown command');
1372 server.send(['ALL', tui.inputEl.value]);
1374 } else if (tui.inputEl.valuelength > 0) {
1375 server.send(['ALL', tui.inputEl.value]);
1377 tui.inputEl.value = "";
1378 } else if (tui.mode.name == 'play') {
1379 if (tui.mode.mode_switch_on_key(event)) {
1381 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1382 server.send(["TASK:PICK_UP"]);
1383 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1384 server.send(["TASK:DROP"]);
1385 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1386 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1387 } else if (event.key === tui.keys.teleport) {
1390 } else if (tui.mode.name == 'study') {
1391 if (tui.mode.mode_switch_on_key(event)) {
1393 } else if (event.key in tui.movement_keys) {
1394 explorer.move(tui.movement_keys[event.key]);
1395 } else if (event.key == tui.keys.toggle_map_mode) {
1396 tui.toggle_map_mode();
1398 } else if (tui.mode.name == 'control_tile_draw') {
1399 if (tui.mode.mode_switch_on_key(event)) {
1401 } else if (event.key in tui.movement_keys) {
1402 explorer.move(tui.movement_keys[event.key]);
1403 } else if (event.key === tui.keys.toggle_tile_draw) {
1404 tui.toggle_tile_draw();
1406 } else if (tui.mode.name == 'admin') {
1407 if (tui.mode.mode_switch_on_key(event)) {
1409 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1410 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1412 } else if (tui.mode.name == 'edit') {
1413 if (tui.mode.mode_switch_on_key(event)) {
1415 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1416 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1417 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1418 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1419 } else if (event.key == tui.keys.toggle_map_mode) {
1420 tui.toggle_map_mode();
1426 rows_selector.addEventListener('input', function() {
1427 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1430 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1431 terminal.initialize();
1434 cols_selector.addEventListener('input', function() {
1435 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1438 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1439 terminal.initialize();
1440 tui.window_width = terminal.cols / 2,
1443 for (let key_selector of key_selectors) {
1444 key_selector.addEventListener('input', function() {
1445 window.localStorage.setItem(key_selector.id, key_selector.value);
1449 window.setInterval(function() {
1450 if (server.connected) {
1451 server.send(['PING']);
1453 server.reconnect_to(server.url);
1454 tui.log_msg('@ attempting reconnect …')
1457 window.setInterval(function() {
1459 let span_decoration = "none";
1460 if (document.activeElement == tui.inputEl) {
1461 val = "on (click outside terminal to change)";
1463 val = "off (click into terminal to change)";
1464 span_decoration = "line-through";
1466 document.getElementById("keyboard_control").textContent = val;
1467 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1468 span.style.textDecoration = span_decoration;
1471 document.getElementById("terminal").onclick = function() {
1472 tui.inputEl.focus();
1474 document.getElementById("help").onclick = function() {
1475 tui.show_help = true;
1478 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1479 const mode = switchEl.id.slice("switch_to_".length);
1480 switchEl.onclick = function() {
1481 tui.switch_mode(mode);
1485 document.getElementById("toggle_tile_draw").onclick = function() {
1486 tui.toggle_tile_draw();
1488 document.getElementById("toggle_map_mode").onclick = function() {
1489 tui.toggle_map_mode();
1492 document.getElementById("take_thing").onclick = function() {
1493 server.send(['TASK:PICK_UP']);
1495 document.getElementById("drop_thing").onclick = function() {
1496 server.send(['TASK:DROP']);
1498 document.getElementById("flatten").onclick = function() {
1499 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1501 document.getElementById("teleport").onclick = function() {
1504 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1505 let direction = move_button.id.split('_')[2].toUpperCase();
1506 move_button.onclick = function() {
1507 if (tui.mode.available_actions.includes("move")
1508 || tui.mode.available_actions.includes("move_explorer")) {
1509 server.send(['TASK:MOVE', direction]);
1511 explorer.move(direction);