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="door"></button>
57 <button id="teleport"></button>
61 <td><button id="switch_to_edit"></button></td>
63 <button id="switch_to_write"></button>
64 <button id="flatten"></button>
65 <button id="switch_to_annotate"></button>
66 <button id="switch_to_portal"></button>
67 <button id="switch_to_name_thing"></button>
68 <button id="switch_to_password"></button>
72 <td><button id="switch_to_admin_enter"></button></td>
74 <button id="switch_to_control_pw_type"></button>
75 <button id="switch_to_control_tile_type"></button>
76 <button id="switch_to_admin_thing_protect"></button>
77 <button id="toggle_tile_draw"></button>
82 <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 />
84 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
85 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
86 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
87 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
88 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
89 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
90 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
91 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
92 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
93 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
94 <li>help: <input id="key_help" type="text" value="h" />
95 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
96 <li>teleport: <input id="key_teleport" type="text" value="p" />
97 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
98 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
99 <li>open/close: <input id="key_door" type="text" value="D" />
100 <li><input id="key_switch_to_chat" type="text" value="t" />
101 <li><input id="key_switch_to_play" type="text" value="p" />
102 <li><input id="key_switch_to_study" type="text" value="?" />
103 <li><input id="key_switch_to_edit" type="text" value="E" />
104 <li><input id="key_switch_to_write" type="text" value="m" />
105 <li><input id="key_switch_to_name_thing" type="text" value="N" />
106 <li><input id="key_switch_to_password" type="text" value="P" />
107 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
108 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
109 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
110 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
111 <li><input id="key_switch_to_annotate" type="text" value="M" />
112 <li><input id="key_switch_to_portal" type="text" value="T" />
113 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
114 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
119 let websocket_location = "wss://plomlompom.com/rogue_chat/";
120 //let websocket_location = "ws://localhost:8000/";
125 'long': 'This mode allows you to interact with the map in various ways.'
129 '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.'},
131 'short': 'world edit',
132 '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.'
135 'short': 'name thing',
136 'long': 'Give name to/change name of thing here.'
138 'admin_thing_protect': {
139 'short': 'change thing protection',
140 'long': 'Change protection character for thing here.'
143 'short': 'change terrain',
144 '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.'
147 'short': 'change protection character password',
148 '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.'
151 'short': 'change protection character password',
152 '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.'
154 'control_tile_type': {
155 'short': 'change tiles protection',
156 '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.'
158 'control_tile_draw': {
159 'short': 'change tiles protection',
160 '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.'
163 'short': 'annotate tile',
164 '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.'
167 'short': 'edit portal',
168 '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.'
172 '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:'
176 'long': 'Enter your player name.'
178 'waiting_for_server': {
179 'short': 'waiting for server response',
180 'long': 'Waiting for a server response.'
183 'short': 'waiting for server response',
184 'long': 'Waiting for a server response.'
187 'short': 'set world edit password',
188 '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.'
191 'short': 'become admin',
192 'long': 'This mode allows you to become admin if you know an admin password.'
196 'long': 'This mode allows you access to actions limited to administrators.'
199 let key_descriptions = {
201 'flatten': 'flatten surroundings',
202 'teleport': 'teleport',
203 'take_thing': 'pick up thing',
204 'drop_thing': 'drop thing',
205 'door': 'open/close',
206 'toggle_map_mode': 'toggle map view',
207 'toggle_tile_draw': 'toggle protection character drawing',
208 'hex_move_upleft': 'up-left',
209 'hex_move_upright': 'up-right',
210 'hex_move_right': 'right',
211 'hex_move_left': 'left',
212 'hex_move_downleft': 'down-left',
213 'hex_move_downright': 'down-right',
214 'square_move_up': 'up',
215 'square_move_left': 'left',
216 'square_move_down': 'down',
217 'square_move_right': 'right',
219 for (const mode_name of Object.keys(mode_helps)) {
220 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
223 let rows_selector = document.getElementById("n_rows");
224 let cols_selector = document.getElementById("n_cols");
225 let key_selectors = document.querySelectorAll('[id^="key_"]');
227 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
228 const action = key_switch_selector.id.slice("key_switch_to_".length);
229 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
232 function restore_selector_value(selector) {
233 let stored_selection = window.localStorage.getItem(selector.id);
234 if (stored_selection) {
235 selector.value = stored_selection;
238 restore_selector_value(rows_selector);
239 restore_selector_value(cols_selector);
240 for (let key_selector of key_selectors) {
241 restore_selector_value(key_selector);
244 function escapeHTML(str) {
246 replace(/&/g, '&').
247 replace(/</g, '<').
248 replace(/>/g, '>').
249 replace(/'/g, ''').
250 replace(/"/g, '"');
256 initialize: function() {
257 this.rows = rows_selector.value;
258 this.cols = cols_selector.value;
259 this.pre_el = document.getElementById("terminal");
260 this.pre_el.style.color = this.foreground;
261 this.pre_el.style.backgroundColor = this.background;
264 for (let y = 0, x = 0; y <= this.rows; x++) {
265 if (x == this.cols) {
268 this.content.push(line);
270 if (y == this.rows) {
277 blink_screen: function() {
278 this.pre_el.style.color = this.background;
279 this.pre_el.style.backgroundColor = this.foreground;
281 this.pre_el.style.color = this.foreground;
282 this.pre_el.style.backgroundColor = this.background;
285 refresh: function() {
286 let pre_content = '';
287 for (let y = 0; y < this.rows; y++) {
288 let line = this.content[y].join('');
290 if (y in tui.links) {
292 for (let span of tui.links[y]) {
293 chunks.push(escapeHTML(line.slice(start_x, span[0])));
294 chunks.push('<a target="_blank" href="');
295 chunks.push(escapeHTML(span[2]));
297 chunks.push(escapeHTML(line.slice(span[0], span[1])));
301 chunks.push(escapeHTML(line.slice(start_x)));
303 chunks = [escapeHTML(line)];
305 for (const chunk of chunks) {
306 pre_content += chunk;
310 this.pre_el.innerHTML = pre_content;
312 write: function(start_y, start_x, msg) {
313 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
314 this.content[start_y][x] = msg[i];
317 drawBox: function(start_y, start_x, height, width) {
318 let end_y = start_y + height;
319 let end_x = start_x + width;
320 for (let y = start_y, x = start_x; y < this.rows; x++) {
328 this.content[y][x] = ' ';
332 terminal.initialize();
335 tokenize: function(str) {
340 for (let i = 0; i < str.length; i++) {
346 } else if (c == '\\') {
348 } else if (c == '"') {
353 } else if (c == '"') {
355 } else if (c === ' ') {
356 if (token.length > 0) {
364 if (token.length > 0) {
369 parse_yx: function(position_string) {
370 let coordinate_strings = position_string.split(',')
371 let position = [0, 0];
372 position[0] = parseInt(coordinate_strings[0].slice(2));
373 position[1] = parseInt(coordinate_strings[1].slice(2));
385 init: function(url) {
387 this.websocket = new WebSocket(this.url);
388 this.websocket.onopen = function(event) {
389 server.connected = true;
390 game.thing_types = {};
392 server.send(['TASKS']);
393 server.send(['TERRAINS']);
394 server.send(['THING_TYPES']);
395 tui.log_msg("@ server connected! :)");
396 tui.switch_mode('login');
398 this.websocket.onclose = function(event) {
399 server.connected = false;
400 tui.switch_mode('waiting_for_server');
401 tui.log_msg("@ server disconnected :(");
403 this.websocket.onmessage = this.handle_event;
405 reconnect_to: function(url) {
406 this.websocket.close();
409 send: function(tokens) {
410 this.websocket.send(unparser.untokenize(tokens));
412 handle_event: function(event) {
413 let tokens = parser.tokenize(event.data);
414 if (tokens[0] === 'TURN') {
415 game.turn_complete = false;
416 explorer.empty_info_db();
419 game.turn = parseInt(tokens[1]);
420 } else if (tokens[0] === 'THING') {
421 let t = game.get_thing(tokens[4], true);
422 t.position = parser.parse_yx(tokens[1]);
424 t.protection = tokens[3];
425 } else if (tokens[0] === 'THING_NAME') {
426 let t = game.get_thing(tokens[1], false);
430 } else if (tokens[0] === 'THING_CHAR') {
431 let t = game.get_thing(tokens[1], false);
433 t.thing_char = tokens[2];
435 } else if (tokens[0] === 'TASKS') {
436 game.tasks = tokens[1].split(',');
437 console.log(game.tasks);
438 tui.mode_write.legal = game.tasks.includes('WRITE');
439 } else if (tokens[0] === 'THING_TYPE') {
440 game.thing_types[tokens[1]] = tokens[2]
441 } else if (tokens[0] === 'TERRAIN') {
442 game.terrains[tokens[1]] = tokens[2]
443 } else if (tokens[0] === 'MAP') {
444 game.map_geometry = tokens[1];
446 game.map_size = parser.parse_yx(tokens[2]);
448 } else if (tokens[0] === 'FOV') {
450 } else if (tokens[0] === 'MAP_CONTROL') {
451 game.map_control = tokens[1]
452 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
453 game.turn_complete = true;
454 if (tui.mode.name == 'post_login_wait') {
455 tui.switch_mode('play');
456 } else if (tui.mode.name == 'study') {
457 explorer.query_info();
460 } else if (tokens[0] === 'CHAT') {
461 tui.log_msg('# ' + tokens[1], 1);
462 } else if (tokens[0] === 'PLAYER_ID') {
463 game.player_id = parseInt(tokens[1]);
464 } else if (tokens[0] === 'LOGIN_OK') {
465 this.send(['GET_GAMESTATE']);
466 tui.switch_mode('post_login_wait');
467 } else if (tokens[0] === 'ADMIN_OK') {
469 tui.log_msg('@ you now have admin rights');
470 tui.switch_mode('admin');
471 } else if (tokens[0] === 'PORTAL') {
472 let position = parser.parse_yx(tokens[1]);
473 game.portals[position] = tokens[2];
474 } else if (tokens[0] === 'ANNOTATION_HINT') {
475 let position = parser.parse_yx(tokens[1]);
476 explorer.info_hints = explorer.info_hints.concat([position]);
477 } else if (tokens[0] === 'ANNOTATION') {
478 let position = parser.parse_yx(tokens[1]);
479 explorer.update_info_db(position, tokens[2]);
481 } else if (tokens[0] === 'UNHANDLED_INPUT') {
482 tui.log_msg('? unknown command');
483 } else if (tokens[0] === 'PLAY_ERROR') {
484 tui.log_msg('? ' + tokens[1]);
485 terminal.blink_screen();
486 } else if (tokens[0] === 'ARGUMENT_ERROR') {
487 tui.log_msg('? syntax error: ' + tokens[1]);
488 } else if (tokens[0] === 'GAME_ERROR') {
489 tui.log_msg('? game error: ' + tokens[1]);
490 } else if (tokens[0] === 'PONG') {
493 tui.log_msg('? unhandled input: ' + event.data);
499 quote: function(str) {
501 for (let i = 0; i < str.length; i++) {
503 if (['"', '\\'].includes(c)) {
509 return quoted.join('');
511 to_yx: function(yx_coordinate) {
512 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
514 untokenize: function(tokens) {
515 let quoted_tokens = [];
516 for (let token of tokens) {
517 quoted_tokens.push(this.quote(token));
519 return quoted_tokens.join(" ");
524 constructor(name, has_input_prompt=false, shows_info=false,
525 is_intro=false, is_single_char_entry=false) {
527 this.short_desc = mode_helps[name].short;
528 this.available_modes = [];
529 this.available_actions = [];
530 this.has_input_prompt = has_input_prompt;
531 this.shows_info= shows_info;
532 this.is_intro = is_intro;
533 this.help_intro = mode_helps[name].long;
534 this.is_single_char_entry = is_single_char_entry;
537 *iter_available_modes() {
538 for (let mode_name of this.available_modes) {
539 let mode = tui['mode_' + mode_name];
543 let key = tui.keys['switch_to_' + mode.name];
547 list_available_modes() {
549 if (this.available_modes.length > 0) {
550 msg += 'Other modes available from here:\n';
551 for (let [mode, key] of this.iter_available_modes()) {
552 msg += '[' + key + '] – ' + mode.short_desc + '\n';
557 mode_switch_on_key(key_event) {
558 for (let [mode, key] of this.iter_available_modes()) {
559 if (key_event.key == key) {
560 event.preventDefault();
561 tui.switch_mode(mode.name);
573 window_width: terminal.cols / 2,
581 mode_waiting_for_server: new Mode('waiting_for_server',
583 mode_login: new Mode('login', true, false, true),
584 mode_post_login_wait: new Mode('post_login_wait'),
585 mode_chat: new Mode('chat', true),
586 mode_annotate: new Mode('annotate', true, true),
587 mode_play: new Mode('play'),
588 mode_study: new Mode('study', false, true),
589 mode_write: new Mode('write', false, false, false, true),
590 mode_edit: new Mode('edit'),
591 mode_control_pw_type: new Mode('control_pw_type', true),
592 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
593 mode_portal: new Mode('portal', true, true),
594 mode_password: new Mode('password', true),
595 mode_name_thing: new Mode('name_thing', true, true),
596 mode_admin_enter: new Mode('admin_enter', true),
597 mode_admin: new Mode('admin'),
598 mode_control_pw_pw: new Mode('control_pw_pw', true),
599 mode_control_tile_type: new Mode('control_tile_type', true),
600 mode_control_tile_draw: new Mode('control_tile_draw'),
602 'flatten': 'FLATTEN_SURROUNDINGS',
603 'take_thing': 'PICK_UP',
604 'drop_thing': 'DROP',
609 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
610 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
611 this.mode_play.available_actions = ["move", "take_thing", "drop_thing",
613 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
614 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
615 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
616 "control_tile_type", "chat",
617 "study", "play", "edit"]
618 this.mode_admin.available_actions = ["move"];
619 this.mode_control_tile_draw.available_modes = ["admin_enter"]
620 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
621 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
622 "password", "chat", "study", "play",
624 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
625 this.mode = this.mode_waiting_for_server;
626 this.inputEl = document.getElementById("input");
627 this.inputEl.focus();
628 this.recalc_input_lines();
629 this.height_header = this.height_turn_line + this.height_mode_line;
630 this.log_msg("@ waiting for server connection ...");
633 init_keys: function() {
634 document.getElementById("move_table").hidden = true;
636 for (let key_selector of key_selectors) {
637 this.keys[key_selector.id.slice(4)] = key_selector.value;
639 this.movement_keys = {};
640 let geometry_prefix = 'undefinedMapGeometry_';
641 if (game.map_geometry) {
642 geometry_prefix = game.map_geometry.toLowerCase() + '_';
644 for (const key_name of Object.keys(key_descriptions)) {
645 if (key_name.startsWith(geometry_prefix)) {
646 let direction = key_name.split('_')[2].toUpperCase();
647 let key = this.keys[key_name];
648 this.movement_keys[key] = direction;
651 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
652 if (move_button.id.startsWith('key_')) {
655 move_button.hidden = true;
657 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
658 document.getElementById("move_table").hidden = false;
659 move_button.hidden = false;
661 for (let el of document.getElementsByTagName("button")) {
662 let action_desc = key_descriptions[el.id];
663 let action_key = '[' + this.keys[el.id] + ']';
664 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
667 task_action_on: function(action) {
668 return game.tasks.includes(this.action_tasks[action]);
670 switch_mode: function(mode_name) {
671 if (this.mode.name == 'control_tile_draw') {
672 tui.log_msg('@ finished tile protection drawing.')
674 this.tile_draw = false;
675 if (mode_name == 'admin_enter' && this.is_admin) {
677 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
678 let player_position = game.things[game.player_id].position;
680 for (let t_id in game.things) {
681 if (t_id == game.player_id) {
684 let t = game.things[t_id];
685 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
691 terminal.blink_screen();
692 this.log_msg('? not standing over thing');
695 this.selected_thing_id = thing_id;
698 this.mode = this['mode_' + mode_name];
699 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
700 this.map_mode = 'protections';
701 } else if (this.mode.name != "edit") {
702 this.map_mode = 'terrain + things';
704 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
705 this.inputEl.focus();
707 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
708 explorer.position = game.things[game.player_id].position;
709 if (this.mode.shows_info) {
710 explorer.query_info();
713 this.inputEl.value = "";
714 this.restore_input_values();
715 for (let el of document.getElementsByTagName("button")) {
718 document.getElementById("help").disabled = false;
719 for (const action of this.mode.available_actions) {
720 if (["move", "move_explorer"].includes(action)) {
721 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
722 move_key.disabled = false;
724 } else if (Object.keys(this.action_tasks).includes(action)) {
725 if (this.task_action_on(action)) {
726 document.getElementById(action).disabled = false;
729 document.getElementById(action).disabled = false;
732 for (const mode_name of this.mode.available_modes) {
733 document.getElementById('switch_to_' + mode_name).disabled = false;
735 if (this.mode.name == 'login') {
736 if (this.login_name) {
737 server.send(['LOGIN', this.login_name]);
739 this.log_msg("? need login name");
741 } else if (this.mode.is_single_char_entry) {
742 this.show_help = true;
743 } else if (this.mode.name == 'admin_enter') {
744 this.log_msg('@ enter admin password:')
745 } else if (this.mode.name == 'control_pw_type') {
746 this.log_msg('@ enter protection character for which you want to change the password:')
747 } else if (this.mode.name == 'control_tile_type') {
748 this.log_msg('@ enter protection character which you want to draw:')
749 } else if (this.mode.name == 'admin_thing_protect') {
750 this.log_msg('@ enter thing protection character:')
751 } else if (this.mode.name == 'control_pw_pw') {
752 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
753 } else if (this.mode.name == 'control_tile_draw') {
754 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 + '].')
758 offset_links: function(offset, links) {
759 for (let y in links) {
760 let real_y = offset[0] + parseInt(y);
761 if (!this.links[real_y]) {
762 this.links[real_y] = [];
764 for (let link of links[y]) {
765 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
766 this.links[real_y].push(offset_link);
770 restore_input_values: function() {
771 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
772 let info = explorer.info_db[explorer.position];
773 if (info != "(none)") {
774 this.inputEl.value = info;
776 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
777 let portal = game.portals[explorer.position]
778 this.inputEl.value = portal;
779 } else if (this.mode.name == 'password') {
780 this.inputEl.value = this.password;
781 } else if (this.mode.name == 'name_thing') {
782 let t = game.get_thing(this.selected_thing_id);
784 this.inputEl.value = t.name_;
786 } else if (this.mode.name == 'admin_thing_protect') {
787 let t = game.get_thing(this.selected_thing_id);
788 if (t && t.protection) {
789 this.inputEl.value = t.protection;
793 recalc_input_lines: function() {
794 if (this.mode.has_input_prompt) {
796 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
798 this.input_lines = [];
800 this.height_input = this.input_lines.length;
802 msg_into_lines_of_width: function(msg, width) {
803 function push_inner_link(y, end_x) {
804 if (!inner_links[y]) {
807 inner_links[y].push([url_start_x, end_x, url]);
809 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
812 for (const match of matches) {
813 const url = match[0];
814 const url_start = match.index;
815 const url_end = match.index + match[0].length;
816 link_data[url_start] = url;
817 url_ends.push(url_end);
821 let inner_links = {};
825 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
826 if (x >= width || msg[i] == "\n") {
828 push_inner_link(y, chunk.length);
830 if (url_ends[0] == i) {
838 if (msg[i] == "\n") {
843 if (msg[i] != "\n") {
846 if (i in link_data) {
850 } else if (url_ends[0] == i) {
852 push_inner_link(y, x);
858 push_inner_link(lines.length - 1, chunk.length);
860 return [lines, inner_links];
862 log_msg: function(msg) {
864 while (this.log.length > 100) {
869 draw_map: function() {
870 let map_lines_split = [];
872 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
873 if (j == game.map_size[1]) {
874 map_lines_split.push(line);
878 if (this.map_mode == 'protections') {
879 line.push(game.map_control[i] + ' ');
881 line.push(game.map[i] + ' ');
884 map_lines_split.push(line);
885 if (this.map_mode == 'terrain + annotations') {
886 for (const coordinate of explorer.info_hints) {
887 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
889 } else if (this.map_mode == 'terrain + things') {
890 for (const p in game.portals) {
891 let coordinate = p.split(',')
892 let original = map_lines_split[coordinate[0]][coordinate[1]];
893 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
895 let used_positions = [];
896 for (const thing_id in game.things) {
897 let t = game.things[thing_id];
898 let symbol = game.thing_types[t.type_];
901 meta_char = t.thing_char;
903 if (used_positions.includes(t.position.toString())) {
906 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
907 used_positions.push(t.position.toString());
910 let player = game.things[game.player_id];
911 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
912 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
913 } else if (tui.map_mode != 'terrain + things') {
914 map_lines_split[player.position[0]][player.position[1]] = '??';
917 if (game.map_geometry == 'Square') {
918 for (let line_split of map_lines_split) {
919 map_lines.push(line_split.join(''));
921 } else if (game.map_geometry == 'Hex') {
923 for (let line_split of map_lines_split) {
924 map_lines.push(' '.repeat(indent) + line_split.join(''));
932 let window_center = [terminal.rows / 2, this.window_width / 2];
933 let center_position = [player.position[0], player.position[1]];
934 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
935 center_position = [explorer.position[0], explorer.position[1]];
937 center_position[1] = center_position[1] * 2;
938 let offset = [center_position[0] - window_center[0],
939 center_position[1] - window_center[1]]
940 if (game.map_geometry == 'Hex' && offset[0] % 2) {
943 let term_y = Math.max(0, -offset[0]);
944 let term_x = Math.max(0, -offset[1]);
945 let map_y = Math.max(0, offset[0]);
946 let map_x = Math.max(0, offset[1]);
947 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
948 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
949 terminal.write(term_y, term_x, to_draw);
952 draw_mode_line: function() {
953 let help = 'hit [' + this.keys.help + '] for help';
954 if (this.mode.has_input_prompt) {
955 help = 'enter /help for help';
957 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
959 draw_turn_line: function(n) {
960 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
962 draw_history: function() {
963 let log_display_lines = [];
965 let y_offset_in_log = 0;
966 for (let line of this.log) {
967 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
969 log_display_lines = log_display_lines.concat(new_lines);
970 for (const y in link_data) {
971 const rel_y = y_offset_in_log + parseInt(y);
972 log_links[rel_y] = [];
973 for (let link of link_data[y]) {
974 log_links[rel_y].push(link);
977 y_offset_in_log += new_lines.length;
979 let i = log_display_lines.length - 1;
980 for (let y = terminal.rows - 1 - this.height_input;
981 y >= this.height_header && i >= 0;
983 terminal.write(y, this.window_width, log_display_lines[i]);
985 for (const key of Object.keys(log_links)) {
986 if (parseInt(key) <= i) {
987 delete log_links[key];
990 let offset = [terminal.rows - this.height_input - log_display_lines.length,
992 this.offset_links(offset, log_links);
994 draw_info: function() {
995 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
997 let offset = [this.height_header, this.window_width];
998 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
999 terminal.write(y, offset[1], lines[i]);
1001 this.offset_links(offset, link_data);
1003 draw_input: function() {
1004 if (this.mode.has_input_prompt) {
1005 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1006 terminal.write(y, this.window_width, this.input_lines[i]);
1010 draw_help: function() {
1011 let movement_keys_desc = '';
1012 if (!this.mode.is_intro) {
1013 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1015 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1016 if (this.mode.name == 'chat') {
1017 content += '/nick NAME – re-name yourself to NAME\n';
1018 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1019 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1020 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1021 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1022 } else if (this.mode.available_actions.length > 0) {
1023 content += "Available actions:\n";
1024 for (let action of this.mode.available_actions) {
1025 if (Object.keys(this.action_tasks).includes(action)) {
1026 if (!this.task_action_on(action)) {
1030 if (action == 'move_explorer') {
1033 if (action == 'move') {
1034 content += "[" + movement_keys_desc + "] – move\n"
1036 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1041 content += this.mode.list_available_modes();
1043 if (!this.mode.has_input_prompt) {
1044 start_x = this.window_width
1046 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1047 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1048 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1049 terminal.write(y, start_x, lines[i]);
1052 toggle_tile_draw: function() {
1053 if (tui.tile_draw) {
1054 tui.tile_draw = false;
1056 tui.tile_draw = true;
1059 toggle_map_mode: function() {
1060 if (tui.map_mode == 'terrain only') {
1061 tui.map_mode = 'terrain + annotations';
1062 } else if (tui.map_mode == 'terrain + annotations') {
1063 tui.map_mode = 'terrain + things';
1064 } else if (tui.map_mode == 'terrain + things') {
1065 tui.map_mode = 'protections';
1066 } else if (tui.map_mode == 'protections') {
1067 tui.map_mode = 'terrain only';
1070 full_refresh: function() {
1072 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1073 this.recalc_input_lines();
1074 if (this.mode.is_intro) {
1075 this.draw_history();
1078 if (game.turn_complete) {
1080 this.draw_turn_line();
1082 this.draw_mode_line();
1083 if (this.mode.shows_info) {
1086 this.draw_history();
1090 if (this.show_help) {
1102 this.map_control = "";
1103 this.map_size = [0,0];
1104 this.player_id = -1;
1108 get_thing: function(id_, create_if_not_found=false) {
1109 if (id_ in game.things) {
1110 return game.things[id_];
1111 } else if (create_if_not_found) {
1112 let t = new Thing([0,0]);
1113 game.things[id_] = t;
1117 move: function(start_position, direction) {
1118 let target = [start_position[0], start_position[1]];
1119 if (direction == 'LEFT') {
1121 } else if (direction == 'RIGHT') {
1123 } else if (game.map_geometry == 'Square') {
1124 if (direction == 'UP') {
1126 } else if (direction == 'DOWN') {
1129 } else if (game.map_geometry == 'Hex') {
1130 let start_indented = start_position[0] % 2;
1131 if (direction == 'UPLEFT') {
1133 if (!start_indented) {
1136 } else if (direction == 'UPRIGHT') {
1138 if (start_indented) {
1141 } else if (direction == 'DOWNLEFT') {
1143 if (!start_indented) {
1146 } else if (direction == 'DOWNRIGHT') {
1148 if (start_indented) {
1153 if (target[0] < 0 || target[1] < 0 ||
1154 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1159 teleport: function() {
1160 let player = this.get_thing(game.player_id);
1161 if (player.position in this.portals) {
1162 server.reconnect_to(this.portals[player.position]);
1164 terminal.blink_screen();
1165 tui.log_msg('? not standing on portal')
1173 server.init(websocket_location);
1179 move: function(direction) {
1180 let target = game.move(this.position, direction);
1182 this.position = target
1183 if (tui.mode.shows_info) {
1185 } else if (tui.tile_draw) {
1186 this.send_tile_control_command();
1189 terminal.blink_screen();
1192 update_info_db: function(yx, str) {
1193 this.info_db[yx] = str;
1194 if (tui.mode.name == 'study') {
1198 empty_info_db: function() {
1200 this.info_hints = [];
1201 if (tui.mode.name == 'study') {
1205 query_info: function() {
1206 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1208 get_info: function() {
1209 let info = "MAP VIEW: " + tui.map_mode + "\n";
1210 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1211 if (game.fov[position_i] != '.') {
1212 return info + 'outside field of view';
1214 let terrain_char = game.map[position_i]
1215 let terrain_desc = '?'
1216 if (game.terrains[terrain_char]) {
1217 terrain_desc = game.terrains[terrain_char];
1219 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1220 let protection = game.map_control[position_i];
1221 if (protection == '.') {
1222 protection = 'unprotected';
1224 info += 'PROTECTION: ' + protection + '\n';
1225 for (let t_id in game.things) {
1226 let t = game.things[t_id];
1227 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1228 let symbol = game.thing_types[t.type_];
1229 let protection = t.protection;
1230 if (protection == '.') {
1231 protection = 'none';
1233 info += "THING: " + t.type_ + " / " + symbol;
1235 info += t.thing_char;
1238 info += " (" + t.name_ + ")";
1240 info += " / protection: " + protection + "\n";
1243 if (this.position in game.portals) {
1244 info += "PORTAL: " + game.portals[this.position] + "\n";
1246 if (this.position in this.info_db) {
1247 info += "ANNOTATIONS: " + this.info_db[this.position];
1249 info += 'waiting …';
1253 annotate: function(msg) {
1254 if (msg.length == 0) {
1255 msg = " "; // triggers annotation deletion
1257 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1259 set_portal: function(msg) {
1260 if (msg.length == 0) {
1261 msg = " "; // triggers portal deletion
1263 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1265 send_tile_control_command: function() {
1266 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1270 tui.inputEl.addEventListener('input', (event) => {
1271 if (tui.mode.has_input_prompt) {
1272 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1273 if (tui.inputEl.value.length > max_length) {
1274 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1276 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1277 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1278 tui.switch_mode('edit');
1282 document.onclick = function() {
1283 tui.show_help = false;
1285 tui.inputEl.addEventListener('keydown', (event) => {
1286 tui.show_help = false;
1287 if (event.key == 'Enter') {
1288 event.preventDefault();
1290 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1291 tui.show_help = true;
1292 tui.inputEl.value = "";
1293 tui.restore_input_values();
1294 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1295 && !tui.mode.is_single_char_entry) {
1296 tui.show_help = true;
1297 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1298 tui.login_name = tui.inputEl.value;
1299 server.send(['LOGIN', tui.inputEl.value]);
1300 tui.inputEl.value = "";
1301 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1302 if (tui.inputEl.value.length == 0) {
1303 tui.log_msg('@ aborted');
1305 server.send(['SET_MAP_CONTROL_PASSWORD',
1306 tui.tile_control_char, tui.inputEl.value]);
1307 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1309 tui.switch_mode('admin');
1310 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1311 explorer.set_portal(tui.inputEl.value);
1312 tui.switch_mode('edit');
1313 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1314 if (tui.inputEl.value.length == 0) {
1315 tui.inputEl.value = " ";
1317 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1319 tui.switch_mode('edit');
1320 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1321 explorer.annotate(tui.inputEl.value);
1322 tui.switch_mode('edit');
1323 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1324 if (tui.inputEl.value.length == 0) {
1325 tui.inputEl.value = " ";
1327 tui.password = tui.inputEl.value
1328 tui.switch_mode('edit');
1329 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1330 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1331 tui.switch_mode('play');
1332 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1333 if (tui.inputEl.value.length != 1) {
1334 tui.log_msg('@ entered non-single-char, therefore aborted');
1335 tui.switch_mode('admin');
1337 tui.tile_control_char = tui.inputEl.value[0];
1338 tui.switch_mode('control_pw_pw');
1340 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1341 if (tui.inputEl.value.length != 1) {
1342 tui.log_msg('@ entered non-single-char, therefore aborted');
1343 tui.switch_mode('admin');
1345 tui.tile_control_char = tui.inputEl.value[0];
1346 tui.switch_mode('control_tile_draw');
1348 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1349 if (tui.inputEl.value.length != 1) {
1350 tui.log_msg('@ entered non-single-char, therefore aborted');
1352 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1353 tui.log_msg('@ sent new protection character for thing');
1355 tui.switch_mode('admin');
1356 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1357 let tokens = parser.tokenize(tui.inputEl.value);
1358 if (tokens.length > 0 && tokens[0].length > 0) {
1359 if (tui.inputEl.value[0][0] == '/') {
1360 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1361 tui.switch_mode('play');
1362 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1363 tui.switch_mode('study');
1364 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1365 tui.switch_mode('edit');
1366 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1367 tui.switch_mode('admin_enter');
1368 } else if (tokens[0].slice(1) == 'nick') {
1369 if (tokens.length > 1) {
1370 server.send(['NICK', tokens[1]]);
1372 tui.log_msg('? need new name');
1375 tui.log_msg('? unknown command');
1378 server.send(['ALL', tui.inputEl.value]);
1380 } else if (tui.inputEl.valuelength > 0) {
1381 server.send(['ALL', tui.inputEl.value]);
1383 tui.inputEl.value = "";
1384 } else if (tui.mode.name == 'play') {
1385 if (tui.mode.mode_switch_on_key(event)) {
1387 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1388 server.send(["TASK:PICK_UP"]);
1389 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1390 server.send(["TASK:DROP"]);
1391 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1392 server.send(["TASK:DOOR"]);
1393 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1394 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1395 } else if (event.key === tui.keys.teleport) {
1398 } else if (tui.mode.name == 'study') {
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_map_mode) {
1404 tui.toggle_map_mode();
1406 } else if (tui.mode.name == 'control_tile_draw') {
1407 if (tui.mode.mode_switch_on_key(event)) {
1409 } else if (event.key in tui.movement_keys) {
1410 explorer.move(tui.movement_keys[event.key]);
1411 } else if (event.key === tui.keys.toggle_tile_draw) {
1412 tui.toggle_tile_draw();
1414 } else if (tui.mode.name == 'admin') {
1415 if (tui.mode.mode_switch_on_key(event)) {
1417 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1418 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1420 } else if (tui.mode.name == 'edit') {
1421 if (tui.mode.mode_switch_on_key(event)) {
1423 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1424 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1425 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1426 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1427 } else if (event.key == tui.keys.toggle_map_mode) {
1428 tui.toggle_map_mode();
1434 rows_selector.addEventListener('input', function() {
1435 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1438 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1439 terminal.initialize();
1442 cols_selector.addEventListener('input', function() {
1443 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1446 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1447 terminal.initialize();
1448 tui.window_width = terminal.cols / 2,
1451 for (let key_selector of key_selectors) {
1452 key_selector.addEventListener('input', function() {
1453 window.localStorage.setItem(key_selector.id, key_selector.value);
1457 window.setInterval(function() {
1458 if (server.connected) {
1459 server.send(['PING']);
1461 server.reconnect_to(server.url);
1462 tui.log_msg('@ attempting reconnect …')
1465 window.setInterval(function() {
1467 let span_decoration = "none";
1468 if (document.activeElement == tui.inputEl) {
1469 val = "on (click outside terminal to change)";
1471 val = "off (click into terminal to change)";
1472 span_decoration = "line-through";
1474 document.getElementById("keyboard_control").textContent = val;
1475 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1476 span.style.textDecoration = span_decoration;
1479 document.getElementById("terminal").onclick = function() {
1480 tui.inputEl.focus();
1482 document.getElementById("help").onclick = function() {
1483 tui.show_help = true;
1486 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1487 const mode = switchEl.id.slice("switch_to_".length);
1488 switchEl.onclick = function() {
1489 tui.switch_mode(mode);
1493 document.getElementById("toggle_tile_draw").onclick = function() {
1494 tui.toggle_tile_draw();
1496 document.getElementById("toggle_map_mode").onclick = function() {
1497 tui.toggle_map_mode();
1500 document.getElementById("take_thing").onclick = function() {
1501 server.send(['TASK:PICK_UP']);
1503 document.getElementById("drop_thing").onclick = function() {
1504 server.send(['TASK:DROP']);
1506 document.getElementById("flatten").onclick = function() {
1507 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1509 document.getElementById("door").onclick = function() {
1510 server.send(['TASK:DOOR']);
1512 document.getElementById("teleport").onclick = function() {
1515 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1516 let direction = move_button.id.split('_')[2].toUpperCase();
1517 move_button.onclick = function() {
1518 if (tui.mode.available_actions.includes("move")
1519 || tui.mode.available_actions.includes("move_explorer")) {
1520 server.send(['TASK:MOVE', direction]);
1522 explorer.move(direction);