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>
76 <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 />
78 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
79 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
80 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
81 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
82 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
83 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
84 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
85 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
86 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
87 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
88 <li>help: <input id="key_help" type="text" value="h" />
89 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
90 <li>teleport: <input id="key_teleport" type="text" value="p" />
91 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
92 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
93 <li><input id="key_switch_to_chat" type="text" value="t" />
94 <li><input id="key_switch_to_play" type="text" value="p" />
95 <li><input id="key_switch_to_study" type="text" value="?" />
96 <li><input id="key_switch_to_edit" type="text" value="E" />
97 <li><input id="key_switch_to_write" type="text" value="m" />
98 <li><input id="key_switch_to_password" type="text" value="P" />
99 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
100 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
101 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
102 <li><input id="key_switch_to_annotate" type="text" value="M" />
103 <li><input id="key_switch_to_portal" type="text" value="T" />
104 <li>toggle everything/terrain/annotations view: <input id="key_toggle_map_mode" type="text" value="M" />
109 let websocket_location = "wss://plomlompom.com/rogue_chat/";
110 //let websocket_location = "ws://localhost:8000/";
115 'long': 'This mode allows you to interact with the map.'
119 '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.'},
122 'long': 'This mode allows you to change the map in various ways.'
125 'short': 'terrain write',
126 '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.'
129 'short': 'change tiles control password',
130 '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!'
133 'short': 'change tiles control password',
134 '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.'
136 'control_tile_type': {
137 'short': 'change tiles control',
138 '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.'
140 'control_tile_draw': {
141 'short': 'change tiles control',
142 'long': 'This mode is the second of two steps to change tile control areas on the map. Move cursor around the map to draw selected tile control character'
145 'short': 'annotate tile',
146 '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.'
149 'short': 'edit portal',
150 '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.'
154 '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:'
158 'long': 'Pick your player name.'
160 'waiting_for_server': {
161 'short': 'waiting for server response',
162 'long': 'Waiting for a server response.'
165 'short': 'waiting for server response',
166 'long': 'Waiting for a server response.'
169 'short': 'map edit password',
170 '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.'
173 'short': 'become admin',
174 'long': 'This mode allows you to become admin if you know an admin password.'
178 'long': 'This mode allows you access to actions limited to administrators.'
182 let rows_selector = document.getElementById("n_rows");
183 let cols_selector = document.getElementById("n_cols");
184 let key_selectors = document.querySelectorAll('[id^="key_"]');
186 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
187 const action = key_switch_selector.id.slice("key_switch_to_".length);
188 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
191 function restore_selector_value(selector) {
192 let stored_selection = window.localStorage.getItem(selector.id);
193 if (stored_selection) {
194 selector.value = stored_selection;
197 restore_selector_value(rows_selector);
198 restore_selector_value(cols_selector);
199 for (let key_selector of key_selectors) {
200 restore_selector_value(key_selector);
206 initialize: function() {
207 this.rows = rows_selector.value;
208 this.cols = cols_selector.value;
209 this.pre_el = document.getElementById("terminal");
210 this.pre_el.style.color = this.foreground;
211 this.pre_el.style.backgroundColor = this.background;
214 for (let y = 0, x = 0; y <= this.rows; x++) {
215 if (x == this.cols) {
218 this.content.push(line);
220 if (y == this.rows) {
227 blink_screen: function() {
228 this.pre_el.style.color = this.background;
229 this.pre_el.style.backgroundColor = this.foreground;
231 this.pre_el.style.color = this.foreground;
232 this.pre_el.style.backgroundColor = this.background;
235 refresh: function() {
236 function escapeHTML(str) {
238 replace(/&/g, '&').
239 replace(/</g, '<').
240 replace(/>/g, '>').
241 replace(/'/g, ''').
242 replace(/"/g, '"');
244 let pre_content = '';
245 for (let y = 0; y < this.rows; y++) {
246 let line = this.content[y].join('');
248 if (y in tui.links) {
250 for (let span of tui.links[y]) {
251 chunks.push(escapeHTML(line.slice(start_x, span[0])));
252 chunks.push('<a href="');
253 chunks.push(escapeHTML(span[2]));
255 chunks.push(escapeHTML(line.slice(span[0], span[1])));
259 chunks.push(escapeHTML(line.slice(start_x)));
261 chunks = [escapeHTML(line)];
263 for (const chunk of chunks) {
264 pre_content += chunk;
268 this.pre_el.innerHTML = pre_content;
270 write: function(start_y, start_x, msg) {
271 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
272 this.content[start_y][x] = msg[i];
275 drawBox: function(start_y, start_x, height, width) {
276 let end_y = start_y + height;
277 let end_x = start_x + width;
278 for (let y = start_y, x = start_x; y < this.rows; x++) {
286 this.content[y][x] = ' ';
290 terminal.initialize();
293 tokenize: function(str) {
298 for (let i = 0; i < str.length; i++) {
304 } else if (c == '\\') {
306 } else if (c == '"') {
311 } else if (c == '"') {
313 } else if (c === ' ') {
314 if (token.length > 0) {
322 if (token.length > 0) {
327 parse_yx: function(position_string) {
328 let coordinate_strings = position_string.split(',')
329 let position = [0, 0];
330 position[0] = parseInt(coordinate_strings[0].slice(2));
331 position[1] = parseInt(coordinate_strings[1].slice(2));
343 init: function(url) {
345 this.websocket = new WebSocket(this.url);
346 this.websocket.onopen = function(event) {
347 server.connected = true;
348 game.thing_types = {};
350 server.send(['TASKS']);
351 server.send(['TERRAINS']);
352 server.send(['THING_TYPES']);
353 tui.log_msg("@ server connected! :)");
354 tui.switch_mode('login');
356 this.websocket.onclose = function(event) {
357 server.connected = false;
358 tui.switch_mode('waiting_for_server');
359 tui.log_msg("@ server disconnected :(");
361 this.websocket.onmessage = this.handle_event;
363 reconnect_to: function(url) {
364 this.websocket.close();
367 send: function(tokens) {
368 this.websocket.send(unparser.untokenize(tokens));
370 handle_event: function(event) {
371 let tokens = parser.tokenize(event.data);
372 if (tokens[0] === 'TURN') {
373 game.turn_complete = false;
374 explorer.empty_info_db();
377 game.turn = parseInt(tokens[1]);
378 } else if (tokens[0] === 'THING') {
379 let t = game.get_thing(tokens[3], true);
380 t.position = parser.parse_yx(tokens[1]);
382 } else if (tokens[0] === 'THING_NAME') {
383 let t = game.get_thing(tokens[1], false);
387 } else if (tokens[0] === 'THING_CHAR') {
388 let t = game.get_thing(tokens[1], false);
390 t.player_char = tokens[2];
392 } else if (tokens[0] === 'TASKS') {
393 game.tasks = tokens[1].split(',');
394 tui.mode_write.legal = game.tasks.includes('WRITE');
395 } else if (tokens[0] === 'THING_TYPE') {
396 game.thing_types[tokens[1]] = tokens[2]
397 } else if (tokens[0] === 'TERRAIN') {
398 game.terrains[tokens[1]] = tokens[2]
399 } else if (tokens[0] === 'MAP') {
400 game.map_geometry = tokens[1];
402 game.map_size = parser.parse_yx(tokens[2]);
404 } else if (tokens[0] === 'FOV') {
406 } else if (tokens[0] === 'MAP_CONTROL') {
407 game.map_control = tokens[1]
408 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
409 game.turn_complete = true;
410 if (tui.mode.name == 'post_login_wait') {
411 tui.switch_mode('play');
412 } else if (tui.mode.name == 'study') {
413 explorer.query_info();
416 } else if (tokens[0] === 'CHAT') {
417 tui.log_msg('# ' + tokens[1], 1);
418 } else if (tokens[0] === 'PLAYER_ID') {
419 game.player_id = parseInt(tokens[1]);
420 } else if (tokens[0] === 'LOGIN_OK') {
421 this.send(['GET_GAMESTATE']);
422 tui.switch_mode('post_login_wait');
423 } else if (tokens[0] === 'ADMIN_OK') {
425 tui.log_msg('@ you now have admin rights');
426 tui.switch_mode('admin');
427 } else if (tokens[0] === 'PORTAL') {
428 let position = parser.parse_yx(tokens[1]);
429 game.portals[position] = tokens[2];
430 } else if (tokens[0] === 'ANNOTATION_HINT') {
431 let position = parser.parse_yx(tokens[1]);
432 explorer.info_hints = explorer.info_hints.concat([position]);
433 } else if (tokens[0] === 'ANNOTATION') {
434 let position = parser.parse_yx(tokens[1]);
435 explorer.update_info_db(position, tokens[2]);
436 tui.restore_input_values();
438 } else if (tokens[0] === 'UNHANDLED_INPUT') {
439 tui.log_msg('? unknown command');
440 } else if (tokens[0] === 'PLAY_ERROR') {
441 tui.log_msg('? ' + tokens[1]);
442 terminal.blink_screen();
443 } else if (tokens[0] === 'ARGUMENT_ERROR') {
444 tui.log_msg('? syntax error: ' + tokens[1]);
445 } else if (tokens[0] === 'GAME_ERROR') {
446 tui.log_msg('? game error: ' + tokens[1]);
447 } else if (tokens[0] === 'PONG') {
450 tui.log_msg('? unhandled input: ' + event.data);
456 quote: function(str) {
458 for (let i = 0; i < str.length; i++) {
460 if (['"', '\\'].includes(c)) {
466 return quoted.join('');
468 to_yx: function(yx_coordinate) {
469 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
471 untokenize: function(tokens) {
472 let quoted_tokens = [];
473 for (let token of tokens) {
474 quoted_tokens.push(this.quote(token));
476 return quoted_tokens.join(" ");
481 constructor(name, has_input_prompt=false, shows_info=false,
482 is_intro=false, is_single_char_entry=false) {
484 this.short_desc = mode_helps[name].short;
485 this.available_modes = [];
486 this.has_input_prompt = has_input_prompt;
487 this.shows_info= shows_info;
488 this.is_intro = is_intro;
489 this.help_intro = mode_helps[name].long;
490 this.is_single_char_entry = is_single_char_entry;
493 *iter_available_modes() {
494 for (let mode_name of this.available_modes) {
495 let mode = tui['mode_' + mode_name];
499 let key = tui.keys['switch_to_' + mode.name];
503 list_available_modes() {
505 if (this.available_modes.length > 0) {
506 msg += 'Other modes available from here:\n';
507 for (let [mode, key] of this.iter_available_modes()) {
508 msg += '[' + key + '] – ' + mode.short_desc + '\n';
513 mode_switch_on_key(key_event) {
514 for (let [mode, key] of this.iter_available_modes()) {
515 if (key_event.key == key) {
516 event.preventDefault();
517 tui.switch_mode(mode.name);
529 window_width: terminal.cols / 2,
536 mode_waiting_for_server: new Mode('waiting_for_server',
538 mode_login: new Mode('login', true, false, true),
539 mode_post_login_wait: new Mode('post_login_wait'),
540 mode_chat: new Mode('chat', true),
541 mode_annotate: new Mode('annotate', true, true),
542 mode_play: new Mode('play'),
543 mode_study: new Mode('study', false, true),
544 mode_write: new Mode('write', false, false, false, true),
545 mode_edit: new Mode('edit'),
546 mode_control_pw_type: new Mode('control_pw_type',
547 false, false, false, true),
548 mode_portal: new Mode('portal', true, true),
549 mode_password: new Mode('password', true),
550 mode_admin_enter: new Mode('admin_enter', true),
551 mode_admin: new Mode('admin'),
552 mode_control_pw_pw: new Mode('control_pw_pw', true),
553 mode_control_tile_type: new Mode('control_tile_type',
554 false, false, false, true),
555 mode_control_tile_draw: new Mode('control_tile_draw'),
557 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
558 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
559 this.mode_admin.available_modes = ["control_pw_type",
560 "control_tile_type", "chat",
561 "study", "play", "edit"]
562 this.mode_control_tile_draw.available_modes = ["admin_enter"]
563 this.mode_edit.available_modes = ["write", "annotate", "portal",
564 "password", "chat", "study", "play",
566 this.mode = this.mode_waiting_for_server;
567 this.inputEl = document.getElementById("input");
568 this.inputEl.focus();
569 this.recalc_input_lines();
570 this.height_header = this.height_turn_line + this.height_mode_line;
571 this.log_msg("@ waiting for server connection ...");
574 init_keys: function() {
576 for (let key_selector of key_selectors) {
577 this.keys[key_selector.id.slice(4)] = key_selector.value;
579 if (game.map_geometry == 'Square') {
580 this.movement_keys = {
581 [this.keys.square_move_up]: 'UP',
582 [this.keys.square_move_left]: 'LEFT',
583 [this.keys.square_move_down]: 'DOWN',
584 [this.keys.square_move_right]: 'RIGHT'
586 document.getElementById("move_upright").hidden = true;
587 document.getElementById("move_upleft").hidden = true;
588 document.getElementById("move_downright").hidden = true;
589 document.getElementById("move_downleft").hidden = true;
590 document.getElementById("move_up").hidden = false;
591 document.getElementById("move_down").hidden = false;
592 } else if (game.map_geometry == 'Hex') {
593 document.getElementById("move_upright").hidden = false;
594 document.getElementById("move_upleft").hidden = false;
595 document.getElementById("move_downright").hidden = false;
596 document.getElementById("move_downleft").hidden = false;
597 document.getElementById("move_up").hidden = true;
598 document.getElementById("move_down").hidden = true;
599 this.movement_keys = {
600 [this.keys.hex_move_upleft]: 'UPLEFT',
601 [this.keys.hex_move_upright]: 'UPRIGHT',
602 [this.keys.hex_move_right]: 'RIGHT',
603 [this.keys.hex_move_downright]: 'DOWNRIGHT',
604 [this.keys.hex_move_downleft]: 'DOWNLEFT',
605 [this.keys.hex_move_left]: 'LEFT'
609 switch_mode: function(mode_name) {
610 this.inputEl.focus();
611 this.map_mode = 'all';
612 if (mode_name == 'admin_enter' && this.is_admin) {
615 this.mode = this['mode_' + mode_name];
616 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
617 explorer.position = game.things[game.player_id].position;
618 if (this.mode.shows_info) {
619 explorer.query_info();
620 } else if (this.mode.name == 'control_tile_draw') {
621 explorer.send_tile_control_command();
622 this.map_mode = 'control';
626 this.restore_input_values();
627 for (let el of document.getElementsByTagName("button")) {
630 document.getElementById("help").disabled = false;
631 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
632 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
633 move_key.disabled = false;
636 if (!this.mode.is_intro && this.mode.name != 'play') {
637 document.getElementById("switch_to_play").disabled = false;
639 if (!this.mode.is_intro && this.mode.name != 'study') {
640 document.getElementById("switch_to_study").disabled = false;
642 if (!this.mode.is_intro && this.mode.name != 'chat') {
643 document.getElementById("switch_to_chat").disabled = false;
645 if (!this.mode.is_intro && this.mode.name != 'edit') {
646 document.getElementById("switch_to_edit").disabled = false;
648 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
649 document.getElementById("switch_to_admin_enter").disabled = false;
651 if (this.mode.name == 'login') {
652 if (this.login_name) {
653 server.send(['LOGIN', this.login_name]);
655 this.log_msg("? need login name");
657 } else if (this.mode.name == 'play') {
658 if (game.tasks.includes('PICK_UP')) {
659 document.getElementById("take_thing").disabled = false;
661 if (game.tasks.includes('DROP')) {
662 document.getElementById("drop_thing").disabled = false;
664 if (game.tasks.includes('MOVE')) {
666 document.getElementById("teleport").disabled = false;
667 } else if (this.mode.name == 'edit') {
668 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
669 document.getElementById("flatten").disabled = false;
671 document.getElementById("switch_to_annotate").disabled = false;
672 document.getElementById("switch_to_write").disabled = false;
673 document.getElementById("switch_to_portal").disabled = false;
674 document.getElementById("switch_to_password").disabled = false;
675 } else if (this.mode.name == 'admin') {
676 document.getElementById("switch_to_control_pw_type").disabled = false;
677 document.getElementById("switch_to_control_tile_type").disabled = false;
678 } else if (this.mode.name == 'study') {
679 document.getElementById("toggle_map_mode").disabled = false;
680 } else if (this.mode.is_single_char_entry) {
681 this.show_help = true;
682 } else if (this.mode.name == 'admin_enter') {
683 this.log_msg('@ enter admin password:')
684 } else if (this.mode.name == 'control_pw_pw') {
685 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
686 } else if (this.mode.name == 'control_pw_pw') {
687 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
691 offset_links: function(offset, links) {
692 for (let y in links) {
693 let real_y = offset[0] + parseInt(y);
694 if (!this.links[real_y]) {
695 this.links[real_y] = [];
697 for (let link of links[y]) {
698 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
699 this.links[real_y].push(offset_link);
703 restore_input_values: function() {
704 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
705 let info = explorer.info_db[explorer.position];
706 if (info != "(none)") {
707 this.inputEl.value = info;
708 this.recalc_input_lines();
710 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
711 let portal = game.portals[explorer.position]
712 this.inputEl.value = portal;
713 this.recalc_input_lines();
714 } else if (this.mode.name == 'password') {
715 this.inputEl.value = this.password;
716 this.recalc_input_lines();
719 empty_input: function(str) {
720 this.inputEl.value = "";
721 if (this.mode.has_input_prompt) {
722 this.recalc_input_lines();
724 this.height_input = 0;
727 recalc_input_lines: function() {
729 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
730 this.height_input = this.input_lines.length;
732 msg_into_lines_of_width: function(msg, width) {
733 function push_inner_link(y, end_x) {
734 if (!inner_links[y]) {
737 inner_links[y].push([url_start_x, end_x, url]);
739 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
742 for (const match of matches) {
743 const url = match[0];
744 const url_start = match.index;
745 const url_end = match.index + match[0].length;
746 link_data[url_start] = url;
747 url_ends.push(url_end);
751 let inner_links = {};
755 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
756 if (x >= width || msg[i] == "\n") {
758 push_inner_link(y, chunk.length);
764 if (msg[i] == "\n") {
769 if (msg[i] != "\n") {
772 if (i in link_data) {
776 } else if (url_ends.includes(i)) {
777 push_inner_link(y, x);
783 push_inner_link(lines.length - 1, chunk.length);
785 return [lines, inner_links];
787 log_msg: function(msg) {
789 while (this.log.length > 100) {
794 draw_map: function() {
795 let map_lines_split = [];
797 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
798 if (j == game.map_size[1]) {
799 map_lines_split.push(line);
803 if (['edit', 'write', 'control_tile_draw',
804 'control_tile_type'].includes(this.mode.name)) {
805 line.push(game.map[i] + game.map_control[i]);
807 line.push(game.map[i] + ' ');
810 map_lines_split.push(line);
811 if (this.map_mode == 'annotations') {
812 for (const coordinate of explorer.info_hints) {
813 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
815 } else if (this.map_mode == 'all') {
816 for (const p in game.portals) {
817 let coordinate = p.split(',')
818 let original = map_lines_split[coordinate[0]][coordinate[1]];
819 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
821 let used_positions = [];
822 for (const thing_id in game.things) {
823 let t = game.things[thing_id];
824 let symbol = game.thing_types[t.type_];
827 meta_char = t.player_char;
829 if (used_positions.includes(t.position.toString())) {
832 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
833 used_positions.push(t.position.toString());
836 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
837 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
840 if (game.map_geometry == 'Square') {
841 for (let line_split of map_lines_split) {
842 map_lines.push(line_split.join(''));
844 } else if (game.map_geometry == 'Hex') {
846 for (let line_split of map_lines_split) {
847 map_lines.push(' '.repeat(indent) + line_split.join(''));
855 let window_center = [terminal.rows / 2, this.window_width / 2];
856 let player = game.things[game.player_id];
857 let center_position = [player.position[0], player.position[1]];
858 if (tui.mode.shows_info) {
859 center_position = [explorer.position[0], explorer.position[1]];
861 center_position[1] = center_position[1] * 2;
862 let offset = [center_position[0] - window_center[0],
863 center_position[1] - window_center[1]]
864 if (game.map_geometry == 'Hex' && offset[0] % 2) {
867 let term_y = Math.max(0, -offset[0]);
868 let term_x = Math.max(0, -offset[1]);
869 let map_y = Math.max(0, offset[0]);
870 let map_x = Math.max(0, offset[1]);
871 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
872 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
873 terminal.write(term_y, term_x, to_draw);
876 draw_mode_line: function() {
877 let help = 'hit [' + this.keys.help + '] for help';
878 if (this.mode.has_input_prompt) {
879 help = 'enter /help for help';
881 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
883 draw_turn_line: function(n) {
884 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
886 draw_history: function() {
887 let log_display_lines = [];
889 let y_offset_in_log = 0;
890 for (let line of this.log) {
891 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
893 log_display_lines = log_display_lines.concat(new_lines);
894 for (const y in link_data) {
895 const rel_y = y_offset_in_log + parseInt(y);
896 log_links[rel_y] = [];
897 for (let link of link_data[y]) {
898 log_links[rel_y].push(link);
901 y_offset_in_log += new_lines.length;
903 let i = log_display_lines.length - 1;
904 for (let y = terminal.rows - 1 - this.height_input;
905 y >= this.height_header && i >= 0;
907 terminal.write(y, this.window_width, log_display_lines[i]);
909 for (const key of Object.keys(log_links)) {
910 if (parseInt(key) <= i) {
911 delete log_links[key];
914 let offset = [terminal.rows - this.height_input - log_display_lines.length,
916 this.offset_links(offset, log_links);
918 draw_info: function() {
919 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
921 let offset = [this.height_header, this.window_width];
922 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
923 terminal.write(y, offset[1], lines[i]);
925 this.offset_links(offset, link_data);
927 draw_input: function() {
928 if (this.mode.has_input_prompt) {
929 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
930 terminal.write(y, this.window_width, this.input_lines[i]);
934 draw_help: function() {
935 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
936 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
937 if (this.mode.name == 'play') {
938 content += "Available actions:\n";
939 if (game.tasks.includes('MOVE')) {
940 content += "[" + movement_keys_desc + "] – move player\n";
942 if (game.tasks.includes('PICK_UP')) {
943 content += "[" + this.keys.take_thing + "] – pick up thing\n";
945 if (game.tasks.includes('DROP')) {
946 content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
948 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
950 } else if (this.mode.name == 'study') {
951 content += "Available actions:\n";
952 content += '[' + movement_keys_desc + '] – move question mark\n';
953 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
955 } else if (this.mode.name == 'edit') {
956 content += "Available actions:\n";
957 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
958 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
961 } else if (this.mode.name == 'chat') {
962 content += '/nick NAME – re-name yourself to NAME\n';
963 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
964 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
965 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
966 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
968 content += this.mode.list_available_modes();
970 if (!this.mode.has_input_prompt) {
971 start_x = this.window_width
973 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
974 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
975 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
976 terminal.write(y, start_x, lines[i]);
979 full_refresh: function() {
981 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
982 if (this.mode.is_intro) {
986 if (game.turn_complete) {
988 this.draw_turn_line();
990 this.draw_mode_line();
991 if (this.mode.shows_info) {
998 if (this.show_help) {
1010 this.map_control = "";
1011 this.map_size = [0,0];
1012 this.player_id = -1;
1016 get_thing: function(id_, create_if_not_found=false) {
1017 if (id_ in game.things) {
1018 return game.things[id_];
1019 } else if (create_if_not_found) {
1020 let t = new Thing([0,0]);
1021 game.things[id_] = t;
1025 move: function(start_position, direction) {
1026 let target = [start_position[0], start_position[1]];
1027 if (direction == 'LEFT') {
1029 } else if (direction == 'RIGHT') {
1031 } else if (game.map_geometry == 'Square') {
1032 if (direction == 'UP') {
1034 } else if (direction == 'DOWN') {
1037 } else if (game.map_geometry == 'Hex') {
1038 let start_indented = start_position[0] % 2;
1039 if (direction == 'UPLEFT') {
1041 if (!start_indented) {
1044 } else if (direction == 'UPRIGHT') {
1046 if (start_indented) {
1049 } else if (direction == 'DOWNLEFT') {
1051 if (!start_indented) {
1054 } else if (direction == 'DOWNRIGHT') {
1056 if (start_indented) {
1061 if (target[0] < 0 || target[1] < 0 ||
1062 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1067 teleport: function() {
1068 let player = this.get_thing(game.player_id);
1069 if (player.position in this.portals) {
1070 server.reconnect_to(this.portals[player.position]);
1072 terminal.blink_screen();
1073 tui.log_msg('? not standing on portal')
1081 server.init(websocket_location);
1087 move: function(direction) {
1088 let target = game.move(this.position, direction);
1090 this.position = target
1091 if (tui.mode.shows_info) {
1093 } else if (tui.mode.name == 'control_tile_draw') {
1094 this.send_tile_control_command();
1097 terminal.blink_screen();
1100 update_info_db: function(yx, str) {
1101 this.info_db[yx] = str;
1102 if (tui.mode.name == 'study') {
1106 empty_info_db: function() {
1108 this.info_hints = [];
1109 if (tui.mode.name == 'study') {
1113 query_info: function() {
1114 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1116 get_info: function() {
1117 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1118 if (game.fov[position_i] != '.') {
1119 return 'outside field of view';
1122 let terrain_char = game.map[position_i]
1123 let terrain_desc = '?'
1124 if (game.terrains[terrain_char]) {
1125 terrain_desc = game.terrains[terrain_char];
1127 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1128 let protection = game.map_control[position_i];
1129 if (protection == '.') {
1130 protection = 'unprotected';
1132 info += 'PROTECTION: ' + protection + '\n';
1133 for (let t_id in game.things) {
1134 let t = game.things[t_id];
1135 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1136 let symbol = game.thing_types[t.type_];
1137 info += "THING: " + t.type_ + " / " + symbol;
1138 if (t.player_char) {
1139 info += t.player_char;
1142 info += " (" + t.name_ + ")";
1147 if (this.position in game.portals) {
1148 info += "PORTAL: " + game.portals[this.position] + "\n";
1150 if (this.position in this.info_db) {
1151 info += "ANNOTATIONS: " + this.info_db[this.position];
1153 info += 'waiting …';
1157 annotate: function(msg) {
1158 if (msg.length == 0) {
1159 msg = " "; // triggers annotation deletion
1161 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1163 set_portal: function(msg) {
1164 if (msg.length == 0) {
1165 msg = " "; // triggers portal deletion
1167 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1169 send_tile_control_command: function() {
1170 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1174 tui.inputEl.addEventListener('input', (event) => {
1175 if (tui.mode.has_input_prompt) {
1176 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1177 if (tui.inputEl.value.length > max_length) {
1178 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1180 tui.recalc_input_lines();
1181 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1182 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1183 tui.switch_mode('edit');
1184 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1185 tui.tile_control_char = tui.inputEl.value[0];
1186 tui.switch_mode('control_pw_pw');
1187 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1188 tui.tile_control_char = tui.inputEl.value[0];
1189 tui.switch_mode('control_tile_draw');
1193 tui.inputEl.addEventListener('keydown', (event) => {
1194 tui.show_help = false;
1195 if (event.key == 'Enter') {
1196 event.preventDefault();
1198 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1199 tui.show_help = true;
1201 tui.restore_input_values();
1202 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1203 && !tui.mode.is_single_char_entry) {
1204 tui.show_help = true;
1205 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1206 tui.login_name = tui.inputEl.value;
1207 server.send(['LOGIN', tui.inputEl.value]);
1209 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1210 if (tui.inputEl.value.length == 0) {
1211 tui.log_msg('@ aborted');
1213 server.send(['SET_MAP_CONTROL_PASSWORD',
1214 tui.tile_control_char, tui.inputEl.value]);
1216 tui.switch_mode('admin');
1217 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1218 explorer.set_portal(tui.inputEl.value);
1219 tui.switch_mode('edit');
1220 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1221 explorer.annotate(tui.inputEl.value);
1222 tui.switch_mode('edit');
1223 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1224 if (tui.inputEl.value.length == 0) {
1225 tui.inputEl.value = " ";
1227 tui.password = tui.inputEl.value
1228 tui.switch_mode('edit');
1229 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1230 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1231 tui.switch_mode('play');
1232 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1233 let tokens = parser.tokenize(tui.inputEl.value);
1234 if (tokens.length > 0 && tokens[0].length > 0) {
1235 if (tui.inputEl.value[0][0] == '/') {
1236 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1237 tui.switch_mode('play');
1238 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1239 tui.switch_mode('study');
1240 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1241 tui.switch_mode('edit');
1242 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1243 tui.switch_mode('admin_enter');
1244 } else if (tokens[0].slice(1) == 'nick') {
1245 if (tokens.length > 1) {
1246 server.send(['NICK', tokens[1]]);
1248 tui.log_msg('? need new name');
1251 tui.log_msg('? unknown command');
1254 server.send(['ALL', tui.inputEl.value]);
1256 } else if (tui.inputEl.valuelength > 0) {
1257 server.send(['ALL', tui.inputEl.value]);
1260 } else if (tui.mode.name == 'play') {
1261 if (tui.mode.mode_switch_on_key(event)) {
1263 } else if (event.key === tui.keys.take_thing
1264 && game.tasks.includes('PICK_UP')) {
1265 server.send(["TASK:PICK_UP"]);
1266 } else if (event.key === tui.keys.drop_thing
1267 && game.tasks.includes('DROP')) {
1268 server.send(["TASK:DROP"]);
1269 } else if (event.key in tui.movement_keys
1270 && game.tasks.includes('MOVE')) {
1271 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1272 } else if (event.key === tui.keys.teleport) {
1275 } else if (tui.mode.name == 'study') {
1276 if (tui.mode.mode_switch_on_key(event)) {
1278 } else if (event.key in tui.movement_keys) {
1279 explorer.move(tui.movement_keys[event.key]);
1280 } else if (event.key == tui.keys.toggle_map_mode) {
1281 if (tui.map_mode == 'terrain') {
1282 tui.map_mode = 'annotations';
1283 } else if (tui.map_mode == 'annotations') {
1284 tui.map_mode = 'all';
1286 tui.map_mode = 'terrain';
1289 } else if (tui.mode.name == 'control_tile_draw') {
1290 if (tui.mode.mode_switch_on_key(event)) {
1292 } else if (event.key in tui.movement_keys) {
1293 explorer.move(tui.movement_keys[event.key]);
1295 } else if (tui.mode.name == 'admin') {
1296 if (tui.mode.mode_switch_on_key(event)) {
1299 } else if (tui.mode.name == 'edit') {
1300 if (tui.mode.mode_switch_on_key(event)) {
1302 } else if (event.key in tui.movement_keys
1303 && game.tasks.includes('MOVE')) {
1304 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1305 } else if (event.key === tui.keys.flatten
1306 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1307 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1313 rows_selector.addEventListener('input', function() {
1314 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1317 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1318 terminal.initialize();
1321 cols_selector.addEventListener('input', function() {
1322 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1325 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1326 terminal.initialize();
1327 tui.window_width = terminal.cols / 2,
1330 for (let key_selector of key_selectors) {
1331 key_selector.addEventListener('input', function() {
1332 window.localStorage.setItem(key_selector.id, key_selector.value);
1336 window.setInterval(function() {
1337 if (server.connected) {
1338 server.send(['PING']);
1340 server.reconnect_to(server.url);
1341 tui.log_msg('@ attempting reconnect …')
1344 document.getElementById("terminal").onclick = function() {
1345 tui.inputEl.focus();
1347 document.getElementById("help").onclick = function() {
1348 tui.show_help = true;
1351 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1352 const mode = switchEl.id.slice("switch_to_".length);
1353 switchEl.onclick = function() {
1354 tui.switch_mode(mode);
1358 document.getElementById("toggle_map_mode").onclick = function() {
1359 if (tui.map_mode == 'terrain') {
1360 tui.map_mode = 'annotations';
1361 } else if (tui.map_mode == 'annotations') {
1362 tui.map_mode = 'all';
1364 tui.map_mode = 'terrain';
1368 document.getElementById("take_thing").onclick = function() {
1369 server.send(['TASK:PICK_UP']);
1371 document.getElementById("drop_thing").onclick = function() {
1372 server.send(['TASK:DROP']);
1374 document.getElementById("flatten").onclick = function() {
1375 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1377 document.getElementById("teleport").onclick = function() {
1380 document.getElementById("move_upleft").onclick = function() {
1381 if (tui.mode.name == 'play') {
1382 server.send(['TASK:MOVE', 'UPLEFT']);
1384 explorer.move('UPLEFT');
1387 document.getElementById("move_left").onclick = function() {
1388 if (tui.mode.name == 'play') {
1389 server.send(['TASK:MOVE', 'LEFT']);
1391 explorer.move('LEFT');
1394 document.getElementById("move_downleft").onclick = function() {
1395 if (tui.mode.name == 'play') {
1396 server.send(['TASK:MOVE', 'DOWNLEFT']);
1398 explorer.move('DOWNLEFT');
1401 document.getElementById("move_down").onclick = function() {
1402 if (tui.mode.name == 'play') {
1403 server.send(['TASK:MOVE', 'DOWN']);
1405 explorer.move('DOWN');
1408 document.getElementById("move_up").onclick = function() {
1409 if (tui.mode.name == 'play') {
1410 server.send(['TASK:MOVE', 'UP']);
1412 explorer.move('UP');
1415 document.getElementById("move_upright").onclick = function() {
1416 if (tui.mode.name == 'play') {
1417 server.send(['TASK:MOVE', 'UPRIGHT']);
1419 explorer.move('UPRIGHT');
1422 document.getElementById("move_right").onclick = function() {
1423 if (tui.mode.name == 'play') {
1424 server.send(['TASK:MOVE', 'RIGHT']);
1426 explorer.move('RIGHT');
1429 document.getElementById("move_downright").onclick = function() {
1430 if (tui.mode.name == 'play') {
1431 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1433 explorer.move('DOWNRIGHT');