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 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
819 let used_positions = [];
820 for (const thing_id in game.things) {
821 let t = game.things[thing_id];
822 let symbol = game.thing_types[t.type_];
825 meta_char = t.player_char;
827 if (used_positions.includes(t.position.toString())) {
830 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
831 used_positions.push(t.position.toString());
834 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
835 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
838 if (game.map_geometry == 'Square') {
839 for (let line_split of map_lines_split) {
840 map_lines.push(line_split.join(''));
842 } else if (game.map_geometry == 'Hex') {
844 for (let line_split of map_lines_split) {
845 map_lines.push(' '.repeat(indent) + line_split.join(''));
853 let window_center = [terminal.rows / 2, this.window_width / 2];
854 let player = game.things[game.player_id];
855 let center_position = [player.position[0], player.position[1]];
856 if (tui.mode.shows_info) {
857 center_position = [explorer.position[0], explorer.position[1]];
859 center_position[1] = center_position[1] * 2;
860 let offset = [center_position[0] - window_center[0],
861 center_position[1] - window_center[1]]
862 if (game.map_geometry == 'Hex' && offset[0] % 2) {
865 let term_y = Math.max(0, -offset[0]);
866 let term_x = Math.max(0, -offset[1]);
867 let map_y = Math.max(0, offset[0]);
868 let map_x = Math.max(0, offset[1]);
869 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
870 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
871 terminal.write(term_y, term_x, to_draw);
874 draw_mode_line: function() {
875 let help = 'hit [' + this.keys.help + '] for help';
876 if (this.mode.has_input_prompt) {
877 help = 'enter /help for help';
879 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
881 draw_turn_line: function(n) {
882 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
884 draw_history: function() {
885 let log_display_lines = [];
887 let y_offset_in_log = 0;
888 for (let line of this.log) {
889 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
891 log_display_lines = log_display_lines.concat(new_lines);
892 for (const y in link_data) {
893 const rel_y = y_offset_in_log + parseInt(y);
894 log_links[rel_y] = [];
895 for (let link of link_data[y]) {
896 log_links[rel_y].push(link);
899 y_offset_in_log += new_lines.length;
901 let i = log_display_lines.length - 1;
902 for (let y = terminal.rows - 1 - this.height_input;
903 y >= this.height_header && i >= 0;
905 terminal.write(y, this.window_width, log_display_lines[i]);
907 for (const key of Object.keys(log_links)) {
908 if (parseInt(key) <= i) {
909 delete log_links[key];
912 let offset = [terminal.rows - this.height_input - log_display_lines.length,
914 this.offset_links(offset, log_links);
916 draw_info: function() {
917 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
919 let offset = [this.height_header, this.window_width];
920 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
921 terminal.write(y, offset[1], lines[i]);
923 this.offset_links(offset, link_data);
925 draw_input: function() {
926 if (this.mode.has_input_prompt) {
927 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
928 terminal.write(y, this.window_width, this.input_lines[i]);
932 draw_help: function() {
933 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
934 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
935 if (this.mode.name == 'play') {
936 content += "Available actions:\n";
937 if (game.tasks.includes('MOVE')) {
938 content += "[" + movement_keys_desc + "] – move player\n";
940 if (game.tasks.includes('PICK_UP')) {
941 content += "[" + this.keys.take_thing + "] – take thing under player\n";
943 if (game.tasks.includes('DROP')) {
944 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
946 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
948 } else if (this.mode.name == 'study') {
949 content += "Available actions:\n";
950 content += '[' + movement_keys_desc + '] – move question mark\n';
951 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
953 } else if (this.mode.name == 'edit') {
954 content += "Available actions:\n";
955 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
956 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
959 } else if (this.mode.name == 'chat') {
960 content += '/nick NAME – re-name yourself to NAME\n';
961 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
962 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
963 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
964 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
966 content += this.mode.list_available_modes();
968 if (!this.mode.has_input_prompt) {
969 start_x = this.window_width
971 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
972 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
973 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
974 terminal.write(y, start_x, lines[i]);
977 full_refresh: function() {
979 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
980 if (this.mode.is_intro) {
984 if (game.turn_complete) {
986 this.draw_turn_line();
988 this.draw_mode_line();
989 if (this.mode.shows_info) {
996 if (this.show_help) {
1008 this.map_control = "";
1009 this.map_size = [0,0];
1010 this.player_id = -1;
1014 get_thing: function(id_, create_if_not_found=false) {
1015 if (id_ in game.things) {
1016 return game.things[id_];
1017 } else if (create_if_not_found) {
1018 let t = new Thing([0,0]);
1019 game.things[id_] = t;
1023 move: function(start_position, direction) {
1024 let target = [start_position[0], start_position[1]];
1025 if (direction == 'LEFT') {
1027 } else if (direction == 'RIGHT') {
1029 } else if (game.map_geometry == 'Square') {
1030 if (direction == 'UP') {
1032 } else if (direction == 'DOWN') {
1035 } else if (game.map_geometry == 'Hex') {
1036 let start_indented = start_position[0] % 2;
1037 if (direction == 'UPLEFT') {
1039 if (!start_indented) {
1042 } else if (direction == 'UPRIGHT') {
1044 if (start_indented) {
1047 } else if (direction == 'DOWNLEFT') {
1049 if (!start_indented) {
1052 } else if (direction == 'DOWNRIGHT') {
1054 if (start_indented) {
1059 if (target[0] < 0 || target[1] < 0 ||
1060 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1065 teleport: function() {
1066 let player = this.get_thing(game.player_id);
1067 if (player.position in this.portals) {
1068 server.reconnect_to(this.portals[player.position]);
1070 terminal.blink_screen();
1071 tui.log_msg('? not standing on portal')
1079 server.init(websocket_location);
1085 move: function(direction) {
1086 let target = game.move(this.position, direction);
1088 this.position = target
1089 if (tui.mode.shows_info) {
1091 } else if (tui.mode.name == 'control_tile_draw') {
1092 this.send_tile_control_command();
1095 terminal.blink_screen();
1098 update_info_db: function(yx, str) {
1099 this.info_db[yx] = str;
1100 if (tui.mode.name == 'study') {
1104 empty_info_db: function() {
1106 this.info_hints = [];
1107 if (tui.mode.name == 'study') {
1111 query_info: function() {
1112 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1114 get_info: function() {
1115 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1116 if (game.fov[position_i] != '.') {
1117 return 'outside field of view';
1120 let terrain_char = game.map[position_i]
1121 let terrain_desc = '?'
1122 if (game.terrains[terrain_char]) {
1123 terrain_desc = game.terrains[terrain_char];
1125 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1126 let protection = game.map_control[position_i];
1127 if (protection == '.') {
1128 protection = 'unprotected';
1130 info += 'PROTECTION: ' + protection + '\n';
1131 for (let t_id in game.things) {
1132 let t = game.things[t_id];
1133 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1134 let symbol = game.thing_types[t.type_];
1135 info += "THING: " + t.type_ + " / " + symbol;
1136 if (t.player_char) {
1137 info += t.player_char;
1140 info += " (" + t.name_ + ")";
1145 if (this.position in game.portals) {
1146 info += "PORTAL: " + game.portals[this.position] + "\n";
1148 if (this.position in this.info_db) {
1149 info += "ANNOTATIONS: " + this.info_db[this.position];
1151 info += 'waiting …';
1155 annotate: function(msg) {
1156 if (msg.length == 0) {
1157 msg = " "; // triggers annotation deletion
1159 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1161 set_portal: function(msg) {
1162 if (msg.length == 0) {
1163 msg = " "; // triggers portal deletion
1165 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1167 send_tile_control_command: function() {
1168 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1172 tui.inputEl.addEventListener('input', (event) => {
1173 if (tui.mode.has_input_prompt) {
1174 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1175 if (tui.inputEl.value.length > max_length) {
1176 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1178 tui.recalc_input_lines();
1179 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1180 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1181 tui.switch_mode('edit');
1182 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1183 tui.tile_control_char = tui.inputEl.value[0];
1184 tui.switch_mode('control_pw_pw');
1185 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1186 tui.tile_control_char = tui.inputEl.value[0];
1187 tui.switch_mode('control_tile_draw');
1191 tui.inputEl.addEventListener('keydown', (event) => {
1192 tui.show_help = false;
1193 if (event.key == 'Enter') {
1194 event.preventDefault();
1196 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1197 tui.show_help = true;
1199 tui.restore_input_values();
1200 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1201 && !tui.mode.is_single_char_entry) {
1202 tui.show_help = true;
1203 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1204 tui.login_name = tui.inputEl.value;
1205 server.send(['LOGIN', tui.inputEl.value]);
1207 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1208 if (tui.inputEl.value.length == 0) {
1209 tui.log_msg('@ aborted');
1211 server.send(['SET_MAP_CONTROL_PASSWORD',
1212 tui.tile_control_char, tui.inputEl.value]);
1214 tui.switch_mode('admin');
1215 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1216 explorer.set_portal(tui.inputEl.value);
1217 tui.switch_mode('edit');
1218 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1219 explorer.annotate(tui.inputEl.value);
1220 tui.switch_mode('edit');
1221 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1222 if (tui.inputEl.value.length == 0) {
1223 tui.inputEl.value = " ";
1225 tui.password = tui.inputEl.value
1226 tui.switch_mode('edit');
1227 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1228 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1229 tui.switch_mode('play');
1230 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1231 let tokens = parser.tokenize(tui.inputEl.value);
1232 if (tokens.length > 0 && tokens[0].length > 0) {
1233 if (tui.inputEl.value[0][0] == '/') {
1234 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1235 tui.switch_mode('play');
1236 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1237 tui.switch_mode('study');
1238 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1239 tui.switch_mode('edit');
1240 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1241 tui.switch_mode('admin_enter');
1242 } else if (tokens[0].slice(1) == 'nick') {
1243 if (tokens.length > 1) {
1244 server.send(['NICK', tokens[1]]);
1246 tui.log_msg('? need new name');
1249 tui.log_msg('? unknown command');
1252 server.send(['ALL', tui.inputEl.value]);
1254 } else if (tui.inputEl.valuelength > 0) {
1255 server.send(['ALL', tui.inputEl.value]);
1258 } else if (tui.mode.name == 'play') {
1259 if (tui.mode.mode_switch_on_key(event)) {
1261 } else if (event.key === tui.keys.take_thing
1262 && game.tasks.includes('PICK_UP')) {
1263 server.send(["TASK:PICK_UP"]);
1264 } else if (event.key === tui.keys.drop_thing
1265 && game.tasks.includes('DROP')) {
1266 server.send(["TASK:DROP"]);
1267 } else if (event.key in tui.movement_keys
1268 && game.tasks.includes('MOVE')) {
1269 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1270 } else if (event.key === tui.keys.teleport) {
1273 } else if (tui.mode.name == 'study') {
1274 if (tui.mode.mode_switch_on_key(event)) {
1276 } else if (event.key in tui.movement_keys) {
1277 explorer.move(tui.movement_keys[event.key]);
1278 } else if (event.key == tui.keys.toggle_map_mode) {
1279 if (tui.map_mode == 'terrain') {
1280 tui.map_mode = 'annotations';
1281 } else if (tui.map_mode == 'annotations') {
1282 tui.map_mode = 'control';
1284 tui.map_mode = 'terrain';
1287 } else if (tui.mode.name == 'control_tile_draw') {
1288 if (tui.mode.mode_switch_on_key(event)) {
1290 } else if (event.key in tui.movement_keys) {
1291 explorer.move(tui.movement_keys[event.key]);
1293 } else if (tui.mode.name == 'admin') {
1294 if (tui.mode.mode_switch_on_key(event)) {
1297 } else if (tui.mode.name == 'edit') {
1298 if (tui.mode.mode_switch_on_key(event)) {
1300 } else if (event.key in tui.movement_keys
1301 && game.tasks.includes('MOVE')) {
1302 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1303 } else if (event.key === tui.keys.flatten
1304 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1305 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1311 rows_selector.addEventListener('input', function() {
1312 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1315 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1316 terminal.initialize();
1319 cols_selector.addEventListener('input', function() {
1320 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1323 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1324 terminal.initialize();
1325 tui.window_width = terminal.cols / 2,
1328 for (let key_selector of key_selectors) {
1329 key_selector.addEventListener('input', function() {
1330 window.localStorage.setItem(key_selector.id, key_selector.value);
1334 window.setInterval(function() {
1335 if (server.connected) {
1336 server.send(['PING']);
1338 server.reconnect_to(server.url);
1339 tui.log_msg('@ attempting reconnect …')
1342 document.getElementById("terminal").onclick = function() {
1343 tui.inputEl.focus();
1345 document.getElementById("help").onclick = function() {
1346 tui.show_help = true;
1349 document.getElementById("switch_to_play").onclick = function() {
1350 tui.switch_mode('play');
1353 document.getElementById("switch_to_study").onclick = function() {
1354 tui.switch_mode('study');
1357 document.getElementById("switch_to_chat").onclick = function() {
1358 tui.switch_mode('chat');
1361 document.getElementById("switch_to_password").onclick = function() {
1362 tui.switch_mode('password');
1365 document.getElementById("switch_to_edit").onclick = function() {
1366 tui.switch_mode('edit');
1369 document.getElementById("switch_to_write").onclick = function() {
1370 tui.switch_mode('write');
1373 document.getElementById("switch_to_annotate").onclick = function() {
1374 tui.switch_mode('annotate');
1377 document.getElementById("switch_to_portal").onclick = function() {
1378 tui.switch_mode('portal');
1381 document.getElementById("switch_to_admin_enter").onclick = function() {
1382 tui.switch_mode('admin');
1385 document.getElementById("switch_to_control_pw_type").onclick = function() {
1386 tui.switch_mode('control_pw_type');
1389 document.getElementById("switch_to_control_tile_type").onclick = function() {
1390 tui.switch_mode('control_tile_type');
1393 document.getElementById("toggle_map_mode").onclick = function() {
1394 if (tui.map_mode == 'terrain') {
1395 tui.map_mode = 'annotations';
1396 } else if (tui.map_mode == 'annotations') {
1397 tui.map_mode = 'control';
1399 tui.map_mode = 'terrain';
1403 document.getElementById("take_thing").onclick = function() {
1404 server.send(['TASK:PICK_UP']);
1406 document.getElementById("drop_thing").onclick = function() {
1407 server.send(['TASK:DROP']);
1409 document.getElementById("flatten").onclick = function() {
1410 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1412 document.getElementById("teleport").onclick = function() {
1415 document.getElementById("move_upleft").onclick = function() {
1416 if (tui.mode.name == 'play') {
1417 server.send(['TASK:MOVE', 'UPLEFT']);
1419 explorer.move('UPLEFT');
1422 document.getElementById("move_left").onclick = function() {
1423 if (tui.mode.name == 'play') {
1424 server.send(['TASK:MOVE', 'LEFT']);
1426 explorer.move('LEFT');
1429 document.getElementById("move_downleft").onclick = function() {
1430 if (tui.mode.name == 'play') {
1431 server.send(['TASK:MOVE', 'DOWNLEFT']);
1433 explorer.move('DOWNLEFT');
1436 document.getElementById("move_down").onclick = function() {
1437 if (tui.mode.name == 'play') {
1438 server.send(['TASK:MOVE', 'DOWN']);
1440 explorer.move('DOWN');
1443 document.getElementById("move_up").onclick = function() {
1444 if (tui.mode.name == 'play') {
1445 server.send(['TASK:MOVE', 'UP']);
1447 explorer.move('UP');
1450 document.getElementById("move_upright").onclick = function() {
1451 if (tui.mode.name == 'play') {
1452 server.send(['TASK:MOVE', 'UPRIGHT']);
1454 explorer.move('UPRIGHT');
1457 document.getElementById("move_right").onclick = function() {
1458 if (tui.mode.name == 'play') {
1459 server.send(['TASK:MOVE', 'RIGHT']);
1461 explorer.move('RIGHT');
1464 document.getElementById("move_downright").onclick = function() {
1465 if (tui.mode.name == 'play') {
1466 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1468 explorer.move('DOWNRIGHT');