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 everything/terrain/annotations view</button>
50 <td><button id="switch_to_play">play mode</button></td>
52 <button id="take_thing">take thing</button>
53 <button id="teleport">teleport</button>
54 <button id="drop_thing">drop thing</button>
58 <td><button id="switch_to_edit">map edit mode</button></td>
60 <button id="switch_to_write">change tile</button>
61 <button id="flatten">flatten surroundings</button>
62 <button id="switch_to_password">change tile editing password</button>
63 <button id="switch_to_annotate">annotate tile</button>
64 <button id="switch_to_portal">edit portal link</button>
68 <td><button id="switch_to_admin_enter">admin mode</button></td>
70 <button id="switch_to_control_pw_type">change tile control password</button>
71 <button id="switch_to_control_tile_type">change tiles control</button>
72 <button id="toggle_tile_draw">toggle tiles control 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>take thing under player: <input id="key_take_thing" type="text" value="z" />
93 <li>drop carried 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 everything/terrain/annotations view: <input id="key_toggle_map_mode" type="text" value="M" />
106 <li>toggle everything/terrain/annotations view: <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.'
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.'},
124 'long': 'This mode allows you to change the map in various ways.'
127 'short': 'terrain write',
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 tiles control password',
132 'long': 'This mode is the first of two steps to change the password for a tile control character. First enter the tile control 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 control character. Enter the new password for the tile control 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 control areas on the map. First enter the tile control 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 control areas on the map. Toggle tile control drawing on, then move cursor around the map to draw selected tile control 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': 'Pick 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': '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 = Object.keys(this.movement_keys).join(',');
940 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
941 if (this.mode.name == 'play') {
942 content += "Available actions:\n";
943 if (game.tasks.includes('MOVE')) {
944 content += "[" + movement_keys_desc + "] – move player\n";
946 if (game.tasks.includes('PICK_UP')) {
947 content += "[" + this.keys.take_thing + "] – pick up thing\n";
949 if (game.tasks.includes('DROP')) {
950 content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
952 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
954 } else if (this.mode.name == 'study') {
955 content += "Available actions:\n";
956 content += '[' + movement_keys_desc + '] – move question mark\n';
957 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
959 } else if (this.mode.name == 'edit') {
960 content += "Available actions:\n";
961 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
962 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
965 } else if (this.mode.name == 'control_tile_draw') {
966 content += "Available actions:\n";
967 content += "[" + tui.keys.toggle_tile_draw + "] – toggle tile control drawing\n";
969 } else if (this.mode.name == 'chat') {
970 content += '/nick NAME – re-name yourself to NAME\n';
971 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
972 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
973 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
974 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
976 content += this.mode.list_available_modes();
978 if (!this.mode.has_input_prompt) {
979 start_x = this.window_width
981 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
982 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
983 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
984 terminal.write(y, start_x, lines[i]);
987 toggle_tile_draw: function() {
989 tui.tile_draw = false;
991 tui.tile_draw = true;
994 toggle_map_mode: function() {
995 if (tui.map_mode == 'terrain only') {
996 tui.map_mode = 'terrain + annotations';
997 } else if (tui.map_mode == 'terrain + annotations') {
998 tui.map_mode = 'terrain + things';
1000 tui.map_mode = 'terrain only';
1003 full_refresh: function() {
1005 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1006 if (this.mode.is_intro) {
1007 this.draw_history();
1010 if (game.turn_complete) {
1012 this.draw_turn_line();
1014 this.draw_mode_line();
1015 if (this.mode.shows_info) {
1018 this.draw_history();
1022 if (this.show_help) {
1034 this.map_control = "";
1035 this.map_size = [0,0];
1036 this.player_id = -1;
1040 get_thing: function(id_, create_if_not_found=false) {
1041 if (id_ in game.things) {
1042 return game.things[id_];
1043 } else if (create_if_not_found) {
1044 let t = new Thing([0,0]);
1045 game.things[id_] = t;
1049 move: function(start_position, direction) {
1050 let target = [start_position[0], start_position[1]];
1051 if (direction == 'LEFT') {
1053 } else if (direction == 'RIGHT') {
1055 } else if (game.map_geometry == 'Square') {
1056 if (direction == 'UP') {
1058 } else if (direction == 'DOWN') {
1061 } else if (game.map_geometry == 'Hex') {
1062 let start_indented = start_position[0] % 2;
1063 if (direction == 'UPLEFT') {
1065 if (!start_indented) {
1068 } else if (direction == 'UPRIGHT') {
1070 if (start_indented) {
1073 } else if (direction == 'DOWNLEFT') {
1075 if (!start_indented) {
1078 } else if (direction == 'DOWNRIGHT') {
1080 if (start_indented) {
1085 if (target[0] < 0 || target[1] < 0 ||
1086 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1091 teleport: function() {
1092 let player = this.get_thing(game.player_id);
1093 if (player.position in this.portals) {
1094 server.reconnect_to(this.portals[player.position]);
1096 terminal.blink_screen();
1097 tui.log_msg('? not standing on portal')
1105 server.init(websocket_location);
1111 move: function(direction) {
1112 let target = game.move(this.position, direction);
1114 this.position = target
1115 if (tui.mode.shows_info) {
1117 } else if (tui.tile_draw) {
1118 this.send_tile_control_command();
1121 terminal.blink_screen();
1124 update_info_db: function(yx, str) {
1125 this.info_db[yx] = str;
1126 if (tui.mode.name == 'study') {
1130 empty_info_db: function() {
1132 this.info_hints = [];
1133 if (tui.mode.name == 'study') {
1137 query_info: function() {
1138 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1140 get_info: function() {
1141 let info = "MAP VIEW: " + tui.map_mode + "\n";
1142 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1143 if (game.fov[position_i] != '.') {
1144 return info + 'outside field of view';
1146 let terrain_char = game.map[position_i]
1147 let terrain_desc = '?'
1148 if (game.terrains[terrain_char]) {
1149 terrain_desc = game.terrains[terrain_char];
1151 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1152 let protection = game.map_control[position_i];
1153 if (protection == '.') {
1154 protection = 'unprotected';
1156 info += 'PROTECTION: ' + protection + '\n';
1157 for (let t_id in game.things) {
1158 let t = game.things[t_id];
1159 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1160 let symbol = game.thing_types[t.type_];
1161 info += "THING: " + t.type_ + " / " + symbol;
1162 if (t.player_char) {
1163 info += t.player_char;
1166 info += " (" + t.name_ + ")";
1171 if (this.position in game.portals) {
1172 info += "PORTAL: " + game.portals[this.position] + "\n";
1174 if (this.position in this.info_db) {
1175 info += "ANNOTATIONS: " + this.info_db[this.position];
1177 info += 'waiting …';
1181 annotate: function(msg) {
1182 if (msg.length == 0) {
1183 msg = " "; // triggers annotation deletion
1185 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1187 set_portal: function(msg) {
1188 if (msg.length == 0) {
1189 msg = " "; // triggers portal deletion
1191 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1193 send_tile_control_command: function() {
1194 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1198 tui.inputEl.addEventListener('input', (event) => {
1199 if (tui.mode.has_input_prompt) {
1200 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1201 if (tui.inputEl.value.length > max_length) {
1202 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1204 tui.recalc_input_lines();
1205 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1206 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1207 tui.switch_mode('edit');
1211 tui.inputEl.addEventListener('keydown', (event) => {
1212 tui.show_help = false;
1213 if (event.key == 'Enter') {
1214 event.preventDefault();
1216 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1217 tui.show_help = true;
1219 tui.restore_input_values();
1220 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1221 && !tui.mode.is_single_char_entry) {
1222 tui.show_help = true;
1223 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1224 tui.login_name = tui.inputEl.value;
1225 server.send(['LOGIN', tui.inputEl.value]);
1227 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1228 if (tui.inputEl.value.length == 0) {
1229 tui.log_msg('@ aborted');
1231 server.send(['SET_MAP_CONTROL_PASSWORD',
1232 tui.tile_control_char, tui.inputEl.value]);
1234 tui.switch_mode('admin');
1235 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1236 explorer.set_portal(tui.inputEl.value);
1237 tui.switch_mode('edit');
1238 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1239 explorer.annotate(tui.inputEl.value);
1240 tui.switch_mode('edit');
1241 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1242 if (tui.inputEl.value.length == 0) {
1243 tui.inputEl.value = " ";
1245 tui.password = tui.inputEl.value
1246 tui.switch_mode('edit');
1247 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1248 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1249 tui.switch_mode('play');
1250 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1251 if (tui.inputEl.value.length != 1) {
1252 tui.log_msg('@ entered non-single-char, therefore aborted');
1253 tui.switch_mode('admin');
1255 tui.tile_control_char = tui.inputEl.value[0];
1256 tui.switch_mode('control_pw_pw');
1258 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1259 if (tui.inputEl.value.length != 1) {
1260 tui.log_msg('@ entered non-single-char, therefore aborted');
1261 tui.switch_mode('admin');
1263 tui.tile_control_char = tui.inputEl.value[0];
1264 tui.switch_mode('control_tile_draw');
1266 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1267 let tokens = parser.tokenize(tui.inputEl.value);
1268 if (tokens.length > 0 && tokens[0].length > 0) {
1269 if (tui.inputEl.value[0][0] == '/') {
1270 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1271 tui.switch_mode('play');
1272 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1273 tui.switch_mode('study');
1274 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1275 tui.switch_mode('edit');
1276 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1277 tui.switch_mode('admin_enter');
1278 } else if (tokens[0].slice(1) == 'nick') {
1279 if (tokens.length > 1) {
1280 server.send(['NICK', tokens[1]]);
1282 tui.log_msg('? need new name');
1285 tui.log_msg('? unknown command');
1288 server.send(['ALL', tui.inputEl.value]);
1290 } else if (tui.inputEl.valuelength > 0) {
1291 server.send(['ALL', tui.inputEl.value]);
1294 } else if (tui.mode.name == 'play') {
1295 if (tui.mode.mode_switch_on_key(event)) {
1297 } else if (event.key === tui.keys.take_thing
1298 && game.tasks.includes('PICK_UP')) {
1299 server.send(["TASK:PICK_UP"]);
1300 } else if (event.key === tui.keys.drop_thing
1301 && game.tasks.includes('DROP')) {
1302 server.send(["TASK:DROP"]);
1303 } else if (event.key in tui.movement_keys
1304 && game.tasks.includes('MOVE')) {
1305 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1306 } else if (event.key === tui.keys.teleport) {
1309 } else if (tui.mode.name == 'study') {
1310 if (tui.mode.mode_switch_on_key(event)) {
1312 } else if (event.key in tui.movement_keys) {
1313 explorer.move(tui.movement_keys[event.key]);
1314 } else if (event.key == tui.keys.toggle_map_mode) {
1315 tui.toggle_map_mode();
1317 } else if (tui.mode.name == 'control_tile_draw') {
1318 if (tui.mode.mode_switch_on_key(event)) {
1320 } else if (event.key in tui.movement_keys) {
1321 explorer.move(tui.movement_keys[event.key]);
1322 } else if (event.key === tui.keys.toggle_tile_draw) {
1323 tui.toggle_tile_draw();
1325 } else if (tui.mode.name == 'admin') {
1326 if (tui.mode.mode_switch_on_key(event)) {
1329 } else if (tui.mode.name == 'edit') {
1330 if (tui.mode.mode_switch_on_key(event)) {
1332 } else if (event.key in tui.movement_keys
1333 && game.tasks.includes('MOVE')) {
1334 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1335 } else if (event.key === tui.keys.flatten
1336 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1337 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1343 rows_selector.addEventListener('input', function() {
1344 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1347 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1348 terminal.initialize();
1351 cols_selector.addEventListener('input', function() {
1352 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1355 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1356 terminal.initialize();
1357 tui.window_width = terminal.cols / 2,
1360 for (let key_selector of key_selectors) {
1361 key_selector.addEventListener('input', function() {
1362 window.localStorage.setItem(key_selector.id, key_selector.value);
1366 window.setInterval(function() {
1367 if (server.connected) {
1368 server.send(['PING']);
1370 server.reconnect_to(server.url);
1371 tui.log_msg('@ attempting reconnect …')
1374 document.getElementById("terminal").onclick = function() {
1375 tui.inputEl.focus();
1377 document.getElementById("help").onclick = function() {
1378 tui.show_help = true;
1381 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1382 const mode = switchEl.id.slice("switch_to_".length);
1383 switchEl.onclick = function() {
1384 tui.switch_mode(mode);
1388 document.getElementById("toggle_tile_draw").onclick = function() {
1389 tui.toggle_tile_draw();
1391 document.getElementById("toggle_map_mode").onclick = function() {
1392 tui.toggle_map_mode();
1395 document.getElementById("take_thing").onclick = function() {
1396 server.send(['TASK:PICK_UP']);
1398 document.getElementById("drop_thing").onclick = function() {
1399 server.send(['TASK:DROP']);
1401 document.getElementById("flatten").onclick = function() {
1402 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1404 document.getElementById("teleport").onclick = function() {
1407 document.getElementById("move_upleft").onclick = function() {
1408 if (tui.mode.name == 'play') {
1409 server.send(['TASK:MOVE', 'UPLEFT']);
1411 explorer.move('UPLEFT');
1414 document.getElementById("move_left").onclick = function() {
1415 if (tui.mode.name == 'play') {
1416 server.send(['TASK:MOVE', 'LEFT']);
1418 explorer.move('LEFT');
1421 document.getElementById("move_downleft").onclick = function() {
1422 if (tui.mode.name == 'play') {
1423 server.send(['TASK:MOVE', 'DOWNLEFT']);
1425 explorer.move('DOWNLEFT');
1428 document.getElementById("move_down").onclick = function() {
1429 if (tui.mode.name == 'play') {
1430 server.send(['TASK:MOVE', 'DOWN']);
1432 explorer.move('DOWN');
1435 document.getElementById("move_up").onclick = function() {
1436 if (tui.mode.name == 'play') {
1437 server.send(['TASK:MOVE', 'UP']);
1439 explorer.move('UP');
1442 document.getElementById("move_upright").onclick = function() {
1443 if (tui.mode.name == 'play') {
1444 server.send(['TASK:MOVE', 'UPRIGHT']);
1446 explorer.move('UPRIGHT');
1449 document.getElementById("move_right").onclick = function() {
1450 if (tui.mode.name == 'play') {
1451 server.send(['TASK:MOVE', 'RIGHT']);
1453 explorer.move('RIGHT');
1456 document.getElementById("move_downright").onclick = function() {
1457 if (tui.mode.name == 'play') {
1458 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1460 explorer.move('DOWNRIGHT');