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 terrain/annotations/control 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 terrain/annotations/control 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"]
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 = 'terrain';
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 let map_content = game.map;
798 if (this.map_mode == 'control') {
799 map_content = game.map_control;
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 line.push(map_content[i] + ' ');
809 map_lines_split.push(line);
810 if (this.map_mode == 'annotations') {
811 for (const coordinate of explorer.info_hints) {
812 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
814 } else if (this.map_mode == 'terrain') {
815 for (const p in game.portals) {
816 let coordinate = p.split(',')
817 let original = map_lines_split[coordinate[0]][coordinate[1]];
818 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
820 let used_positions = [];
821 for (const thing_id in game.things) {
822 let t = game.things[thing_id];
823 let symbol = game.thing_types[t.type_];
826 meta_char = t.player_char;
828 if (used_positions.includes(t.position.toString())) {
831 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
832 used_positions.push(t.position.toString());
835 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
836 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
839 if (game.map_geometry == 'Square') {
840 for (let line_split of map_lines_split) {
841 map_lines.push(line_split.join(''));
843 } else if (game.map_geometry == 'Hex') {
845 for (let line_split of map_lines_split) {
846 map_lines.push(' '.repeat(indent) + line_split.join(''));
854 let window_center = [terminal.rows / 2, this.window_width / 2];
855 let player = game.things[game.player_id];
856 let center_position = [player.position[0], player.position[1]];
857 if (tui.mode.shows_info) {
858 center_position = [explorer.position[0], explorer.position[1]];
860 center_position[1] = center_position[1] * 2;
861 let offset = [center_position[0] - window_center[0],
862 center_position[1] - window_center[1]]
863 if (game.map_geometry == 'Hex' && offset[0] % 2) {
866 let term_y = Math.max(0, -offset[0]);
867 let term_x = Math.max(0, -offset[1]);
868 let map_y = Math.max(0, offset[0]);
869 let map_x = Math.max(0, offset[1]);
870 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
871 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
872 terminal.write(term_y, term_x, to_draw);
875 draw_mode_line: function() {
876 let help = 'hit [' + this.keys.help + '] for help';
877 if (this.mode.has_input_prompt) {
878 help = 'enter /help for help';
880 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
882 draw_turn_line: function(n) {
883 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
885 draw_history: function() {
886 let log_display_lines = [];
888 let y_offset_in_log = 0;
889 for (let line of this.log) {
890 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
892 log_display_lines = log_display_lines.concat(new_lines);
893 for (const y in link_data) {
894 const rel_y = y_offset_in_log + parseInt(y);
895 log_links[rel_y] = [];
896 for (let link of link_data[y]) {
897 log_links[rel_y].push(link);
900 y_offset_in_log += new_lines.length;
902 let i = log_display_lines.length - 1;
903 for (let y = terminal.rows - 1 - this.height_input;
904 y >= this.height_header && i >= 0;
906 terminal.write(y, this.window_width, log_display_lines[i]);
908 for (const key of Object.keys(log_links)) {
909 if (parseInt(key) <= i) {
910 delete log_links[key];
913 let offset = [terminal.rows - this.height_input - log_display_lines.length,
915 this.offset_links(offset, log_links);
917 draw_info: function() {
918 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
920 let offset = [this.height_header, this.window_width];
921 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
922 terminal.write(y, offset[1], lines[i]);
924 this.offset_links(offset, link_data);
926 draw_input: function() {
927 if (this.mode.has_input_prompt) {
928 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
929 terminal.write(y, this.window_width, this.input_lines[i]);
933 draw_help: function() {
934 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
935 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
936 if (this.mode.name == 'play') {
937 content += "Available actions:\n";
938 if (game.tasks.includes('MOVE')) {
939 content += "[" + movement_keys_desc + "] – move player\n";
941 if (game.tasks.includes('PICK_UP')) {
942 content += "[" + this.keys.take_thing + "] – pick up thing\n";
944 if (game.tasks.includes('DROP')) {
945 content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
947 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
949 } else if (this.mode.name == 'study') {
950 content += "Available actions:\n";
951 content += '[' + movement_keys_desc + '] – move question mark\n';
952 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
954 } else if (this.mode.name == 'edit') {
955 content += "Available actions:\n";
956 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
957 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
960 } else if (this.mode.name == 'chat') {
961 content += '/nick NAME – re-name yourself to NAME\n';
962 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
963 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
964 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
965 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
967 content += this.mode.list_available_modes();
969 if (!this.mode.has_input_prompt) {
970 start_x = this.window_width
972 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
973 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
974 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
975 terminal.write(y, start_x, lines[i]);
978 full_refresh: function() {
980 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
981 if (this.mode.is_intro) {
985 if (game.turn_complete) {
987 this.draw_turn_line();
989 this.draw_mode_line();
990 if (this.mode.shows_info) {
997 if (this.show_help) {
1009 this.map_control = "";
1010 this.map_size = [0,0];
1011 this.player_id = -1;
1015 get_thing: function(id_, create_if_not_found=false) {
1016 if (id_ in game.things) {
1017 return game.things[id_];
1018 } else if (create_if_not_found) {
1019 let t = new Thing([0,0]);
1020 game.things[id_] = t;
1024 move: function(start_position, direction) {
1025 let target = [start_position[0], start_position[1]];
1026 if (direction == 'LEFT') {
1028 } else if (direction == 'RIGHT') {
1030 } else if (game.map_geometry == 'Square') {
1031 if (direction == 'UP') {
1033 } else if (direction == 'DOWN') {
1036 } else if (game.map_geometry == 'Hex') {
1037 let start_indented = start_position[0] % 2;
1038 if (direction == 'UPLEFT') {
1040 if (!start_indented) {
1043 } else if (direction == 'UPRIGHT') {
1045 if (start_indented) {
1048 } else if (direction == 'DOWNLEFT') {
1050 if (!start_indented) {
1053 } else if (direction == 'DOWNRIGHT') {
1055 if (start_indented) {
1060 if (target[0] < 0 || target[1] < 0 ||
1061 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1066 teleport: function() {
1067 let player = this.get_thing(game.player_id);
1068 if (player.position in this.portals) {
1069 server.reconnect_to(this.portals[player.position]);
1071 terminal.blink_screen();
1072 tui.log_msg('? not standing on portal')
1080 server.init(websocket_location);
1086 move: function(direction) {
1087 let target = game.move(this.position, direction);
1089 this.position = target
1090 if (tui.mode.shows_info) {
1092 } else if (tui.mode.name == 'control_tile_draw') {
1093 this.send_tile_control_command();
1096 terminal.blink_screen();
1099 update_info_db: function(yx, str) {
1100 this.info_db[yx] = str;
1101 if (tui.mode.name == 'study') {
1105 empty_info_db: function() {
1107 this.info_hints = [];
1108 if (tui.mode.name == 'study') {
1112 query_info: function() {
1113 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1115 get_info: function() {
1116 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1117 if (game.fov[position_i] != '.') {
1118 return 'outside field of view';
1121 let terrain_char = game.map[position_i]
1122 let terrain_desc = '?'
1123 if (game.terrains[terrain_char]) {
1124 terrain_desc = game.terrains[terrain_char];
1126 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1127 let protection = game.map_control[position_i];
1128 if (protection == '.') {
1129 protection = 'unprotected';
1131 info += 'PROTECTION: ' + protection + '\n';
1132 for (let t_id in game.things) {
1133 let t = game.things[t_id];
1134 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1135 let symbol = game.thing_types[t.type_];
1136 info += "THING: " + t.type_ + " / " + symbol;
1137 if (t.player_char) {
1138 info += t.player_char;
1141 info += " (" + t.name_ + ")";
1146 if (this.position in game.portals) {
1147 info += "PORTAL: " + game.portals[this.position] + "\n";
1149 if (this.position in this.info_db) {
1150 info += "ANNOTATIONS: " + this.info_db[this.position];
1152 info += 'waiting …';
1156 annotate: function(msg) {
1157 if (msg.length == 0) {
1158 msg = " "; // triggers annotation deletion
1160 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1162 set_portal: function(msg) {
1163 if (msg.length == 0) {
1164 msg = " "; // triggers portal deletion
1166 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1168 send_tile_control_command: function() {
1169 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1173 tui.inputEl.addEventListener('input', (event) => {
1174 if (tui.mode.has_input_prompt) {
1175 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1176 if (tui.inputEl.value.length > max_length) {
1177 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1179 tui.recalc_input_lines();
1180 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1181 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1182 tui.switch_mode('edit');
1183 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1184 tui.tile_control_char = tui.inputEl.value[0];
1185 tui.switch_mode('control_pw_pw');
1186 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1187 tui.tile_control_char = tui.inputEl.value[0];
1188 tui.switch_mode('control_tile_draw');
1192 tui.inputEl.addEventListener('keydown', (event) => {
1193 tui.show_help = false;
1194 if (event.key == 'Enter') {
1195 event.preventDefault();
1197 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1198 tui.show_help = true;
1200 tui.restore_input_values();
1201 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1202 && !tui.mode.is_single_char_entry) {
1203 tui.show_help = true;
1204 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1205 tui.login_name = tui.inputEl.value;
1206 server.send(['LOGIN', tui.inputEl.value]);
1208 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1209 if (tui.inputEl.value.length == 0) {
1210 tui.log_msg('@ aborted');
1212 server.send(['SET_MAP_CONTROL_PASSWORD',
1213 tui.tile_control_char, tui.inputEl.value]);
1215 tui.switch_mode('admin');
1216 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1217 explorer.set_portal(tui.inputEl.value);
1218 tui.switch_mode('edit');
1219 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1220 explorer.annotate(tui.inputEl.value);
1221 tui.switch_mode('edit');
1222 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1223 if (tui.inputEl.value.length == 0) {
1224 tui.inputEl.value = " ";
1226 tui.password = tui.inputEl.value
1227 tui.switch_mode('edit');
1228 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1229 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1230 tui.switch_mode('play');
1231 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1232 let tokens = parser.tokenize(tui.inputEl.value);
1233 if (tokens.length > 0 && tokens[0].length > 0) {
1234 if (tui.inputEl.value[0][0] == '/') {
1235 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1236 tui.switch_mode('play');
1237 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1238 tui.switch_mode('study');
1239 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1240 tui.switch_mode('edit');
1241 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1242 tui.switch_mode('admin_enter');
1243 } else if (tokens[0].slice(1) == 'nick') {
1244 if (tokens.length > 1) {
1245 server.send(['NICK', tokens[1]]);
1247 tui.log_msg('? need new name');
1250 tui.log_msg('? unknown command');
1253 server.send(['ALL', tui.inputEl.value]);
1255 } else if (tui.inputEl.valuelength > 0) {
1256 server.send(['ALL', tui.inputEl.value]);
1259 } else if (tui.mode.name == 'play') {
1260 if (tui.mode.mode_switch_on_key(event)) {
1262 } else if (event.key === tui.keys.take_thing
1263 && game.tasks.includes('PICK_UP')) {
1264 server.send(["TASK:PICK_UP"]);
1265 } else if (event.key === tui.keys.drop_thing
1266 && game.tasks.includes('DROP')) {
1267 server.send(["TASK:DROP"]);
1268 } else if (event.key in tui.movement_keys
1269 && game.tasks.includes('MOVE')) {
1270 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1271 } else if (event.key === tui.keys.teleport) {
1274 } else if (tui.mode.name == 'study') {
1275 if (tui.mode.mode_switch_on_key(event)) {
1277 } else if (event.key in tui.movement_keys) {
1278 explorer.move(tui.movement_keys[event.key]);
1279 } else if (event.key == tui.keys.toggle_map_mode) {
1280 if (tui.map_mode == 'terrain') {
1281 tui.map_mode = 'annotations';
1282 } else if (tui.map_mode == 'annotations') {
1283 tui.map_mode = 'control';
1285 tui.map_mode = 'terrain';
1288 } else if (tui.mode.name == 'control_tile_draw') {
1289 if (tui.mode.mode_switch_on_key(event)) {
1291 } else if (event.key in tui.movement_keys) {
1292 explorer.move(tui.movement_keys[event.key]);
1294 } else if (tui.mode.name == 'admin') {
1295 if (tui.mode.mode_switch_on_key(event)) {
1298 } else if (tui.mode.name == 'edit') {
1299 if (tui.mode.mode_switch_on_key(event)) {
1301 } else if (event.key in tui.movement_keys
1302 && game.tasks.includes('MOVE')) {
1303 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1304 } else if (event.key === tui.keys.flatten
1305 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1306 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1312 rows_selector.addEventListener('input', function() {
1313 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1316 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1317 terminal.initialize();
1320 cols_selector.addEventListener('input', function() {
1321 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1324 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1325 terminal.initialize();
1326 tui.window_width = terminal.cols / 2,
1329 for (let key_selector of key_selectors) {
1330 key_selector.addEventListener('input', function() {
1331 window.localStorage.setItem(key_selector.id, key_selector.value);
1335 window.setInterval(function() {
1336 if (server.connected) {
1337 server.send(['PING']);
1339 server.reconnect_to(server.url);
1340 tui.log_msg('@ attempting reconnect …')
1343 document.getElementById("terminal").onclick = function() {
1344 tui.inputEl.focus();
1346 document.getElementById("help").onclick = function() {
1347 tui.show_help = true;
1350 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1351 const mode = switchEl.id.slice("switch_to_".length);
1352 switchEl.onclick = function() {
1353 tui.switch_mode(mode);
1357 document.getElementById("toggle_map_mode").onclick = function() {
1358 if (tui.map_mode == 'terrain') {
1359 tui.map_mode = 'annotations';
1360 } else if (tui.map_mode == 'annotations') {
1361 tui.map_mode = 'control';
1363 tui.map_mode = 'terrain';
1367 document.getElementById("take_thing").onclick = function() {
1368 server.send(['TASK:PICK_UP']);
1370 document.getElementById("drop_thing").onclick = function() {
1371 server.send(['TASK:DROP']);
1373 document.getElementById("flatten").onclick = function() {
1374 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1376 document.getElementById("teleport").onclick = function() {
1379 document.getElementById("move_upleft").onclick = function() {
1380 if (tui.mode.name == 'play') {
1381 server.send(['TASK:MOVE', 'UPLEFT']);
1383 explorer.move('UPLEFT');
1386 document.getElementById("move_left").onclick = function() {
1387 if (tui.mode.name == 'play') {
1388 server.send(['TASK:MOVE', 'LEFT']);
1390 explorer.move('LEFT');
1393 document.getElementById("move_downleft").onclick = function() {
1394 if (tui.mode.name == 'play') {
1395 server.send(['TASK:MOVE', 'DOWNLEFT']);
1397 explorer.move('DOWNLEFT');
1400 document.getElementById("move_down").onclick = function() {
1401 if (tui.mode.name == 'play') {
1402 server.send(['TASK:MOVE', 'DOWN']);
1404 explorer.move('DOWN');
1407 document.getElementById("move_up").onclick = function() {
1408 if (tui.mode.name == 'play') {
1409 server.send(['TASK:MOVE', 'UP']);
1411 explorer.move('UP');
1414 document.getElementById("move_upright").onclick = function() {
1415 if (tui.mode.name == 'play') {
1416 server.send(['TASK:MOVE', 'UPRIGHT']);
1418 explorer.move('UPRIGHT');
1421 document.getElementById("move_right").onclick = function() {
1422 if (tui.mode.name == 'play') {
1423 server.send(['TASK:MOVE', 'RIGHT']);
1425 explorer.move('RIGHT');
1428 document.getElementById("move_downright").onclick = function() {
1429 if (tui.mode.name == 'play') {
1430 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1432 explorer.move('DOWNRIGHT');