13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table style="float: left">
25 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
26 <td style="text-align: center"><button id="move_up">up</button></td>
27 <td><button id="move_upright">up-right</button></td>
30 <td style="text-align: right;"><button id="move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="move_right">right</button></td>
35 <td><button id="move_downleft">down-left</button></td>
36 <td style="text-align: center"><button id="move_down">down</button></td>
37 <td><button id="move_downright">down-right</button></td>
42 <td><button id="help">help</button></td>
45 <td><button id="switch_to_chat">chat mode</button><br /></td>
48 <td><button id="switch_to_study">study mode</button></td>
49 <td><button id="toggle_map_mode">toggle map view</button>
52 <td><button id="switch_to_play">play mode</button></td>
54 <button id="take_thing">pick up thing</button>
55 <button id="drop_thing">drop thing</button>
56 <button id="teleport">teleport</button>
60 <td><button id="switch_to_edit">map edit mode</button></td>
62 <button id="switch_to_write">change terrain</button>
63 <button id="flatten">flatten surroundings</button>
64 <button id="switch_to_annotate">annotate tile</button>
65 <button id="switch_to_portal">edit portal</button>
66 <button id="switch_to_password">enter map edit password</button>
70 <td><button id="switch_to_admin_enter">admin mode</button></td>
72 <button id="switch_to_control_pw_type">change protection character password</button>
73 <button id="switch_to_control_tile_type">change protection areas</button>
74 <button id="toggle_tile_draw">toggle protection character drawing</button>
79 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
81 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
82 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
83 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
84 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
85 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
86 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
87 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
88 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
89 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
90 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
91 <li>help: <input id="key_help" type="text" value="h" />
92 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
93 <li>teleport: <input id="key_teleport" type="text" value="p" />
94 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
95 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
96 <li><input id="key_switch_to_chat" type="text" value="t" />
97 <li><input id="key_switch_to_play" type="text" value="p" />
98 <li><input id="key_switch_to_study" type="text" value="?" />
99 <li><input id="key_switch_to_edit" type="text" value="E" />
100 <li><input id="key_switch_to_write" type="text" value="m" />
101 <li><input id="key_switch_to_password" type="text" value="P" />
102 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
103 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
104 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
105 <li><input id="key_switch_to_annotate" type="text" value="M" />
106 <li><input id="key_switch_to_portal" type="text" value="T" />
107 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
108 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
113 let websocket_location = "wss://plomlompom.com/rogue_chat/";
114 //let websocket_location = "ws://localhost:8000/";
119 'long': 'This mode allows you to interact with the map in various ways.'
123 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
126 'long': 'This mode allows you to change the map in various ways. Individual map tiles 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 map edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
129 'short': 'change terrain',
130 'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
133 'short': 'change protection character password',
134 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
137 'short': 'change tiles protection password',
138 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
140 'control_tile_type': {
141 'short': 'change tiles protection',
142 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
144 'control_tile_draw': {
145 'short': 'change tiles protection',
146 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected tile protection character.'
149 'short': 'annotate tile',
150 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.'
153 'short': 'edit portal',
154 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
158 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:'
162 'long': 'Enter your player name.'
164 'waiting_for_server': {
165 'short': 'waiting for server response',
166 'long': 'Waiting for a server response.'
169 'short': 'waiting for server response',
170 'long': 'Waiting for a server response.'
173 'short': 'set map edit password',
174 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.'
177 'short': 'become admin',
178 'long': 'This mode allows you to become admin if you know an admin password.'
182 'long': 'This mode allows you access to actions limited to administrators.'
186 let rows_selector = document.getElementById("n_rows");
187 let cols_selector = document.getElementById("n_cols");
188 let key_selectors = document.querySelectorAll('[id^="key_"]');
190 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
191 const action = key_switch_selector.id.slice("key_switch_to_".length);
192 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
195 function restore_selector_value(selector) {
196 let stored_selection = window.localStorage.getItem(selector.id);
197 if (stored_selection) {
198 selector.value = stored_selection;
201 restore_selector_value(rows_selector);
202 restore_selector_value(cols_selector);
203 for (let key_selector of key_selectors) {
204 restore_selector_value(key_selector);
210 initialize: function() {
211 this.rows = rows_selector.value;
212 this.cols = cols_selector.value;
213 this.pre_el = document.getElementById("terminal");
214 this.pre_el.style.color = this.foreground;
215 this.pre_el.style.backgroundColor = this.background;
218 for (let y = 0, x = 0; y <= this.rows; x++) {
219 if (x == this.cols) {
222 this.content.push(line);
224 if (y == this.rows) {
231 blink_screen: function() {
232 this.pre_el.style.color = this.background;
233 this.pre_el.style.backgroundColor = this.foreground;
235 this.pre_el.style.color = this.foreground;
236 this.pre_el.style.backgroundColor = this.background;
239 refresh: function() {
240 function escapeHTML(str) {
242 replace(/&/g, '&').
243 replace(/</g, '<').
244 replace(/>/g, '>').
245 replace(/'/g, ''').
246 replace(/"/g, '"');
248 let pre_content = '';
249 for (let y = 0; y < this.rows; y++) {
250 let line = this.content[y].join('');
252 if (y in tui.links) {
254 for (let span of tui.links[y]) {
255 chunks.push(escapeHTML(line.slice(start_x, span[0])));
256 chunks.push('<a target="_blank" href="');
257 chunks.push(escapeHTML(span[2]));
259 chunks.push(escapeHTML(line.slice(span[0], span[1])));
263 chunks.push(escapeHTML(line.slice(start_x)));
265 chunks = [escapeHTML(line)];
267 for (const chunk of chunks) {
268 pre_content += chunk;
272 this.pre_el.innerHTML = pre_content;
274 write: function(start_y, start_x, msg) {
275 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
276 this.content[start_y][x] = msg[i];
279 drawBox: function(start_y, start_x, height, width) {
280 let end_y = start_y + height;
281 let end_x = start_x + width;
282 for (let y = start_y, x = start_x; y < this.rows; x++) {
290 this.content[y][x] = ' ';
294 terminal.initialize();
297 tokenize: function(str) {
302 for (let i = 0; i < str.length; i++) {
308 } else if (c == '\\') {
310 } else if (c == '"') {
315 } else if (c == '"') {
317 } else if (c === ' ') {
318 if (token.length > 0) {
326 if (token.length > 0) {
331 parse_yx: function(position_string) {
332 let coordinate_strings = position_string.split(',')
333 let position = [0, 0];
334 position[0] = parseInt(coordinate_strings[0].slice(2));
335 position[1] = parseInt(coordinate_strings[1].slice(2));
347 init: function(url) {
349 this.websocket = new WebSocket(this.url);
350 this.websocket.onopen = function(event) {
351 server.connected = true;
352 game.thing_types = {};
354 server.send(['TASKS']);
355 server.send(['TERRAINS']);
356 server.send(['THING_TYPES']);
357 tui.log_msg("@ server connected! :)");
358 tui.switch_mode('login');
360 this.websocket.onclose = function(event) {
361 server.connected = false;
362 tui.switch_mode('waiting_for_server');
363 tui.log_msg("@ server disconnected :(");
365 this.websocket.onmessage = this.handle_event;
367 reconnect_to: function(url) {
368 this.websocket.close();
371 send: function(tokens) {
372 this.websocket.send(unparser.untokenize(tokens));
374 handle_event: function(event) {
375 let tokens = parser.tokenize(event.data);
376 if (tokens[0] === 'TURN') {
377 game.turn_complete = false;
378 explorer.empty_info_db();
381 game.turn = parseInt(tokens[1]);
382 } else if (tokens[0] === 'THING') {
383 let t = game.get_thing(tokens[3], true);
384 t.position = parser.parse_yx(tokens[1]);
386 } else if (tokens[0] === 'THING_NAME') {
387 let t = game.get_thing(tokens[1], false);
391 } else if (tokens[0] === 'THING_CHAR') {
392 let t = game.get_thing(tokens[1], false);
394 t.player_char = tokens[2];
396 } else if (tokens[0] === 'TASKS') {
397 game.tasks = tokens[1].split(',');
398 tui.mode_write.legal = game.tasks.includes('WRITE');
399 } else if (tokens[0] === 'THING_TYPE') {
400 game.thing_types[tokens[1]] = tokens[2]
401 } else if (tokens[0] === 'TERRAIN') {
402 game.terrains[tokens[1]] = tokens[2]
403 } else if (tokens[0] === 'MAP') {
404 game.map_geometry = tokens[1];
406 game.map_size = parser.parse_yx(tokens[2]);
408 } else if (tokens[0] === 'FOV') {
410 } else if (tokens[0] === 'MAP_CONTROL') {
411 game.map_control = tokens[1]
412 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
413 game.turn_complete = true;
414 if (tui.mode.name == 'post_login_wait') {
415 tui.switch_mode('play');
416 } else if (tui.mode.name == 'study') {
417 explorer.query_info();
420 } else if (tokens[0] === 'CHAT') {
421 tui.log_msg('# ' + tokens[1], 1);
422 } else if (tokens[0] === 'PLAYER_ID') {
423 game.player_id = parseInt(tokens[1]);
424 } else if (tokens[0] === 'LOGIN_OK') {
425 this.send(['GET_GAMESTATE']);
426 tui.switch_mode('post_login_wait');
427 } else if (tokens[0] === 'ADMIN_OK') {
429 tui.log_msg('@ you now have admin rights');
430 tui.switch_mode('admin');
431 } else if (tokens[0] === 'PORTAL') {
432 let position = parser.parse_yx(tokens[1]);
433 game.portals[position] = tokens[2];
434 } else if (tokens[0] === 'ANNOTATION_HINT') {
435 let position = parser.parse_yx(tokens[1]);
436 explorer.info_hints = explorer.info_hints.concat([position]);
437 } else if (tokens[0] === 'ANNOTATION') {
438 let position = parser.parse_yx(tokens[1]);
439 explorer.update_info_db(position, tokens[2]);
440 tui.restore_input_values();
442 } else if (tokens[0] === 'UNHANDLED_INPUT') {
443 tui.log_msg('? unknown command');
444 } else if (tokens[0] === 'PLAY_ERROR') {
445 tui.log_msg('? ' + tokens[1]);
446 terminal.blink_screen();
447 } else if (tokens[0] === 'ARGUMENT_ERROR') {
448 tui.log_msg('? syntax error: ' + tokens[1]);
449 } else if (tokens[0] === 'GAME_ERROR') {
450 tui.log_msg('? game error: ' + tokens[1]);
451 } else if (tokens[0] === 'PONG') {
454 tui.log_msg('? unhandled input: ' + event.data);
460 quote: function(str) {
462 for (let i = 0; i < str.length; i++) {
464 if (['"', '\\'].includes(c)) {
470 return quoted.join('');
472 to_yx: function(yx_coordinate) {
473 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
475 untokenize: function(tokens) {
476 let quoted_tokens = [];
477 for (let token of tokens) {
478 quoted_tokens.push(this.quote(token));
480 return quoted_tokens.join(" ");
485 constructor(name, has_input_prompt=false, shows_info=false,
486 is_intro=false, is_single_char_entry=false) {
488 this.short_desc = mode_helps[name].short;
489 this.available_modes = [];
490 this.has_input_prompt = has_input_prompt;
491 this.shows_info= shows_info;
492 this.is_intro = is_intro;
493 this.help_intro = mode_helps[name].long;
494 this.is_single_char_entry = is_single_char_entry;
497 *iter_available_modes() {
498 for (let mode_name of this.available_modes) {
499 let mode = tui['mode_' + mode_name];
503 let key = tui.keys['switch_to_' + mode.name];
507 list_available_modes() {
509 if (this.available_modes.length > 0) {
510 msg += 'Other modes available from here:\n';
511 for (let [mode, key] of this.iter_available_modes()) {
512 msg += '[' + key + '] – ' + mode.short_desc + '\n';
517 mode_switch_on_key(key_event) {
518 for (let [mode, key] of this.iter_available_modes()) {
519 if (key_event.key == key) {
520 event.preventDefault();
521 tui.switch_mode(mode.name);
533 window_width: terminal.cols / 2,
541 mode_waiting_for_server: new Mode('waiting_for_server',
543 mode_login: new Mode('login', true, false, true),
544 mode_post_login_wait: new Mode('post_login_wait'),
545 mode_chat: new Mode('chat', true),
546 mode_annotate: new Mode('annotate', true, true),
547 mode_play: new Mode('play'),
548 mode_study: new Mode('study', false, true),
549 mode_write: new Mode('write', false, false, false, true),
550 mode_edit: new Mode('edit'),
551 mode_control_pw_type: new Mode('control_pw_type', true),
552 mode_portal: new Mode('portal', true, true),
553 mode_password: new Mode('password', true),
554 mode_admin_enter: new Mode('admin_enter', true),
555 mode_admin: new Mode('admin'),
556 mode_control_pw_pw: new Mode('control_pw_pw', true),
557 mode_control_tile_type: new Mode('control_tile_type', true),
558 mode_control_tile_draw: new Mode('control_tile_draw'),
560 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
561 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
562 this.mode_admin.available_modes = ["control_pw_type",
563 "control_tile_type", "chat",
564 "study", "play", "edit"]
565 this.mode_control_tile_draw.available_modes = ["admin_enter"]
566 this.mode_edit.available_modes = ["write", "annotate", "portal",
567 "password", "chat", "study", "play",
569 this.mode = this.mode_waiting_for_server;
570 this.inputEl = document.getElementById("input");
571 this.inputEl.focus();
572 this.recalc_input_lines();
573 this.height_header = this.height_turn_line + this.height_mode_line;
574 this.log_msg("@ waiting for server connection ...");
577 init_keys: function() {
579 for (let key_selector of key_selectors) {
580 this.keys[key_selector.id.slice(4)] = key_selector.value;
582 if (game.map_geometry == 'Square') {
583 this.movement_keys = {
584 [this.keys.square_move_up]: 'UP',
585 [this.keys.square_move_left]: 'LEFT',
586 [this.keys.square_move_down]: 'DOWN',
587 [this.keys.square_move_right]: 'RIGHT'
589 document.getElementById("move_upright").hidden = true;
590 document.getElementById("move_upleft").hidden = true;
591 document.getElementById("move_downright").hidden = true;
592 document.getElementById("move_downleft").hidden = true;
593 document.getElementById("move_up").hidden = false;
594 document.getElementById("move_down").hidden = false;
595 } else if (game.map_geometry == 'Hex') {
596 document.getElementById("move_upright").hidden = false;
597 document.getElementById("move_upleft").hidden = false;
598 document.getElementById("move_downright").hidden = false;
599 document.getElementById("move_downleft").hidden = false;
600 document.getElementById("move_up").hidden = true;
601 document.getElementById("move_down").hidden = true;
602 this.movement_keys = {
603 [this.keys.hex_move_upleft]: 'UPLEFT',
604 [this.keys.hex_move_upright]: 'UPRIGHT',
605 [this.keys.hex_move_right]: 'RIGHT',
606 [this.keys.hex_move_downright]: 'DOWNRIGHT',
607 [this.keys.hex_move_downleft]: 'DOWNLEFT',
608 [this.keys.hex_move_left]: 'LEFT'
612 switch_mode: function(mode_name) {
613 if (this.mode.name == 'control_tile_draw') {
614 tui.log_msg('@ finished tile protection drawing.')
616 this.tile_draw = false;
617 if (mode_name == 'admin_enter' && this.is_admin) {
620 this.mode = this['mode_' + mode_name];
621 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
622 this.map_mode = 'protections';
623 } else if (this.mode.name != "edit") {
624 this.map_mode = 'terrain + things';
626 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
627 this.inputEl.focus();
629 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
630 explorer.position = game.things[game.player_id].position;
631 if (this.mode.shows_info) {
632 explorer.query_info();
636 this.restore_input_values();
637 for (let el of document.getElementsByTagName("button")) {
640 document.getElementById("help").disabled = false;
641 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
642 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
643 move_key.disabled = false;
646 if (!this.mode.is_intro && this.mode.name != 'play') {
647 document.getElementById("switch_to_play").disabled = false;
649 if (!this.mode.is_intro && this.mode.name != 'study') {
650 document.getElementById("switch_to_study").disabled = false;
652 if (!this.mode.is_intro && this.mode.name != 'chat') {
653 document.getElementById("switch_to_chat").disabled = false;
655 if (!this.mode.is_intro && this.mode.name != 'edit') {
656 document.getElementById("switch_to_edit").disabled = false;
658 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
659 document.getElementById("switch_to_admin_enter").disabled = false;
661 if (this.mode.name == 'login') {
662 if (this.login_name) {
663 server.send(['LOGIN', this.login_name]);
665 this.log_msg("? need login name");
667 } else if (this.mode.name == 'play') {
668 if (game.tasks.includes('PICK_UP')) {
669 document.getElementById("take_thing").disabled = false;
671 if (game.tasks.includes('DROP')) {
672 document.getElementById("drop_thing").disabled = false;
674 if (game.tasks.includes('MOVE')) {
676 document.getElementById("teleport").disabled = false;
677 } else if (this.mode.name == 'edit') {
678 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
679 document.getElementById("flatten").disabled = false;
681 document.getElementById("switch_to_annotate").disabled = false;
682 document.getElementById("switch_to_write").disabled = false;
683 document.getElementById("switch_to_portal").disabled = false;
684 document.getElementById("switch_to_password").disabled = false;
685 document.getElementById("toggle_map_mode").disabled = false;
686 } else if (this.mode.name == 'admin') {
687 document.getElementById("switch_to_control_pw_type").disabled = false;
688 document.getElementById("switch_to_control_tile_type").disabled = false;
689 } else if (this.mode.name == 'study') {
690 document.getElementById("toggle_map_mode").disabled = false;
691 } else if (this.mode.is_single_char_entry) {
692 this.show_help = true;
693 } else if (this.mode.name == 'admin_enter') {
694 this.log_msg('@ enter admin password:')
695 } else if (this.mode.name == 'control_pw_type') {
696 this.log_msg('@ enter tile protection character for which you want to change the password:')
697 } else if (this.mode.name == 'control_tile_type') {
698 this.log_msg('@ enter tile protection character which you want to draw:')
699 } else if (this.mode.name == 'control_pw_pw') {
700 this.log_msg('@ enter tile protection password for "' + this.tile_control_char + '":');
701 } else if (this.mode.name == 'control_tile_draw') {
702 document.getElementById("toggle_tile_draw").disabled = false;
703 this.log_msg('@ can draw tile protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
707 offset_links: function(offset, links) {
708 for (let y in links) {
709 let real_y = offset[0] + parseInt(y);
710 if (!this.links[real_y]) {
711 this.links[real_y] = [];
713 for (let link of links[y]) {
714 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
715 this.links[real_y].push(offset_link);
719 restore_input_values: function() {
720 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
721 let info = explorer.info_db[explorer.position];
722 if (info != "(none)") {
723 this.inputEl.value = info;
724 this.recalc_input_lines();
726 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
727 let portal = game.portals[explorer.position]
728 this.inputEl.value = portal;
729 this.recalc_input_lines();
730 } else if (this.mode.name == 'password') {
731 this.inputEl.value = this.password;
732 this.recalc_input_lines();
735 empty_input: function(str) {
736 this.inputEl.value = "";
737 if (this.mode.has_input_prompt) {
738 this.recalc_input_lines();
740 this.height_input = 0;
743 recalc_input_lines: function() {
745 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
746 this.height_input = this.input_lines.length;
748 msg_into_lines_of_width: function(msg, width) {
749 function push_inner_link(y, end_x) {
750 if (!inner_links[y]) {
753 inner_links[y].push([url_start_x, end_x, url]);
755 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
758 for (const match of matches) {
759 const url = match[0];
760 const url_start = match.index;
761 const url_end = match.index + match[0].length;
762 link_data[url_start] = url;
763 url_ends.push(url_end);
767 let inner_links = {};
771 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
772 if (x >= width || msg[i] == "\n") {
774 push_inner_link(y, chunk.length);
780 if (msg[i] == "\n") {
785 if (msg[i] != "\n") {
788 if (i in link_data) {
792 } else if (url_ends.includes(i)) {
793 push_inner_link(y, x);
799 push_inner_link(lines.length - 1, chunk.length);
801 return [lines, inner_links];
803 log_msg: function(msg) {
805 while (this.log.length > 100) {
810 draw_map: function() {
811 let map_lines_split = [];
813 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
814 if (j == game.map_size[1]) {
815 map_lines_split.push(line);
819 if (this.map_mode == 'protections') {
820 line.push(game.map_control[i] + ' ');
822 line.push(game.map[i] + ' ');
825 map_lines_split.push(line);
826 if (this.map_mode == 'terrain + annotations') {
827 for (const coordinate of explorer.info_hints) {
828 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
830 } else if (this.map_mode == 'terrain + things') {
831 for (const p in game.portals) {
832 let coordinate = p.split(',')
833 let original = map_lines_split[coordinate[0]][coordinate[1]];
834 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
836 let used_positions = [];
837 for (const thing_id in game.things) {
838 let t = game.things[thing_id];
839 let symbol = game.thing_types[t.type_];
842 meta_char = t.player_char;
844 if (used_positions.includes(t.position.toString())) {
847 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
848 used_positions.push(t.position.toString());
851 let player = game.things[game.player_id];
852 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
853 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
854 } else if (tui.map_mode != 'terrain + things') {
855 map_lines_split[player.position[0]][player.position[1]] = '??';
858 if (game.map_geometry == 'Square') {
859 for (let line_split of map_lines_split) {
860 map_lines.push(line_split.join(''));
862 } else if (game.map_geometry == 'Hex') {
864 for (let line_split of map_lines_split) {
865 map_lines.push(' '.repeat(indent) + line_split.join(''));
873 let window_center = [terminal.rows / 2, this.window_width / 2];
874 let center_position = [player.position[0], player.position[1]];
875 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
876 center_position = [explorer.position[0], explorer.position[1]];
878 center_position[1] = center_position[1] * 2;
879 let offset = [center_position[0] - window_center[0],
880 center_position[1] - window_center[1]]
881 if (game.map_geometry == 'Hex' && offset[0] % 2) {
884 let term_y = Math.max(0, -offset[0]);
885 let term_x = Math.max(0, -offset[1]);
886 let map_y = Math.max(0, offset[0]);
887 let map_x = Math.max(0, offset[1]);
888 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
889 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
890 terminal.write(term_y, term_x, to_draw);
893 draw_mode_line: function() {
894 let help = 'hit [' + this.keys.help + '] for help';
895 if (this.mode.has_input_prompt) {
896 help = 'enter /help for help';
898 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
900 draw_turn_line: function(n) {
901 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
903 draw_history: function() {
904 let log_display_lines = [];
906 let y_offset_in_log = 0;
907 for (let line of this.log) {
908 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
910 log_display_lines = log_display_lines.concat(new_lines);
911 for (const y in link_data) {
912 const rel_y = y_offset_in_log + parseInt(y);
913 log_links[rel_y] = [];
914 for (let link of link_data[y]) {
915 log_links[rel_y].push(link);
918 y_offset_in_log += new_lines.length;
920 let i = log_display_lines.length - 1;
921 for (let y = terminal.rows - 1 - this.height_input;
922 y >= this.height_header && i >= 0;
924 terminal.write(y, this.window_width, log_display_lines[i]);
926 for (const key of Object.keys(log_links)) {
927 if (parseInt(key) <= i) {
928 delete log_links[key];
931 let offset = [terminal.rows - this.height_input - log_display_lines.length,
933 this.offset_links(offset, log_links);
935 draw_info: function() {
936 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
938 let offset = [this.height_header, this.window_width];
939 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
940 terminal.write(y, offset[1], lines[i]);
942 this.offset_links(offset, link_data);
944 draw_input: function() {
945 if (this.mode.has_input_prompt) {
946 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
947 terminal.write(y, this.window_width, this.input_lines[i]);
951 draw_help: function() {
952 let movement_keys_desc = '';
953 if (!this.mode.is_intro) {
954 movement_keys_desc = Object.keys(this.movement_keys).join(',');
956 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
957 if (this.mode.name == 'play') {
958 content += "Available actions:\n";
959 if (game.tasks.includes('MOVE')) {
960 content += "[" + movement_keys_desc + "] – move player\n";
962 if (game.tasks.includes('PICK_UP')) {
963 content += "[" + this.keys.take_thing + "] – pick up thing\n";
965 if (game.tasks.includes('DROP')) {
966 content += "[" + this.keys.drop_thing + "] – drop thing\n";
968 content += "[" + tui.keys.teleport + "] – teleport\n";
970 } else if (this.mode.name == 'study') {
971 content += "Available actions:\n";
972 content += '[' + movement_keys_desc + '] – move question mark\n';
973 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
975 } else if (this.mode.name == 'edit') {
976 content += "Available actions:\n";
977 if (game.tasks.includes('MOVE')) {
978 content += "[" + movement_keys_desc + "] – move player\n";
980 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
981 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
983 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
985 } else if (this.mode.name == 'control_tile_draw') {
986 content += "Available actions:\n";
987 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
989 } else if (this.mode.name == 'chat') {
990 content += '/nick NAME – re-name yourself to NAME\n';
991 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
992 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
993 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
994 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
996 content += this.mode.list_available_modes();
998 if (!this.mode.has_input_prompt) {
999 start_x = this.window_width
1001 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1002 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1003 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1004 terminal.write(y, start_x, lines[i]);
1007 toggle_tile_draw: function() {
1008 if (tui.tile_draw) {
1009 tui.tile_draw = false;
1011 tui.tile_draw = true;
1014 toggle_map_mode: function() {
1015 if (tui.map_mode == 'terrain only') {
1016 tui.map_mode = 'terrain + annotations';
1017 } else if (tui.map_mode == 'terrain + annotations') {
1018 tui.map_mode = 'terrain + things';
1019 } else if (tui.map_mode == 'terrain + things') {
1020 tui.map_mode = 'protections';
1021 } else if (tui.map_mode == 'protections') {
1022 tui.map_mode = 'terrain only';
1025 full_refresh: function() {
1027 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1028 if (this.mode.is_intro) {
1029 this.draw_history();
1032 if (game.turn_complete) {
1034 this.draw_turn_line();
1036 this.draw_mode_line();
1037 if (this.mode.shows_info) {
1040 this.draw_history();
1044 if (this.show_help) {
1056 this.map_control = "";
1057 this.map_size = [0,0];
1058 this.player_id = -1;
1062 get_thing: function(id_, create_if_not_found=false) {
1063 if (id_ in game.things) {
1064 return game.things[id_];
1065 } else if (create_if_not_found) {
1066 let t = new Thing([0,0]);
1067 game.things[id_] = t;
1071 move: function(start_position, direction) {
1072 let target = [start_position[0], start_position[1]];
1073 if (direction == 'LEFT') {
1075 } else if (direction == 'RIGHT') {
1077 } else if (game.map_geometry == 'Square') {
1078 if (direction == 'UP') {
1080 } else if (direction == 'DOWN') {
1083 } else if (game.map_geometry == 'Hex') {
1084 let start_indented = start_position[0] % 2;
1085 if (direction == 'UPLEFT') {
1087 if (!start_indented) {
1090 } else if (direction == 'UPRIGHT') {
1092 if (start_indented) {
1095 } else if (direction == 'DOWNLEFT') {
1097 if (!start_indented) {
1100 } else if (direction == 'DOWNRIGHT') {
1102 if (start_indented) {
1107 if (target[0] < 0 || target[1] < 0 ||
1108 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1113 teleport: function() {
1114 let player = this.get_thing(game.player_id);
1115 if (player.position in this.portals) {
1116 server.reconnect_to(this.portals[player.position]);
1118 terminal.blink_screen();
1119 tui.log_msg('? not standing on portal')
1127 server.init(websocket_location);
1133 move: function(direction) {
1134 let target = game.move(this.position, direction);
1136 this.position = target
1137 if (tui.mode.shows_info) {
1139 } else if (tui.tile_draw) {
1140 this.send_tile_control_command();
1143 terminal.blink_screen();
1146 update_info_db: function(yx, str) {
1147 this.info_db[yx] = str;
1148 if (tui.mode.name == 'study') {
1152 empty_info_db: function() {
1154 this.info_hints = [];
1155 if (tui.mode.name == 'study') {
1159 query_info: function() {
1160 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1162 get_info: function() {
1163 let info = "MAP VIEW: " + tui.map_mode + "\n";
1164 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1165 if (game.fov[position_i] != '.') {
1166 return info + 'outside field of view';
1168 let terrain_char = game.map[position_i]
1169 let terrain_desc = '?'
1170 if (game.terrains[terrain_char]) {
1171 terrain_desc = game.terrains[terrain_char];
1173 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1174 let protection = game.map_control[position_i];
1175 if (protection == '.') {
1176 protection = 'unprotected';
1178 info += 'PROTECTION: ' + protection + '\n';
1179 for (let t_id in game.things) {
1180 let t = game.things[t_id];
1181 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1182 let symbol = game.thing_types[t.type_];
1183 info += "THING: " + t.type_ + " / " + symbol;
1184 if (t.player_char) {
1185 info += t.player_char;
1188 info += " (" + t.name_ + ")";
1193 if (this.position in game.portals) {
1194 info += "PORTAL: " + game.portals[this.position] + "\n";
1196 if (this.position in this.info_db) {
1197 info += "ANNOTATIONS: " + this.info_db[this.position];
1199 info += 'waiting …';
1203 annotate: function(msg) {
1204 if (msg.length == 0) {
1205 msg = " "; // triggers annotation deletion
1207 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1209 set_portal: function(msg) {
1210 if (msg.length == 0) {
1211 msg = " "; // triggers portal deletion
1213 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1215 send_tile_control_command: function() {
1216 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1220 tui.inputEl.addEventListener('input', (event) => {
1221 if (tui.mode.has_input_prompt) {
1222 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1223 if (tui.inputEl.value.length > max_length) {
1224 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1226 tui.recalc_input_lines();
1227 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1228 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1229 tui.switch_mode('edit');
1233 document.onclick = function() {
1234 tui.show_help = false;
1236 tui.inputEl.addEventListener('keydown', (event) => {
1237 tui.show_help = false;
1238 if (event.key == 'Enter') {
1239 event.preventDefault();
1241 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1242 tui.show_help = true;
1244 tui.restore_input_values();
1245 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1246 && !tui.mode.is_single_char_entry) {
1247 tui.show_help = true;
1248 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1249 tui.login_name = tui.inputEl.value;
1250 server.send(['LOGIN', tui.inputEl.value]);
1252 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1253 if (tui.inputEl.value.length == 0) {
1254 tui.log_msg('@ aborted');
1256 server.send(['SET_MAP_CONTROL_PASSWORD',
1257 tui.tile_control_char, tui.inputEl.value]);
1258 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1260 tui.switch_mode('admin');
1261 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1262 explorer.set_portal(tui.inputEl.value);
1263 tui.switch_mode('edit');
1264 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1265 explorer.annotate(tui.inputEl.value);
1266 tui.switch_mode('edit');
1267 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1268 if (tui.inputEl.value.length == 0) {
1269 tui.inputEl.value = " ";
1271 tui.password = tui.inputEl.value
1272 tui.switch_mode('edit');
1273 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1274 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1275 tui.switch_mode('play');
1276 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1277 if (tui.inputEl.value.length != 1) {
1278 tui.log_msg('@ entered non-single-char, therefore aborted');
1279 tui.switch_mode('admin');
1281 tui.tile_control_char = tui.inputEl.value[0];
1282 tui.switch_mode('control_pw_pw');
1284 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1285 if (tui.inputEl.value.length != 1) {
1286 tui.log_msg('@ entered non-single-char, therefore aborted');
1287 tui.switch_mode('admin');
1289 tui.tile_control_char = tui.inputEl.value[0];
1290 tui.switch_mode('control_tile_draw');
1292 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1293 let tokens = parser.tokenize(tui.inputEl.value);
1294 if (tokens.length > 0 && tokens[0].length > 0) {
1295 if (tui.inputEl.value[0][0] == '/') {
1296 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1297 tui.switch_mode('play');
1298 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1299 tui.switch_mode('study');
1300 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1301 tui.switch_mode('edit');
1302 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1303 tui.switch_mode('admin_enter');
1304 } else if (tokens[0].slice(1) == 'nick') {
1305 if (tokens.length > 1) {
1306 server.send(['NICK', tokens[1]]);
1308 tui.log_msg('? need new name');
1311 tui.log_msg('? unknown command');
1314 server.send(['ALL', tui.inputEl.value]);
1316 } else if (tui.inputEl.valuelength > 0) {
1317 server.send(['ALL', tui.inputEl.value]);
1320 } else if (tui.mode.name == 'play') {
1321 if (tui.mode.mode_switch_on_key(event)) {
1323 } else if (event.key === tui.keys.take_thing
1324 && game.tasks.includes('PICK_UP')) {
1325 server.send(["TASK:PICK_UP"]);
1326 } else if (event.key === tui.keys.drop_thing
1327 && game.tasks.includes('DROP')) {
1328 server.send(["TASK:DROP"]);
1329 } else if (event.key in tui.movement_keys
1330 && game.tasks.includes('MOVE')) {
1331 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1332 } else if (event.key === tui.keys.teleport) {
1335 } else if (tui.mode.name == 'study') {
1336 if (tui.mode.mode_switch_on_key(event)) {
1338 } else if (event.key in tui.movement_keys) {
1339 explorer.move(tui.movement_keys[event.key]);
1340 } else if (event.key == tui.keys.toggle_map_mode) {
1341 tui.toggle_map_mode();
1343 } else if (tui.mode.name == 'control_tile_draw') {
1344 if (tui.mode.mode_switch_on_key(event)) {
1346 } else if (event.key in tui.movement_keys) {
1347 explorer.move(tui.movement_keys[event.key]);
1348 } else if (event.key === tui.keys.toggle_tile_draw) {
1349 tui.toggle_tile_draw();
1351 } else if (tui.mode.name == 'admin') {
1352 if (tui.mode.mode_switch_on_key(event)) {
1355 } else if (tui.mode.name == 'edit') {
1356 if (tui.mode.mode_switch_on_key(event)) {
1358 } else if (event.key in tui.movement_keys
1359 && game.tasks.includes('MOVE')) {
1360 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1361 } else if (event.key === tui.keys.flatten
1362 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1363 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1364 } else if (event.key == tui.keys.toggle_map_mode) {
1365 tui.toggle_map_mode();
1371 rows_selector.addEventListener('input', function() {
1372 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1375 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1376 terminal.initialize();
1379 cols_selector.addEventListener('input', function() {
1380 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1383 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1384 terminal.initialize();
1385 tui.window_width = terminal.cols / 2,
1388 for (let key_selector of key_selectors) {
1389 key_selector.addEventListener('input', function() {
1390 window.localStorage.setItem(key_selector.id, key_selector.value);
1394 window.setInterval(function() {
1395 if (server.connected) {
1396 server.send(['PING']);
1398 server.reconnect_to(server.url);
1399 tui.log_msg('@ attempting reconnect …')
1402 window.setInterval(function() {
1404 if (document.activeElement == tui.inputEl) {
1405 val = "on (click outside terminal to change)";
1407 val = "off (click into terminal to change)";
1409 document.getElementById("keyboard_control").textContent = val;
1411 document.getElementById("terminal").onclick = function() {
1412 tui.inputEl.focus();
1414 document.getElementById("help").onclick = function() {
1415 tui.show_help = true;
1418 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1419 const mode = switchEl.id.slice("switch_to_".length);
1420 switchEl.onclick = function() {
1421 tui.switch_mode(mode);
1425 document.getElementById("toggle_tile_draw").onclick = function() {
1426 tui.toggle_tile_draw();
1428 document.getElementById("toggle_map_mode").onclick = function() {
1429 tui.toggle_map_mode();
1432 document.getElementById("take_thing").onclick = function() {
1433 server.send(['TASK:PICK_UP']);
1435 document.getElementById("drop_thing").onclick = function() {
1436 server.send(['TASK:DROP']);
1438 document.getElementById("flatten").onclick = function() {
1439 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1441 document.getElementById("teleport").onclick = function() {
1444 document.getElementById("move_upleft").onclick = function() {
1445 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1446 server.send(['TASK:MOVE', 'UPLEFT']);
1448 explorer.move('UPLEFT');
1451 document.getElementById("move_left").onclick = function() {
1452 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1453 server.send(['TASK:MOVE', 'LEFT']);
1455 explorer.move('LEFT');
1458 document.getElementById("move_downleft").onclick = function() {
1459 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1460 server.send(['TASK:MOVE', 'DOWNLEFT']);
1462 explorer.move('DOWNLEFT');
1465 document.getElementById("move_down").onclick = function() {
1466 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1467 server.send(['TASK:MOVE', 'DOWN']);
1469 explorer.move('DOWN');
1472 document.getElementById("move_up").onclick = function() {
1473 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1474 server.send(['TASK:MOVE', 'UP']);
1476 explorer.move('UP');
1479 document.getElementById("move_upright").onclick = function() {
1480 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1481 server.send(['TASK:MOVE', 'UPRIGHT']);
1483 explorer.move('UPRIGHT');
1486 document.getElementById("move_right").onclick = function() {
1487 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1488 server.send(['TASK:MOVE', 'RIGHT']);
1490 explorer.move('RIGHT');
1493 document.getElementById("move_downright").onclick = function() {
1494 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1495 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1497 explorer.move('DOWNRIGHT');