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 />
16 <pre id="terminal"></pre>
17 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
19 <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal / curses client)
20 <h3>button controls for mouse players</h3>
21 <table style="float: left">
23 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
24 <td style="text-align: center"><button id="move_up">up</button></td>
25 <td><button id="move_upright">up-right</button></td>
28 <td style="text-align: right;"><button id="move_left">left</button></td>
29 <td stlye="text-align: center;">move</td>
30 <td><button id="move_right">right</button></td>
33 <td><button id="move_downleft">down-left</button></td>
34 <td style="text-align: center"><button id="move_down">down</button></td>
35 <td><button id="move_downright">down-right</button></td>
40 <td><button id="help">help</button></td>
43 <td><button id="switch_to_chat">chat mode</button><br /></td>
46 <td><button id="switch_to_study">study mode</button></td>
47 <td><button id="toggle_map_mode">toggle map view</button>
50 <td><button id="switch_to_play">play mode</button></td>
52 <button id="take_thing">pick up thing</button>
53 <button id="drop_thing">drop thing</button>
54 <button id="teleport">teleport</button>
58 <td><button id="switch_to_edit">map edit mode</button></td>
60 <button id="switch_to_write">change terrain</button>
61 <button id="flatten">flatten surroundings</button>
62 <button id="switch_to_annotate">annotate tile</button>
63 <button id="switch_to_portal">edit portal</button>
64 <button id="switch_to_password">enter map edit password</button>
68 <td><button id="switch_to_admin_enter">admin mode</button></td>
70 <button id="switch_to_control_pw_type">change protection character password</button>
71 <button id="switch_to_control_tile_type">change protection areas</button>
72 <button id="toggle_tile_draw">toggle protection character drawing</button>
77 <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 />
79 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
80 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
81 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
82 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
83 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
84 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
85 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
86 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
87 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
88 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
89 <li>help: <input id="key_help" type="text" value="h" />
90 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
91 <li>teleport: <input id="key_teleport" type="text" value="p" />
92 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
93 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
94 <li><input id="key_switch_to_chat" type="text" value="t" />
95 <li><input id="key_switch_to_play" type="text" value="p" />
96 <li><input id="key_switch_to_study" type="text" value="?" />
97 <li><input id="key_switch_to_edit" type="text" value="E" />
98 <li><input id="key_switch_to_write" type="text" value="m" />
99 <li><input id="key_switch_to_password" type="text" value="P" />
100 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
101 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
102 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
103 <li><input id="key_switch_to_annotate" type="text" value="M" />
104 <li><input id="key_switch_to_portal" type="text" value="T" />
105 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="M" />
106 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
111 let websocket_location = "wss://plomlompom.com/rogue_chat/";
112 //let websocket_location = "ws://localhost:8000/";
117 'long': 'This mode allows you to interact with the map in various ways.'
121 '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.'},
124 'long': 'This mode allows you to change the map in various ways. Individual map tiles are shown together with their "protection characters". 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.'
127 'short': 'change terrain',
128 '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.'
131 'short': 'change protection character password',
132 '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.'
135 'short': 'change tiles control password',
136 '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.'
138 'control_tile_type': {
139 'short': 'change tiles control',
140 '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.'
142 'control_tile_draw': {
143 'short': 'change tiles control',
144 '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.'
147 'short': 'annotate tile',
148 '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.'
151 'short': 'edit portal',
152 '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.'
156 '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:'
160 'long': 'Enter your player name.'
162 'waiting_for_server': {
163 'short': 'waiting for server response',
164 'long': 'Waiting for a server response.'
167 'short': 'waiting for server response',
168 'long': 'Waiting for a server response.'
171 'short': 'set map edit password',
172 '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.'
175 'short': 'become admin',
176 'long': 'This mode allows you to become admin if you know an admin password.'
180 'long': 'This mode allows you access to actions limited to administrators.'
184 let rows_selector = document.getElementById("n_rows");
185 let cols_selector = document.getElementById("n_cols");
186 let key_selectors = document.querySelectorAll('[id^="key_"]');
188 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
189 const action = key_switch_selector.id.slice("key_switch_to_".length);
190 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
193 function restore_selector_value(selector) {
194 let stored_selection = window.localStorage.getItem(selector.id);
195 if (stored_selection) {
196 selector.value = stored_selection;
199 restore_selector_value(rows_selector);
200 restore_selector_value(cols_selector);
201 for (let key_selector of key_selectors) {
202 restore_selector_value(key_selector);
208 initialize: function() {
209 this.rows = rows_selector.value;
210 this.cols = cols_selector.value;
211 this.pre_el = document.getElementById("terminal");
212 this.pre_el.style.color = this.foreground;
213 this.pre_el.style.backgroundColor = this.background;
216 for (let y = 0, x = 0; y <= this.rows; x++) {
217 if (x == this.cols) {
220 this.content.push(line);
222 if (y == this.rows) {
229 blink_screen: function() {
230 this.pre_el.style.color = this.background;
231 this.pre_el.style.backgroundColor = this.foreground;
233 this.pre_el.style.color = this.foreground;
234 this.pre_el.style.backgroundColor = this.background;
237 refresh: function() {
238 function escapeHTML(str) {
240 replace(/&/g, '&').
241 replace(/</g, '<').
242 replace(/>/g, '>').
243 replace(/'/g, ''').
244 replace(/"/g, '"');
246 let pre_content = '';
247 for (let y = 0; y < this.rows; y++) {
248 let line = this.content[y].join('');
250 if (y in tui.links) {
252 for (let span of tui.links[y]) {
253 chunks.push(escapeHTML(line.slice(start_x, span[0])));
254 chunks.push('<a href="');
255 chunks.push(escapeHTML(span[2]));
257 chunks.push(escapeHTML(line.slice(span[0], span[1])));
261 chunks.push(escapeHTML(line.slice(start_x)));
263 chunks = [escapeHTML(line)];
265 for (const chunk of chunks) {
266 pre_content += chunk;
270 this.pre_el.innerHTML = pre_content;
272 write: function(start_y, start_x, msg) {
273 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
274 this.content[start_y][x] = msg[i];
277 drawBox: function(start_y, start_x, height, width) {
278 let end_y = start_y + height;
279 let end_x = start_x + width;
280 for (let y = start_y, x = start_x; y < this.rows; x++) {
288 this.content[y][x] = ' ';
292 terminal.initialize();
295 tokenize: function(str) {
300 for (let i = 0; i < str.length; i++) {
306 } else if (c == '\\') {
308 } else if (c == '"') {
313 } else if (c == '"') {
315 } else if (c === ' ') {
316 if (token.length > 0) {
324 if (token.length > 0) {
329 parse_yx: function(position_string) {
330 let coordinate_strings = position_string.split(',')
331 let position = [0, 0];
332 position[0] = parseInt(coordinate_strings[0].slice(2));
333 position[1] = parseInt(coordinate_strings[1].slice(2));
345 init: function(url) {
347 this.websocket = new WebSocket(this.url);
348 this.websocket.onopen = function(event) {
349 server.connected = true;
350 game.thing_types = {};
352 server.send(['TASKS']);
353 server.send(['TERRAINS']);
354 server.send(['THING_TYPES']);
355 tui.log_msg("@ server connected! :)");
356 tui.switch_mode('login');
358 this.websocket.onclose = function(event) {
359 server.connected = false;
360 tui.switch_mode('waiting_for_server');
361 tui.log_msg("@ server disconnected :(");
363 this.websocket.onmessage = this.handle_event;
365 reconnect_to: function(url) {
366 this.websocket.close();
369 send: function(tokens) {
370 this.websocket.send(unparser.untokenize(tokens));
372 handle_event: function(event) {
373 let tokens = parser.tokenize(event.data);
374 if (tokens[0] === 'TURN') {
375 game.turn_complete = false;
376 explorer.empty_info_db();
379 game.turn = parseInt(tokens[1]);
380 } else if (tokens[0] === 'THING') {
381 let t = game.get_thing(tokens[3], true);
382 t.position = parser.parse_yx(tokens[1]);
384 } else if (tokens[0] === 'THING_NAME') {
385 let t = game.get_thing(tokens[1], false);
389 } else if (tokens[0] === 'THING_CHAR') {
390 let t = game.get_thing(tokens[1], false);
392 t.player_char = tokens[2];
394 } else if (tokens[0] === 'TASKS') {
395 game.tasks = tokens[1].split(',');
396 tui.mode_write.legal = game.tasks.includes('WRITE');
397 } else if (tokens[0] === 'THING_TYPE') {
398 game.thing_types[tokens[1]] = tokens[2]
399 } else if (tokens[0] === 'TERRAIN') {
400 game.terrains[tokens[1]] = tokens[2]
401 } else if (tokens[0] === 'MAP') {
402 game.map_geometry = tokens[1];
404 game.map_size = parser.parse_yx(tokens[2]);
406 } else if (tokens[0] === 'FOV') {
408 } else if (tokens[0] === 'MAP_CONTROL') {
409 game.map_control = tokens[1]
410 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
411 game.turn_complete = true;
412 if (tui.mode.name == 'post_login_wait') {
413 tui.switch_mode('play');
414 } else if (tui.mode.name == 'study') {
415 explorer.query_info();
418 } else if (tokens[0] === 'CHAT') {
419 tui.log_msg('# ' + tokens[1], 1);
420 } else if (tokens[0] === 'PLAYER_ID') {
421 game.player_id = parseInt(tokens[1]);
422 } else if (tokens[0] === 'LOGIN_OK') {
423 this.send(['GET_GAMESTATE']);
424 tui.switch_mode('post_login_wait');
425 } else if (tokens[0] === 'ADMIN_OK') {
427 tui.log_msg('@ you now have admin rights');
428 tui.switch_mode('admin');
429 } else if (tokens[0] === 'PORTAL') {
430 let position = parser.parse_yx(tokens[1]);
431 game.portals[position] = tokens[2];
432 } else if (tokens[0] === 'ANNOTATION_HINT') {
433 let position = parser.parse_yx(tokens[1]);
434 explorer.info_hints = explorer.info_hints.concat([position]);
435 } else if (tokens[0] === 'ANNOTATION') {
436 let position = parser.parse_yx(tokens[1]);
437 explorer.update_info_db(position, tokens[2]);
438 tui.restore_input_values();
440 } else if (tokens[0] === 'UNHANDLED_INPUT') {
441 tui.log_msg('? unknown command');
442 } else if (tokens[0] === 'PLAY_ERROR') {
443 tui.log_msg('? ' + tokens[1]);
444 terminal.blink_screen();
445 } else if (tokens[0] === 'ARGUMENT_ERROR') {
446 tui.log_msg('? syntax error: ' + tokens[1]);
447 } else if (tokens[0] === 'GAME_ERROR') {
448 tui.log_msg('? game error: ' + tokens[1]);
449 } else if (tokens[0] === 'PONG') {
452 tui.log_msg('? unhandled input: ' + event.data);
458 quote: function(str) {
460 for (let i = 0; i < str.length; i++) {
462 if (['"', '\\'].includes(c)) {
468 return quoted.join('');
470 to_yx: function(yx_coordinate) {
471 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
473 untokenize: function(tokens) {
474 let quoted_tokens = [];
475 for (let token of tokens) {
476 quoted_tokens.push(this.quote(token));
478 return quoted_tokens.join(" ");
483 constructor(name, has_input_prompt=false, shows_info=false,
484 is_intro=false, is_single_char_entry=false) {
486 this.short_desc = mode_helps[name].short;
487 this.available_modes = [];
488 this.has_input_prompt = has_input_prompt;
489 this.shows_info= shows_info;
490 this.is_intro = is_intro;
491 this.help_intro = mode_helps[name].long;
492 this.is_single_char_entry = is_single_char_entry;
495 *iter_available_modes() {
496 for (let mode_name of this.available_modes) {
497 let mode = tui['mode_' + mode_name];
501 let key = tui.keys['switch_to_' + mode.name];
505 list_available_modes() {
507 if (this.available_modes.length > 0) {
508 msg += 'Other modes available from here:\n';
509 for (let [mode, key] of this.iter_available_modes()) {
510 msg += '[' + key + '] – ' + mode.short_desc + '\n';
515 mode_switch_on_key(key_event) {
516 for (let [mode, key] of this.iter_available_modes()) {
517 if (key_event.key == key) {
518 event.preventDefault();
519 tui.switch_mode(mode.name);
531 window_width: terminal.cols / 2,
539 mode_waiting_for_server: new Mode('waiting_for_server',
541 mode_login: new Mode('login', true, false, true),
542 mode_post_login_wait: new Mode('post_login_wait'),
543 mode_chat: new Mode('chat', true),
544 mode_annotate: new Mode('annotate', true, true),
545 mode_play: new Mode('play'),
546 mode_study: new Mode('study', false, true),
547 mode_write: new Mode('write', false, false, false, true),
548 mode_edit: new Mode('edit'),
549 mode_control_pw_type: new Mode('control_pw_type', true),
550 mode_portal: new Mode('portal', true, true),
551 mode_password: new Mode('password', true),
552 mode_admin_enter: new Mode('admin_enter', true),
553 mode_admin: new Mode('admin'),
554 mode_control_pw_pw: new Mode('control_pw_pw', true),
555 mode_control_tile_type: new Mode('control_tile_type', true),
556 mode_control_tile_draw: new Mode('control_tile_draw'),
558 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
559 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
560 this.mode_admin.available_modes = ["control_pw_type",
561 "control_tile_type", "chat",
562 "study", "play", "edit"]
563 this.mode_control_tile_draw.available_modes = ["admin_enter"]
564 this.mode_edit.available_modes = ["write", "annotate", "portal",
565 "password", "chat", "study", "play",
567 this.mode = this.mode_waiting_for_server;
568 this.inputEl = document.getElementById("input");
569 this.inputEl.focus();
570 this.recalc_input_lines();
571 this.height_header = this.height_turn_line + this.height_mode_line;
572 this.log_msg("@ waiting for server connection ...");
575 init_keys: function() {
577 for (let key_selector of key_selectors) {
578 this.keys[key_selector.id.slice(4)] = key_selector.value;
580 if (game.map_geometry == 'Square') {
581 this.movement_keys = {
582 [this.keys.square_move_up]: 'UP',
583 [this.keys.square_move_left]: 'LEFT',
584 [this.keys.square_move_down]: 'DOWN',
585 [this.keys.square_move_right]: 'RIGHT'
587 document.getElementById("move_upright").hidden = true;
588 document.getElementById("move_upleft").hidden = true;
589 document.getElementById("move_downright").hidden = true;
590 document.getElementById("move_downleft").hidden = true;
591 document.getElementById("move_up").hidden = false;
592 document.getElementById("move_down").hidden = false;
593 } else if (game.map_geometry == 'Hex') {
594 document.getElementById("move_upright").hidden = false;
595 document.getElementById("move_upleft").hidden = false;
596 document.getElementById("move_downright").hidden = false;
597 document.getElementById("move_downleft").hidden = false;
598 document.getElementById("move_up").hidden = true;
599 document.getElementById("move_down").hidden = true;
600 this.movement_keys = {
601 [this.keys.hex_move_upleft]: 'UPLEFT',
602 [this.keys.hex_move_upright]: 'UPRIGHT',
603 [this.keys.hex_move_right]: 'RIGHT',
604 [this.keys.hex_move_downright]: 'DOWNRIGHT',
605 [this.keys.hex_move_downleft]: 'DOWNLEFT',
606 [this.keys.hex_move_left]: 'LEFT'
610 switch_mode: function(mode_name) {
611 this.inputEl.focus();
612 this.map_mode = 'terrain + things';
613 this.tile_draw = false;
614 if (mode_name == 'admin_enter' && this.is_admin) {
617 this.mode = this['mode_' + mode_name];
618 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
619 explorer.position = game.things[game.player_id].position;
620 if (this.mode.shows_info) {
621 explorer.query_info();
625 this.restore_input_values();
626 for (let el of document.getElementsByTagName("button")) {
629 document.getElementById("help").disabled = false;
630 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
631 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
632 move_key.disabled = false;
635 if (!this.mode.is_intro && this.mode.name != 'play') {
636 document.getElementById("switch_to_play").disabled = false;
638 if (!this.mode.is_intro && this.mode.name != 'study') {
639 document.getElementById("switch_to_study").disabled = false;
641 if (!this.mode.is_intro && this.mode.name != 'chat') {
642 document.getElementById("switch_to_chat").disabled = false;
644 if (!this.mode.is_intro && this.mode.name != 'edit') {
645 document.getElementById("switch_to_edit").disabled = false;
647 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
648 document.getElementById("switch_to_admin_enter").disabled = false;
650 if (this.mode.name == 'login') {
651 if (this.login_name) {
652 server.send(['LOGIN', this.login_name]);
654 this.log_msg("? need login name");
656 } else if (this.mode.name == 'play') {
657 if (game.tasks.includes('PICK_UP')) {
658 document.getElementById("take_thing").disabled = false;
660 if (game.tasks.includes('DROP')) {
661 document.getElementById("drop_thing").disabled = false;
663 if (game.tasks.includes('MOVE')) {
665 document.getElementById("teleport").disabled = false;
666 } else if (this.mode.name == 'edit') {
667 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
668 document.getElementById("flatten").disabled = false;
670 document.getElementById("switch_to_annotate").disabled = false;
671 document.getElementById("switch_to_write").disabled = false;
672 document.getElementById("switch_to_portal").disabled = false;
673 document.getElementById("switch_to_password").disabled = false;
674 } else if (this.mode.name == 'admin') {
675 document.getElementById("switch_to_control_pw_type").disabled = false;
676 document.getElementById("switch_to_control_tile_type").disabled = false;
677 } else if (this.mode.name == 'study') {
678 document.getElementById("toggle_map_mode").disabled = false;
679 } else if (this.mode.is_single_char_entry) {
680 this.show_help = true;
681 } else if (this.mode.name == 'admin_enter') {
682 this.log_msg('@ enter admin password:')
683 } else if (this.mode.name == 'control_pw_type') {
684 this.log_msg('@ enter tile control character for which you want to change the password:')
685 } else if (this.mode.name == 'control_tile_type') {
686 this.log_msg('@ enter tile control character which you want to draw:')
687 } else if (this.mode.name == 'control_pw_pw') {
688 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
689 } else if (this.mode.name == 'control_tile_draw') {
690 document.getElementById("toggle_tile_draw").disabled = false;
691 this.log_msg('@ can draw tile control character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
695 offset_links: function(offset, links) {
696 for (let y in links) {
697 let real_y = offset[0] + parseInt(y);
698 if (!this.links[real_y]) {
699 this.links[real_y] = [];
701 for (let link of links[y]) {
702 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
703 this.links[real_y].push(offset_link);
707 restore_input_values: function() {
708 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
709 let info = explorer.info_db[explorer.position];
710 if (info != "(none)") {
711 this.inputEl.value = info;
712 this.recalc_input_lines();
714 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
715 let portal = game.portals[explorer.position]
716 this.inputEl.value = portal;
717 this.recalc_input_lines();
718 } else if (this.mode.name == 'password') {
719 this.inputEl.value = this.password;
720 this.recalc_input_lines();
723 empty_input: function(str) {
724 this.inputEl.value = "";
725 if (this.mode.has_input_prompt) {
726 this.recalc_input_lines();
728 this.height_input = 0;
731 recalc_input_lines: function() {
733 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
734 this.height_input = this.input_lines.length;
736 msg_into_lines_of_width: function(msg, width) {
737 function push_inner_link(y, end_x) {
738 if (!inner_links[y]) {
741 inner_links[y].push([url_start_x, end_x, url]);
743 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
746 for (const match of matches) {
747 const url = match[0];
748 const url_start = match.index;
749 const url_end = match.index + match[0].length;
750 link_data[url_start] = url;
751 url_ends.push(url_end);
755 let inner_links = {};
759 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
760 if (x >= width || msg[i] == "\n") {
762 push_inner_link(y, chunk.length);
768 if (msg[i] == "\n") {
773 if (msg[i] != "\n") {
776 if (i in link_data) {
780 } else if (url_ends.includes(i)) {
781 push_inner_link(y, x);
787 push_inner_link(lines.length - 1, chunk.length);
789 return [lines, inner_links];
791 log_msg: function(msg) {
793 while (this.log.length > 100) {
798 draw_map: function() {
799 let map_lines_split = [];
801 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
802 if (j == game.map_size[1]) {
803 map_lines_split.push(line);
807 if (['edit', 'write', 'control_tile_draw',
808 'control_tile_type'].includes(this.mode.name)) {
809 line.push(game.map[i] + game.map_control[i]);
811 line.push(game.map[i] + ' ');
814 map_lines_split.push(line);
815 if (this.map_mode == 'terrain + annotations') {
816 for (const coordinate of explorer.info_hints) {
817 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
819 } else if (this.map_mode == 'terrain + things') {
820 for (const p in game.portals) {
821 let coordinate = p.split(',')
822 let original = map_lines_split[coordinate[0]][coordinate[1]];
823 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
825 let used_positions = [];
826 for (const thing_id in game.things) {
827 let t = game.things[thing_id];
828 let symbol = game.thing_types[t.type_];
831 meta_char = t.player_char;
833 if (used_positions.includes(t.position.toString())) {
836 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
837 used_positions.push(t.position.toString());
840 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
841 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
844 if (game.map_geometry == 'Square') {
845 for (let line_split of map_lines_split) {
846 map_lines.push(line_split.join(''));
848 } else if (game.map_geometry == 'Hex') {
850 for (let line_split of map_lines_split) {
851 map_lines.push(' '.repeat(indent) + line_split.join(''));
859 let window_center = [terminal.rows / 2, this.window_width / 2];
860 let player = game.things[game.player_id];
861 let center_position = [player.position[0], player.position[1]];
862 if (tui.mode.shows_info) {
863 center_position = [explorer.position[0], explorer.position[1]];
865 center_position[1] = center_position[1] * 2;
866 let offset = [center_position[0] - window_center[0],
867 center_position[1] - window_center[1]]
868 if (game.map_geometry == 'Hex' && offset[0] % 2) {
871 let term_y = Math.max(0, -offset[0]);
872 let term_x = Math.max(0, -offset[1]);
873 let map_y = Math.max(0, offset[0]);
874 let map_x = Math.max(0, offset[1]);
875 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
876 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
877 terminal.write(term_y, term_x, to_draw);
880 draw_mode_line: function() {
881 let help = 'hit [' + this.keys.help + '] for help';
882 if (this.mode.has_input_prompt) {
883 help = 'enter /help for help';
885 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
887 draw_turn_line: function(n) {
888 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
890 draw_history: function() {
891 let log_display_lines = [];
893 let y_offset_in_log = 0;
894 for (let line of this.log) {
895 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
897 log_display_lines = log_display_lines.concat(new_lines);
898 for (const y in link_data) {
899 const rel_y = y_offset_in_log + parseInt(y);
900 log_links[rel_y] = [];
901 for (let link of link_data[y]) {
902 log_links[rel_y].push(link);
905 y_offset_in_log += new_lines.length;
907 let i = log_display_lines.length - 1;
908 for (let y = terminal.rows - 1 - this.height_input;
909 y >= this.height_header && i >= 0;
911 terminal.write(y, this.window_width, log_display_lines[i]);
913 for (const key of Object.keys(log_links)) {
914 if (parseInt(key) <= i) {
915 delete log_links[key];
918 let offset = [terminal.rows - this.height_input - log_display_lines.length,
920 this.offset_links(offset, log_links);
922 draw_info: function() {
923 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
925 let offset = [this.height_header, this.window_width];
926 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
927 terminal.write(y, offset[1], lines[i]);
929 this.offset_links(offset, link_data);
931 draw_input: function() {
932 if (this.mode.has_input_prompt) {
933 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
934 terminal.write(y, this.window_width, this.input_lines[i]);
938 draw_help: function() {
939 let movement_keys_desc = '';
940 if (!this.mode.is_intro) {
941 movement_keys_desc = Object.keys(this.movement_keys).join(',');
943 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
944 if (this.mode.name == 'play') {
945 content += "Available actions:\n";
946 if (game.tasks.includes('MOVE')) {
947 content += "[" + movement_keys_desc + "] – move player\n";
949 if (game.tasks.includes('PICK_UP')) {
950 content += "[" + this.keys.take_thing + "] – pick up thing\n";
952 if (game.tasks.includes('DROP')) {
953 content += "[" + this.keys.drop_thing + "] – drop thing\n";
955 content += "[" + tui.keys.teleport + "] – teleport\n";
957 } else if (this.mode.name == 'study') {
958 content += "Available actions:\n";
959 content += '[' + movement_keys_desc + '] – move question mark\n';
960 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
962 } else if (this.mode.name == 'edit') {
963 content += "Available actions:\n";
964 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
965 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
968 } else if (this.mode.name == 'control_tile_draw') {
969 content += "Available actions:\n";
970 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
972 } else if (this.mode.name == 'chat') {
973 content += '/nick NAME – re-name yourself to NAME\n';
974 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
975 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
976 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
977 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
979 content += this.mode.list_available_modes();
981 if (!this.mode.has_input_prompt) {
982 start_x = this.window_width
984 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
985 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
986 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
987 terminal.write(y, start_x, lines[i]);
990 toggle_tile_draw: function() {
992 tui.tile_draw = false;
994 tui.tile_draw = true;
997 toggle_map_mode: function() {
998 if (tui.map_mode == 'terrain only') {
999 tui.map_mode = 'terrain + annotations';
1000 } else if (tui.map_mode == 'terrain + annotations') {
1001 tui.map_mode = 'terrain + things';
1003 tui.map_mode = 'terrain only';
1006 full_refresh: function() {
1008 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1009 if (this.mode.is_intro) {
1010 this.draw_history();
1013 if (game.turn_complete) {
1015 this.draw_turn_line();
1017 this.draw_mode_line();
1018 if (this.mode.shows_info) {
1021 this.draw_history();
1025 if (this.show_help) {
1037 this.map_control = "";
1038 this.map_size = [0,0];
1039 this.player_id = -1;
1043 get_thing: function(id_, create_if_not_found=false) {
1044 if (id_ in game.things) {
1045 return game.things[id_];
1046 } else if (create_if_not_found) {
1047 let t = new Thing([0,0]);
1048 game.things[id_] = t;
1052 move: function(start_position, direction) {
1053 let target = [start_position[0], start_position[1]];
1054 if (direction == 'LEFT') {
1056 } else if (direction == 'RIGHT') {
1058 } else if (game.map_geometry == 'Square') {
1059 if (direction == 'UP') {
1061 } else if (direction == 'DOWN') {
1064 } else if (game.map_geometry == 'Hex') {
1065 let start_indented = start_position[0] % 2;
1066 if (direction == 'UPLEFT') {
1068 if (!start_indented) {
1071 } else if (direction == 'UPRIGHT') {
1073 if (start_indented) {
1076 } else if (direction == 'DOWNLEFT') {
1078 if (!start_indented) {
1081 } else if (direction == 'DOWNRIGHT') {
1083 if (start_indented) {
1088 if (target[0] < 0 || target[1] < 0 ||
1089 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1094 teleport: function() {
1095 let player = this.get_thing(game.player_id);
1096 if (player.position in this.portals) {
1097 server.reconnect_to(this.portals[player.position]);
1099 terminal.blink_screen();
1100 tui.log_msg('? not standing on portal')
1108 server.init(websocket_location);
1114 move: function(direction) {
1115 let target = game.move(this.position, direction);
1117 this.position = target
1118 if (tui.mode.shows_info) {
1120 } else if (tui.tile_draw) {
1121 this.send_tile_control_command();
1124 terminal.blink_screen();
1127 update_info_db: function(yx, str) {
1128 this.info_db[yx] = str;
1129 if (tui.mode.name == 'study') {
1133 empty_info_db: function() {
1135 this.info_hints = [];
1136 if (tui.mode.name == 'study') {
1140 query_info: function() {
1141 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1143 get_info: function() {
1144 let info = "MAP VIEW: " + tui.map_mode + "\n";
1145 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1146 if (game.fov[position_i] != '.') {
1147 return info + 'outside field of view';
1149 let terrain_char = game.map[position_i]
1150 let terrain_desc = '?'
1151 if (game.terrains[terrain_char]) {
1152 terrain_desc = game.terrains[terrain_char];
1154 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1155 let protection = game.map_control[position_i];
1156 if (protection == '.') {
1157 protection = 'unprotected';
1159 info += 'PROTECTION: ' + protection + '\n';
1160 for (let t_id in game.things) {
1161 let t = game.things[t_id];
1162 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1163 let symbol = game.thing_types[t.type_];
1164 info += "THING: " + t.type_ + " / " + symbol;
1165 if (t.player_char) {
1166 info += t.player_char;
1169 info += " (" + t.name_ + ")";
1174 if (this.position in game.portals) {
1175 info += "PORTAL: " + game.portals[this.position] + "\n";
1177 if (this.position in this.info_db) {
1178 info += "ANNOTATIONS: " + this.info_db[this.position];
1180 info += 'waiting …';
1184 annotate: function(msg) {
1185 if (msg.length == 0) {
1186 msg = " "; // triggers annotation deletion
1188 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1190 set_portal: function(msg) {
1191 if (msg.length == 0) {
1192 msg = " "; // triggers portal deletion
1194 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1196 send_tile_control_command: function() {
1197 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1201 tui.inputEl.addEventListener('input', (event) => {
1202 if (tui.mode.has_input_prompt) {
1203 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1204 if (tui.inputEl.value.length > max_length) {
1205 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1207 tui.recalc_input_lines();
1208 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1209 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1210 tui.switch_mode('edit');
1214 document.onclick = function() {
1215 tui.show_help = false;
1217 tui.inputEl.addEventListener('keydown', (event) => {
1218 tui.show_help = false;
1219 if (event.key == 'Enter') {
1220 event.preventDefault();
1222 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1223 tui.show_help = true;
1225 tui.restore_input_values();
1226 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1227 && !tui.mode.is_single_char_entry) {
1228 tui.show_help = true;
1229 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1230 tui.login_name = tui.inputEl.value;
1231 server.send(['LOGIN', tui.inputEl.value]);
1233 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1234 if (tui.inputEl.value.length == 0) {
1235 tui.log_msg('@ aborted');
1237 server.send(['SET_MAP_CONTROL_PASSWORD',
1238 tui.tile_control_char, tui.inputEl.value]);
1240 tui.switch_mode('admin');
1241 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1242 explorer.set_portal(tui.inputEl.value);
1243 tui.switch_mode('edit');
1244 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1245 explorer.annotate(tui.inputEl.value);
1246 tui.switch_mode('edit');
1247 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1248 if (tui.inputEl.value.length == 0) {
1249 tui.inputEl.value = " ";
1251 tui.password = tui.inputEl.value
1252 tui.switch_mode('edit');
1253 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1254 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1255 tui.switch_mode('play');
1256 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1257 if (tui.inputEl.value.length != 1) {
1258 tui.log_msg('@ entered non-single-char, therefore aborted');
1259 tui.switch_mode('admin');
1261 tui.tile_control_char = tui.inputEl.value[0];
1262 tui.switch_mode('control_pw_pw');
1264 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1265 if (tui.inputEl.value.length != 1) {
1266 tui.log_msg('@ entered non-single-char, therefore aborted');
1267 tui.switch_mode('admin');
1269 tui.tile_control_char = tui.inputEl.value[0];
1270 tui.switch_mode('control_tile_draw');
1272 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1273 let tokens = parser.tokenize(tui.inputEl.value);
1274 if (tokens.length > 0 && tokens[0].length > 0) {
1275 if (tui.inputEl.value[0][0] == '/') {
1276 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1277 tui.switch_mode('play');
1278 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1279 tui.switch_mode('study');
1280 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1281 tui.switch_mode('edit');
1282 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1283 tui.switch_mode('admin_enter');
1284 } else if (tokens[0].slice(1) == 'nick') {
1285 if (tokens.length > 1) {
1286 server.send(['NICK', tokens[1]]);
1288 tui.log_msg('? need new name');
1291 tui.log_msg('? unknown command');
1294 server.send(['ALL', tui.inputEl.value]);
1296 } else if (tui.inputEl.valuelength > 0) {
1297 server.send(['ALL', tui.inputEl.value]);
1300 } else if (tui.mode.name == 'play') {
1301 if (tui.mode.mode_switch_on_key(event)) {
1303 } else if (event.key === tui.keys.take_thing
1304 && game.tasks.includes('PICK_UP')) {
1305 server.send(["TASK:PICK_UP"]);
1306 } else if (event.key === tui.keys.drop_thing
1307 && game.tasks.includes('DROP')) {
1308 server.send(["TASK:DROP"]);
1309 } else if (event.key in tui.movement_keys
1310 && game.tasks.includes('MOVE')) {
1311 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1312 } else if (event.key === tui.keys.teleport) {
1315 } else if (tui.mode.name == 'study') {
1316 if (tui.mode.mode_switch_on_key(event)) {
1318 } else if (event.key in tui.movement_keys) {
1319 explorer.move(tui.movement_keys[event.key]);
1320 } else if (event.key == tui.keys.toggle_map_mode) {
1321 tui.toggle_map_mode();
1323 } else if (tui.mode.name == 'control_tile_draw') {
1324 if (tui.mode.mode_switch_on_key(event)) {
1326 } else if (event.key in tui.movement_keys) {
1327 explorer.move(tui.movement_keys[event.key]);
1328 } else if (event.key === tui.keys.toggle_tile_draw) {
1329 tui.toggle_tile_draw();
1331 } else if (tui.mode.name == 'admin') {
1332 if (tui.mode.mode_switch_on_key(event)) {
1335 } else if (tui.mode.name == 'edit') {
1336 if (tui.mode.mode_switch_on_key(event)) {
1338 } else if (event.key in tui.movement_keys
1339 && game.tasks.includes('MOVE')) {
1340 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1341 } else if (event.key === tui.keys.flatten
1342 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1343 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1349 rows_selector.addEventListener('input', function() {
1350 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1353 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1354 terminal.initialize();
1357 cols_selector.addEventListener('input', function() {
1358 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1361 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1362 terminal.initialize();
1363 tui.window_width = terminal.cols / 2,
1366 for (let key_selector of key_selectors) {
1367 key_selector.addEventListener('input', function() {
1368 window.localStorage.setItem(key_selector.id, key_selector.value);
1372 window.setInterval(function() {
1373 if (server.connected) {
1374 server.send(['PING']);
1376 server.reconnect_to(server.url);
1377 tui.log_msg('@ attempting reconnect …')
1380 document.getElementById("terminal").onclick = function() {
1381 tui.inputEl.focus();
1383 document.getElementById("help").onclick = function() {
1384 tui.show_help = true;
1387 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1388 const mode = switchEl.id.slice("switch_to_".length);
1389 switchEl.onclick = function() {
1390 tui.switch_mode(mode);
1394 document.getElementById("toggle_tile_draw").onclick = function() {
1395 tui.toggle_tile_draw();
1397 document.getElementById("toggle_map_mode").onclick = function() {
1398 tui.toggle_map_mode();
1401 document.getElementById("take_thing").onclick = function() {
1402 server.send(['TASK:PICK_UP']);
1404 document.getElementById("drop_thing").onclick = function() {
1405 server.send(['TASK:DROP']);
1407 document.getElementById("flatten").onclick = function() {
1408 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1410 document.getElementById("teleport").onclick = function() {
1413 document.getElementById("move_upleft").onclick = function() {
1414 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1415 server.send(['TASK:MOVE', 'UPLEFT']);
1417 explorer.move('UPLEFT');
1420 document.getElementById("move_left").onclick = function() {
1421 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1422 server.send(['TASK:MOVE', 'LEFT']);
1424 explorer.move('LEFT');
1427 document.getElementById("move_downleft").onclick = function() {
1428 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1429 server.send(['TASK:MOVE', 'DOWNLEFT']);
1431 explorer.move('DOWNLEFT');
1434 document.getElementById("move_down").onclick = function() {
1435 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1436 server.send(['TASK:MOVE', 'DOWN']);
1438 explorer.move('DOWN');
1441 document.getElementById("move_up").onclick = function() {
1442 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1443 server.send(['TASK:MOVE', 'UP']);
1445 explorer.move('UP');
1448 document.getElementById("move_upright").onclick = function() {
1449 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1450 server.send(['TASK:MOVE', 'UPRIGHT']);
1452 explorer.move('UPRIGHT');
1455 document.getElementById("move_right").onclick = function() {
1456 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1457 server.send(['TASK:MOVE', 'RIGHT']);
1459 explorer.move('RIGHT');
1462 document.getElementById("move_downright").onclick = function() {
1463 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1464 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1466 explorer.move('DOWNRIGHT');