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]);
441 } else if (tokens[0] === 'UNHANDLED_INPUT') {
442 tui.log_msg('? unknown command');
443 } else if (tokens[0] === 'PLAY_ERROR') {
444 tui.log_msg('? ' + tokens[1]);
445 terminal.blink_screen();
446 } else if (tokens[0] === 'ARGUMENT_ERROR') {
447 tui.log_msg('? syntax error: ' + tokens[1]);
448 } else if (tokens[0] === 'GAME_ERROR') {
449 tui.log_msg('? game error: ' + tokens[1]);
450 } else if (tokens[0] === 'PONG') {
453 tui.log_msg('? unhandled input: ' + event.data);
459 quote: function(str) {
461 for (let i = 0; i < str.length; i++) {
463 if (['"', '\\'].includes(c)) {
469 return quoted.join('');
471 to_yx: function(yx_coordinate) {
472 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
474 untokenize: function(tokens) {
475 let quoted_tokens = [];
476 for (let token of tokens) {
477 quoted_tokens.push(this.quote(token));
479 return quoted_tokens.join(" ");
484 constructor(name, has_input_prompt=false, shows_info=false,
485 is_intro=false, is_single_char_entry=false) {
487 this.short_desc = mode_helps[name].short;
488 this.available_modes = [];
489 this.has_input_prompt = has_input_prompt;
490 this.shows_info= shows_info;
491 this.is_intro = is_intro;
492 this.help_intro = mode_helps[name].long;
493 this.is_single_char_entry = is_single_char_entry;
496 *iter_available_modes() {
497 for (let mode_name of this.available_modes) {
498 let mode = tui['mode_' + mode_name];
502 let key = tui.keys['switch_to_' + mode.name];
506 list_available_modes() {
508 if (this.available_modes.length > 0) {
509 msg += 'Other modes available from here:\n';
510 for (let [mode, key] of this.iter_available_modes()) {
511 msg += '[' + key + '] – ' + mode.short_desc + '\n';
516 mode_switch_on_key(key_event) {
517 for (let [mode, key] of this.iter_available_modes()) {
518 if (key_event.key == key) {
519 event.preventDefault();
520 tui.switch_mode(mode.name);
532 window_width: terminal.cols / 2,
540 mode_waiting_for_server: new Mode('waiting_for_server',
542 mode_login: new Mode('login', true, false, true),
543 mode_post_login_wait: new Mode('post_login_wait'),
544 mode_chat: new Mode('chat', true),
545 mode_annotate: new Mode('annotate', true, true),
546 mode_play: new Mode('play'),
547 mode_study: new Mode('study', false, true),
548 mode_write: new Mode('write', false, false, false, true),
549 mode_edit: new Mode('edit'),
550 mode_control_pw_type: new Mode('control_pw_type', true),
551 mode_portal: new Mode('portal', true, true),
552 mode_password: new Mode('password', true),
553 mode_admin_enter: new Mode('admin_enter', true),
554 mode_admin: new Mode('admin'),
555 mode_control_pw_pw: new Mode('control_pw_pw', true),
556 mode_control_tile_type: new Mode('control_tile_type', true),
557 mode_control_tile_draw: new Mode('control_tile_draw'),
559 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
560 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
561 this.mode_admin.available_modes = ["control_pw_type",
562 "control_tile_type", "chat",
563 "study", "play", "edit"]
564 this.mode_control_tile_draw.available_modes = ["admin_enter"]
565 this.mode_edit.available_modes = ["write", "annotate", "portal",
566 "password", "chat", "study", "play",
568 this.mode = this.mode_waiting_for_server;
569 this.inputEl = document.getElementById("input");
570 this.inputEl.focus();
571 this.recalc_input_lines();
572 this.height_header = this.height_turn_line + this.height_mode_line;
573 this.log_msg("@ waiting for server connection ...");
576 init_keys: function() {
578 for (let key_selector of key_selectors) {
579 this.keys[key_selector.id.slice(4)] = key_selector.value;
581 if (game.map_geometry == 'Square') {
582 this.movement_keys = {
583 [this.keys.square_move_up]: 'UP',
584 [this.keys.square_move_left]: 'LEFT',
585 [this.keys.square_move_down]: 'DOWN',
586 [this.keys.square_move_right]: 'RIGHT'
588 document.getElementById("move_upright").hidden = true;
589 document.getElementById("move_upleft").hidden = true;
590 document.getElementById("move_downright").hidden = true;
591 document.getElementById("move_downleft").hidden = true;
592 document.getElementById("move_up").hidden = false;
593 document.getElementById("move_down").hidden = false;
594 } else if (game.map_geometry == 'Hex') {
595 document.getElementById("move_upright").hidden = false;
596 document.getElementById("move_upleft").hidden = false;
597 document.getElementById("move_downright").hidden = false;
598 document.getElementById("move_downleft").hidden = false;
599 document.getElementById("move_up").hidden = true;
600 document.getElementById("move_down").hidden = true;
601 this.movement_keys = {
602 [this.keys.hex_move_upleft]: 'UPLEFT',
603 [this.keys.hex_move_upright]: 'UPRIGHT',
604 [this.keys.hex_move_right]: 'RIGHT',
605 [this.keys.hex_move_downright]: 'DOWNRIGHT',
606 [this.keys.hex_move_downleft]: 'DOWNLEFT',
607 [this.keys.hex_move_left]: 'LEFT'
611 switch_mode: function(mode_name) {
612 if (this.mode.name == 'control_tile_draw') {
613 tui.log_msg('@ finished tile protection drawing.')
615 this.tile_draw = false;
616 if (mode_name == 'admin_enter' && this.is_admin) {
619 this.mode = this['mode_' + mode_name];
620 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
621 this.map_mode = 'protections';
622 } else if (this.mode.name != "edit") {
623 this.map_mode = 'terrain + things';
625 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
626 this.inputEl.focus();
628 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
629 explorer.position = game.things[game.player_id].position;
630 if (this.mode.shows_info) {
631 explorer.query_info();
635 this.restore_input_values();
636 for (let el of document.getElementsByTagName("button")) {
639 document.getElementById("help").disabled = false;
640 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
641 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
642 move_key.disabled = false;
645 if (!this.mode.is_intro && this.mode.name != 'play') {
646 document.getElementById("switch_to_play").disabled = false;
648 if (!this.mode.is_intro && this.mode.name != 'study') {
649 document.getElementById("switch_to_study").disabled = false;
651 if (!this.mode.is_intro && this.mode.name != 'chat') {
652 document.getElementById("switch_to_chat").disabled = false;
654 if (!this.mode.is_intro && this.mode.name != 'edit') {
655 document.getElementById("switch_to_edit").disabled = false;
657 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
658 document.getElementById("switch_to_admin_enter").disabled = false;
660 if (this.mode.name == 'login') {
661 if (this.login_name) {
662 server.send(['LOGIN', this.login_name]);
664 this.log_msg("? need login name");
666 } else if (this.mode.name == 'play') {
667 if (game.tasks.includes('PICK_UP')) {
668 document.getElementById("take_thing").disabled = false;
670 if (game.tasks.includes('DROP')) {
671 document.getElementById("drop_thing").disabled = false;
673 if (game.tasks.includes('MOVE')) {
675 document.getElementById("teleport").disabled = false;
676 } else if (this.mode.name == 'edit') {
677 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
678 document.getElementById("flatten").disabled = false;
680 document.getElementById("switch_to_annotate").disabled = false;
681 document.getElementById("switch_to_write").disabled = false;
682 document.getElementById("switch_to_portal").disabled = false;
683 document.getElementById("switch_to_password").disabled = false;
684 document.getElementById("toggle_map_mode").disabled = false;
685 } else if (this.mode.name == 'admin') {
686 document.getElementById("switch_to_control_pw_type").disabled = false;
687 document.getElementById("switch_to_control_tile_type").disabled = false;
688 } else if (this.mode.name == 'study') {
689 document.getElementById("toggle_map_mode").disabled = false;
690 } else if (this.mode.is_single_char_entry) {
691 this.show_help = true;
692 } else if (this.mode.name == 'admin_enter') {
693 this.log_msg('@ enter admin password:')
694 } else if (this.mode.name == 'control_pw_type') {
695 this.log_msg('@ enter tile protection character for which you want to change the password:')
696 } else if (this.mode.name == 'control_tile_type') {
697 this.log_msg('@ enter tile protection character which you want to draw:')
698 } else if (this.mode.name == 'control_pw_pw') {
699 this.log_msg('@ enter tile protection password for "' + this.tile_control_char + '":');
700 } else if (this.mode.name == 'control_tile_draw') {
701 document.getElementById("toggle_tile_draw").disabled = false;
702 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 + '].')
706 offset_links: function(offset, links) {
707 for (let y in links) {
708 let real_y = offset[0] + parseInt(y);
709 if (!this.links[real_y]) {
710 this.links[real_y] = [];
712 for (let link of links[y]) {
713 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
714 this.links[real_y].push(offset_link);
718 restore_input_values: function() {
719 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
720 let info = explorer.info_db[explorer.position];
721 if (info != "(none)") {
722 this.inputEl.value = info;
723 this.recalc_input_lines();
725 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
726 let portal = game.portals[explorer.position]
727 this.inputEl.value = portal;
728 this.recalc_input_lines();
729 } else if (this.mode.name == 'password') {
730 this.inputEl.value = this.password;
731 this.recalc_input_lines();
734 empty_input: function(str) {
735 this.inputEl.value = "";
736 if (this.mode.has_input_prompt) {
737 this.recalc_input_lines();
739 this.height_input = 0;
742 recalc_input_lines: function() {
744 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
745 this.height_input = this.input_lines.length;
747 msg_into_lines_of_width: function(msg, width) {
748 function push_inner_link(y, end_x) {
749 if (!inner_links[y]) {
752 inner_links[y].push([url_start_x, end_x, url]);
754 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
757 for (const match of matches) {
758 const url = match[0];
759 const url_start = match.index;
760 const url_end = match.index + match[0].length;
761 link_data[url_start] = url;
762 url_ends.push(url_end);
766 let inner_links = {};
770 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
771 if (x >= width || msg[i] == "\n") {
773 push_inner_link(y, chunk.length);
779 if (msg[i] == "\n") {
784 if (msg[i] != "\n") {
787 if (i in link_data) {
791 } else if (url_ends.includes(i)) {
792 push_inner_link(y, x);
798 push_inner_link(lines.length - 1, chunk.length);
800 return [lines, inner_links];
802 log_msg: function(msg) {
804 while (this.log.length > 100) {
809 draw_map: function() {
810 let map_lines_split = [];
812 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
813 if (j == game.map_size[1]) {
814 map_lines_split.push(line);
818 if (this.map_mode == 'protections') {
819 line.push(game.map_control[i] + ' ');
821 line.push(game.map[i] + ' ');
824 map_lines_split.push(line);
825 if (this.map_mode == 'terrain + annotations') {
826 for (const coordinate of explorer.info_hints) {
827 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
829 } else if (this.map_mode == 'terrain + things') {
830 for (const p in game.portals) {
831 let coordinate = p.split(',')
832 let original = map_lines_split[coordinate[0]][coordinate[1]];
833 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
835 let used_positions = [];
836 for (const thing_id in game.things) {
837 let t = game.things[thing_id];
838 let symbol = game.thing_types[t.type_];
841 meta_char = t.player_char;
843 if (used_positions.includes(t.position.toString())) {
846 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
847 used_positions.push(t.position.toString());
850 let player = game.things[game.player_id];
851 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
852 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
853 } else if (tui.map_mode != 'terrain + things') {
854 map_lines_split[player.position[0]][player.position[1]] = '??';
857 if (game.map_geometry == 'Square') {
858 for (let line_split of map_lines_split) {
859 map_lines.push(line_split.join(''));
861 } else if (game.map_geometry == 'Hex') {
863 for (let line_split of map_lines_split) {
864 map_lines.push(' '.repeat(indent) + line_split.join(''));
872 let window_center = [terminal.rows / 2, this.window_width / 2];
873 let center_position = [player.position[0], player.position[1]];
874 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
875 center_position = [explorer.position[0], explorer.position[1]];
877 center_position[1] = center_position[1] * 2;
878 let offset = [center_position[0] - window_center[0],
879 center_position[1] - window_center[1]]
880 if (game.map_geometry == 'Hex' && offset[0] % 2) {
883 let term_y = Math.max(0, -offset[0]);
884 let term_x = Math.max(0, -offset[1]);
885 let map_y = Math.max(0, offset[0]);
886 let map_x = Math.max(0, offset[1]);
887 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
888 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
889 terminal.write(term_y, term_x, to_draw);
892 draw_mode_line: function() {
893 let help = 'hit [' + this.keys.help + '] for help';
894 if (this.mode.has_input_prompt) {
895 help = 'enter /help for help';
897 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
899 draw_turn_line: function(n) {
900 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
902 draw_history: function() {
903 let log_display_lines = [];
905 let y_offset_in_log = 0;
906 for (let line of this.log) {
907 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
909 log_display_lines = log_display_lines.concat(new_lines);
910 for (const y in link_data) {
911 const rel_y = y_offset_in_log + parseInt(y);
912 log_links[rel_y] = [];
913 for (let link of link_data[y]) {
914 log_links[rel_y].push(link);
917 y_offset_in_log += new_lines.length;
919 let i = log_display_lines.length - 1;
920 for (let y = terminal.rows - 1 - this.height_input;
921 y >= this.height_header && i >= 0;
923 terminal.write(y, this.window_width, log_display_lines[i]);
925 for (const key of Object.keys(log_links)) {
926 if (parseInt(key) <= i) {
927 delete log_links[key];
930 let offset = [terminal.rows - this.height_input - log_display_lines.length,
932 this.offset_links(offset, log_links);
934 draw_info: function() {
935 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
937 let offset = [this.height_header, this.window_width];
938 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
939 terminal.write(y, offset[1], lines[i]);
941 this.offset_links(offset, link_data);
943 draw_input: function() {
944 if (this.mode.has_input_prompt) {
945 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
946 terminal.write(y, this.window_width, this.input_lines[i]);
950 draw_help: function() {
951 let movement_keys_desc = '';
952 if (!this.mode.is_intro) {
953 movement_keys_desc = Object.keys(this.movement_keys).join(',');
955 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
956 if (this.mode.name == 'play') {
957 content += "Available actions:\n";
958 if (game.tasks.includes('MOVE')) {
959 content += "[" + movement_keys_desc + "] – move player\n";
961 if (game.tasks.includes('PICK_UP')) {
962 content += "[" + this.keys.take_thing + "] – pick up thing\n";
964 if (game.tasks.includes('DROP')) {
965 content += "[" + this.keys.drop_thing + "] – drop thing\n";
967 content += "[" + tui.keys.teleport + "] – teleport\n";
969 } else if (this.mode.name == 'study') {
970 content += "Available actions:\n";
971 content += '[' + movement_keys_desc + '] – move question mark\n';
972 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
974 } else if (this.mode.name == 'edit') {
975 content += "Available actions:\n";
976 if (game.tasks.includes('MOVE')) {
977 content += "[" + movement_keys_desc + "] – move player\n";
979 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
980 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
982 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
984 } else if (this.mode.name == 'control_tile_draw') {
985 content += "Available actions:\n";
986 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
988 } else if (this.mode.name == 'chat') {
989 content += '/nick NAME – re-name yourself to NAME\n';
990 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
991 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
992 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
993 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
995 content += this.mode.list_available_modes();
997 if (!this.mode.has_input_prompt) {
998 start_x = this.window_width
1000 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1001 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1002 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1003 terminal.write(y, start_x, lines[i]);
1006 toggle_tile_draw: function() {
1007 if (tui.tile_draw) {
1008 tui.tile_draw = false;
1010 tui.tile_draw = true;
1013 toggle_map_mode: function() {
1014 if (tui.map_mode == 'terrain only') {
1015 tui.map_mode = 'terrain + annotations';
1016 } else if (tui.map_mode == 'terrain + annotations') {
1017 tui.map_mode = 'terrain + things';
1018 } else if (tui.map_mode == 'terrain + things') {
1019 tui.map_mode = 'protections';
1020 } else if (tui.map_mode == 'protections') {
1021 tui.map_mode = 'terrain only';
1024 full_refresh: function() {
1026 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1027 if (this.mode.is_intro) {
1028 this.draw_history();
1031 if (game.turn_complete) {
1033 this.draw_turn_line();
1035 this.draw_mode_line();
1036 if (this.mode.shows_info) {
1039 this.draw_history();
1043 if (this.show_help) {
1055 this.map_control = "";
1056 this.map_size = [0,0];
1057 this.player_id = -1;
1061 get_thing: function(id_, create_if_not_found=false) {
1062 if (id_ in game.things) {
1063 return game.things[id_];
1064 } else if (create_if_not_found) {
1065 let t = new Thing([0,0]);
1066 game.things[id_] = t;
1070 move: function(start_position, direction) {
1071 let target = [start_position[0], start_position[1]];
1072 if (direction == 'LEFT') {
1074 } else if (direction == 'RIGHT') {
1076 } else if (game.map_geometry == 'Square') {
1077 if (direction == 'UP') {
1079 } else if (direction == 'DOWN') {
1082 } else if (game.map_geometry == 'Hex') {
1083 let start_indented = start_position[0] % 2;
1084 if (direction == 'UPLEFT') {
1086 if (!start_indented) {
1089 } else if (direction == 'UPRIGHT') {
1091 if (start_indented) {
1094 } else if (direction == 'DOWNLEFT') {
1096 if (!start_indented) {
1099 } else if (direction == 'DOWNRIGHT') {
1101 if (start_indented) {
1106 if (target[0] < 0 || target[1] < 0 ||
1107 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1112 teleport: function() {
1113 let player = this.get_thing(game.player_id);
1114 if (player.position in this.portals) {
1115 server.reconnect_to(this.portals[player.position]);
1117 terminal.blink_screen();
1118 tui.log_msg('? not standing on portal')
1126 server.init(websocket_location);
1132 move: function(direction) {
1133 let target = game.move(this.position, direction);
1135 this.position = target
1136 if (tui.mode.shows_info) {
1138 } else if (tui.tile_draw) {
1139 this.send_tile_control_command();
1142 terminal.blink_screen();
1145 update_info_db: function(yx, str) {
1146 this.info_db[yx] = str;
1147 if (tui.mode.name == 'study') {
1151 empty_info_db: function() {
1153 this.info_hints = [];
1154 if (tui.mode.name == 'study') {
1158 query_info: function() {
1159 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1161 get_info: function() {
1162 let info = "MAP VIEW: " + tui.map_mode + "\n";
1163 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1164 if (game.fov[position_i] != '.') {
1165 return info + 'outside field of view';
1167 let terrain_char = game.map[position_i]
1168 let terrain_desc = '?'
1169 if (game.terrains[terrain_char]) {
1170 terrain_desc = game.terrains[terrain_char];
1172 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1173 let protection = game.map_control[position_i];
1174 if (protection == '.') {
1175 protection = 'unprotected';
1177 info += 'PROTECTION: ' + protection + '\n';
1178 for (let t_id in game.things) {
1179 let t = game.things[t_id];
1180 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1181 let symbol = game.thing_types[t.type_];
1182 info += "THING: " + t.type_ + " / " + symbol;
1183 if (t.player_char) {
1184 info += t.player_char;
1187 info += " (" + t.name_ + ")";
1192 if (this.position in game.portals) {
1193 info += "PORTAL: " + game.portals[this.position] + "\n";
1195 if (this.position in this.info_db) {
1196 info += "ANNOTATIONS: " + this.info_db[this.position];
1198 info += 'waiting …';
1202 annotate: function(msg) {
1203 if (msg.length == 0) {
1204 msg = " "; // triggers annotation deletion
1206 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1208 set_portal: function(msg) {
1209 if (msg.length == 0) {
1210 msg = " "; // triggers portal deletion
1212 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1214 send_tile_control_command: function() {
1215 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1219 tui.inputEl.addEventListener('input', (event) => {
1220 if (tui.mode.has_input_prompt) {
1221 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1222 if (tui.inputEl.value.length > max_length) {
1223 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1225 tui.recalc_input_lines();
1226 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1227 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1228 tui.switch_mode('edit');
1232 document.onclick = function() {
1233 tui.show_help = false;
1235 tui.inputEl.addEventListener('keydown', (event) => {
1236 tui.show_help = false;
1237 if (event.key == 'Enter') {
1238 event.preventDefault();
1240 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1241 tui.show_help = true;
1243 tui.restore_input_values();
1244 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1245 && !tui.mode.is_single_char_entry) {
1246 tui.show_help = true;
1247 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1248 tui.login_name = tui.inputEl.value;
1249 server.send(['LOGIN', tui.inputEl.value]);
1251 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1252 if (tui.inputEl.value.length == 0) {
1253 tui.log_msg('@ aborted');
1255 server.send(['SET_MAP_CONTROL_PASSWORD',
1256 tui.tile_control_char, tui.inputEl.value]);
1257 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1259 tui.switch_mode('admin');
1260 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1261 explorer.set_portal(tui.inputEl.value);
1262 tui.switch_mode('edit');
1263 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1264 explorer.annotate(tui.inputEl.value);
1265 tui.switch_mode('edit');
1266 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1267 if (tui.inputEl.value.length == 0) {
1268 tui.inputEl.value = " ";
1270 tui.password = tui.inputEl.value
1271 tui.switch_mode('edit');
1272 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1273 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1274 tui.switch_mode('play');
1275 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1276 if (tui.inputEl.value.length != 1) {
1277 tui.log_msg('@ entered non-single-char, therefore aborted');
1278 tui.switch_mode('admin');
1280 tui.tile_control_char = tui.inputEl.value[0];
1281 tui.switch_mode('control_pw_pw');
1283 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1284 if (tui.inputEl.value.length != 1) {
1285 tui.log_msg('@ entered non-single-char, therefore aborted');
1286 tui.switch_mode('admin');
1288 tui.tile_control_char = tui.inputEl.value[0];
1289 tui.switch_mode('control_tile_draw');
1291 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1292 let tokens = parser.tokenize(tui.inputEl.value);
1293 if (tokens.length > 0 && tokens[0].length > 0) {
1294 if (tui.inputEl.value[0][0] == '/') {
1295 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1296 tui.switch_mode('play');
1297 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1298 tui.switch_mode('study');
1299 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1300 tui.switch_mode('edit');
1301 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1302 tui.switch_mode('admin_enter');
1303 } else if (tokens[0].slice(1) == 'nick') {
1304 if (tokens.length > 1) {
1305 server.send(['NICK', tokens[1]]);
1307 tui.log_msg('? need new name');
1310 tui.log_msg('? unknown command');
1313 server.send(['ALL', tui.inputEl.value]);
1315 } else if (tui.inputEl.valuelength > 0) {
1316 server.send(['ALL', tui.inputEl.value]);
1319 } else if (tui.mode.name == 'play') {
1320 if (tui.mode.mode_switch_on_key(event)) {
1322 } else if (event.key === tui.keys.take_thing
1323 && game.tasks.includes('PICK_UP')) {
1324 server.send(["TASK:PICK_UP"]);
1325 } else if (event.key === tui.keys.drop_thing
1326 && game.tasks.includes('DROP')) {
1327 server.send(["TASK:DROP"]);
1328 } else if (event.key in tui.movement_keys
1329 && game.tasks.includes('MOVE')) {
1330 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1331 } else if (event.key === tui.keys.teleport) {
1334 } else if (tui.mode.name == 'study') {
1335 if (tui.mode.mode_switch_on_key(event)) {
1337 } else if (event.key in tui.movement_keys) {
1338 explorer.move(tui.movement_keys[event.key]);
1339 } else if (event.key == tui.keys.toggle_map_mode) {
1340 tui.toggle_map_mode();
1342 } else if (tui.mode.name == 'control_tile_draw') {
1343 if (tui.mode.mode_switch_on_key(event)) {
1345 } else if (event.key in tui.movement_keys) {
1346 explorer.move(tui.movement_keys[event.key]);
1347 } else if (event.key === tui.keys.toggle_tile_draw) {
1348 tui.toggle_tile_draw();
1350 } else if (tui.mode.name == 'admin') {
1351 if (tui.mode.mode_switch_on_key(event)) {
1354 } else if (tui.mode.name == 'edit') {
1355 if (tui.mode.mode_switch_on_key(event)) {
1357 } else if (event.key in tui.movement_keys
1358 && game.tasks.includes('MOVE')) {
1359 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1360 } else if (event.key === tui.keys.flatten
1361 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1362 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1363 } else if (event.key == tui.keys.toggle_map_mode) {
1364 tui.toggle_map_mode();
1370 rows_selector.addEventListener('input', function() {
1371 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1374 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1375 terminal.initialize();
1378 cols_selector.addEventListener('input', function() {
1379 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1382 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1383 terminal.initialize();
1384 tui.window_width = terminal.cols / 2,
1387 for (let key_selector of key_selectors) {
1388 key_selector.addEventListener('input', function() {
1389 window.localStorage.setItem(key_selector.id, key_selector.value);
1393 window.setInterval(function() {
1394 if (server.connected) {
1395 server.send(['PING']);
1397 server.reconnect_to(server.url);
1398 tui.log_msg('@ attempting reconnect …')
1401 window.setInterval(function() {
1403 if (document.activeElement == tui.inputEl) {
1404 val = "on (click outside terminal to change)";
1406 val = "off (click into terminal to change)";
1408 document.getElementById("keyboard_control").textContent = val;
1410 document.getElementById("terminal").onclick = function() {
1411 tui.inputEl.focus();
1413 document.getElementById("help").onclick = function() {
1414 tui.show_help = true;
1417 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1418 const mode = switchEl.id.slice("switch_to_".length);
1419 switchEl.onclick = function() {
1420 tui.switch_mode(mode);
1424 document.getElementById("toggle_tile_draw").onclick = function() {
1425 tui.toggle_tile_draw();
1427 document.getElementById("toggle_map_mode").onclick = function() {
1428 tui.toggle_map_mode();
1431 document.getElementById("take_thing").onclick = function() {
1432 server.send(['TASK:PICK_UP']);
1434 document.getElementById("drop_thing").onclick = function() {
1435 server.send(['TASK:DROP']);
1437 document.getElementById("flatten").onclick = function() {
1438 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1440 document.getElementById("teleport").onclick = function() {
1443 document.getElementById("move_upleft").onclick = function() {
1444 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1445 server.send(['TASK:MOVE', 'UPLEFT']);
1447 explorer.move('UPLEFT');
1450 document.getElementById("move_left").onclick = function() {
1451 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1452 server.send(['TASK:MOVE', 'LEFT']);
1454 explorer.move('LEFT');
1457 document.getElementById("move_downleft").onclick = function() {
1458 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1459 server.send(['TASK:MOVE', 'DOWNLEFT']);
1461 explorer.move('DOWNLEFT');
1464 document.getElementById("move_down").onclick = function() {
1465 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1466 server.send(['TASK:MOVE', 'DOWN']);
1468 explorer.move('DOWN');
1471 document.getElementById("move_up").onclick = function() {
1472 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1473 server.send(['TASK:MOVE', 'UP']);
1475 explorer.move('UP');
1478 document.getElementById("move_upright").onclick = function() {
1479 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1480 server.send(['TASK:MOVE', 'UPRIGHT']);
1482 explorer.move('UPRIGHT');
1485 document.getElementById("move_right").onclick = function() {
1486 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1487 server.send(['TASK:MOVE', 'RIGHT']);
1489 explorer.move('RIGHT');
1492 document.getElementById("move_downright").onclick = function() {
1493 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1494 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1496 explorer.move('DOWNRIGHT');