7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
33 <button id="switch_to_admin">become admin</button>
34 <button id="switch_to_control_pw">change tile control password</button>
36 <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 />
38 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
39 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
40 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
41 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
42 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
43 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
44 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
45 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
46 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
47 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
48 <li>help: <input id="key_help" type="text" value="h" />
49 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
50 <li>teleport: <input id="key_teleport" type="text" value="p" />
51 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
52 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
53 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
54 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
55 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
56 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
57 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
58 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
59 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw" type="text" value="C" />
60 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
61 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
62 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
67 let websocket_location = "wss://plomlompom.com/rogue_chat/";
68 //let websocket_location = "ws://localhost:8000/";
71 'play': 'This mode allows you to interact with the map.',
72 'study': '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.',
73 'edit': '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.',
74 'control_pw_type': '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!',
75 'control_pw_pw': '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.',
76 'annotate': '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.',
77 'portal': '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.',
78 'chat': '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:',
79 'login': 'Pick your player name.',
80 'post_login_wait': 'Waiting for a server response.',
81 'password': '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.',
82 'admin': 'This mode allows you to become admin if you know an admin password.'
85 let rows_selector = document.getElementById("n_rows");
86 let cols_selector = document.getElementById("n_cols");
87 let key_selectors = document.querySelectorAll('[id^="key_"]');
89 function restore_selector_value(selector) {
90 let stored_selection = window.localStorage.getItem(selector.id);
91 if (stored_selection) {
92 selector.value = stored_selection;
95 restore_selector_value(rows_selector);
96 restore_selector_value(cols_selector);
97 for (let key_selector of key_selectors) {
98 restore_selector_value(key_selector);
104 initialize: function() {
105 this.rows = rows_selector.value;
106 this.cols = cols_selector.value;
107 this.pre_el = document.getElementById("terminal");
108 this.pre_el.style.color = this.foreground;
109 this.pre_el.style.backgroundColor = this.background;
112 for (let y = 0, x = 0; y <= this.rows; x++) {
113 if (x == this.cols) {
116 this.content.push(line);
118 if (y == this.rows) {
125 blink_screen: function() {
126 this.pre_el.style.color = this.background;
127 this.pre_el.style.backgroundColor = this.foreground;
129 this.pre_el.style.color = this.foreground;
130 this.pre_el.style.backgroundColor = this.background;
133 refresh: function() {
135 for (let y = 0; y < this.rows; y++) {
136 let line = this.content[y].join('');
137 pre_string += line + '\n';
139 this.pre_el.textContent = pre_string;
141 write: function(start_y, start_x, msg) {
142 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
143 this.content[start_y][x] = msg[i];
146 drawBox: function(start_y, start_x, height, width) {
147 let end_y = start_y + height;
148 let end_x = start_x + width;
149 for (let y = start_y, x = start_x; y < this.rows; x++) {
157 this.content[y][x] = ' ';
161 terminal.initialize();
164 tokenize: function(str) {
169 for (let i = 0; i < str.length; i++) {
175 } else if (c == '\\') {
177 } else if (c == '"') {
182 } else if (c == '"') {
184 } else if (c === ' ') {
185 if (token.length > 0) {
193 if (token.length > 0) {
198 parse_yx: function(position_string) {
199 let coordinate_strings = position_string.split(',')
200 let position = [0, 0];
201 position[0] = parseInt(coordinate_strings[0].slice(2));
202 position[1] = parseInt(coordinate_strings[1].slice(2));
214 init: function(url) {
216 this.websocket = new WebSocket(this.url);
217 this.websocket.onopen = function(event) {
218 server.connected = true;
219 game.thing_types = {};
221 server.send(['TASKS']);
222 server.send(['TERRAINS']);
223 server.send(['THING_TYPES']);
224 tui.log_msg("@ server connected! :)");
225 tui.switch_mode('login');
227 this.websocket.onclose = function(event) {
228 server.connected = false;
229 tui.switch_mode('waiting_for_server');
230 tui.log_msg("@ server disconnected :(");
232 this.websocket.onmessage = this.handle_event;
234 reconnect_to: function(url) {
235 this.websocket.close();
238 send: function(tokens) {
239 this.websocket.send(unparser.untokenize(tokens));
241 handle_event: function(event) {
242 let tokens = parser.tokenize(event.data);
243 if (tokens[0] === 'TURN') {
244 game.turn_complete = false;
245 explorer.empty_info_db();
248 game.turn = parseInt(tokens[1]);
249 } else if (tokens[0] === 'THING') {
250 let t = game.get_thing(tokens[3], true);
251 t.position = parser.parse_yx(tokens[1]);
253 } else if (tokens[0] === 'THING_NAME') {
254 let t = game.get_thing(tokens[1], false);
258 } else if (tokens[0] === 'THING_CHAR') {
259 let t = game.get_thing(tokens[1], false);
261 t.player_char = tokens[2];
263 } else if (tokens[0] === 'TASKS') {
264 game.tasks = tokens[1].split(',')
265 } else if (tokens[0] === 'THING_TYPE') {
266 game.thing_types[tokens[1]] = tokens[2]
267 } else if (tokens[0] === 'TERRAIN') {
268 game.terrains[tokens[1]] = tokens[2]
269 } else if (tokens[0] === 'MAP') {
270 game.map_geometry = tokens[1];
272 game.map_size = parser.parse_yx(tokens[2]);
274 } else if (tokens[0] === 'FOV') {
276 } else if (tokens[0] === 'MAP_CONTROL') {
277 game.map_control = tokens[1]
278 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
279 game.turn_complete = true;
280 if (tui.mode.name == 'post_login_wait') {
281 tui.switch_mode('play');
282 } else if (tui.mode.name == 'study') {
283 explorer.query_info();
286 } else if (tokens[0] === 'CHAT') {
287 tui.log_msg('# ' + tokens[1], 1);
288 } else if (tokens[0] === 'PLAYER_ID') {
289 game.player_id = parseInt(tokens[1]);
290 } else if (tokens[0] === 'LOGIN_OK') {
291 this.send(['GET_GAMESTATE']);
292 tui.switch_mode('post_login_wait');
293 } else if (tokens[0] === 'PORTAL') {
294 let position = parser.parse_yx(tokens[1]);
295 game.portals[position] = tokens[2];
296 } else if (tokens[0] === 'ANNOTATION_HINT') {
297 let position = parser.parse_yx(tokens[1]);
298 explorer.info_hints = explorer.info_hints.concat([position]);
299 } else if (tokens[0] === 'ANNOTATION') {
300 let position = parser.parse_yx(tokens[1]);
301 explorer.update_info_db(position, tokens[2]);
302 tui.restore_input_values();
304 } else if (tokens[0] === 'UNHANDLED_INPUT') {
305 tui.log_msg('? unknown command');
306 } else if (tokens[0] === 'PLAY_ERROR') {
307 tui.log_msg('? ' + tokens[1]);
308 terminal.blink_screen();
309 } else if (tokens[0] === 'ARGUMENT_ERROR') {
310 tui.log_msg('? syntax error: ' + tokens[1]);
311 } else if (tokens[0] === 'GAME_ERROR') {
312 tui.log_msg('? game error: ' + tokens[1]);
313 } else if (tokens[0] === 'PONG') {
316 tui.log_msg('? unhandled input: ' + event.data);
322 quote: function(str) {
324 for (let i = 0; i < str.length; i++) {
326 if (['"', '\\'].includes(c)) {
332 return quoted.join('');
334 to_yx: function(yx_coordinate) {
335 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
337 untokenize: function(tokens) {
338 let quoted_tokens = [];
339 for (let token of tokens) {
340 quoted_tokens.push(this.quote(token));
342 return quoted_tokens.join(" ");
347 constructor(name, has_input_prompt=false, shows_info=false,
348 is_intro=false, is_single_char_entry=false) {
350 this.has_input_prompt = has_input_prompt;
351 this.shows_info= shows_info;
352 this.is_intro = is_intro;
353 this.help_intro = mode_helps[name];
354 this.is_single_char_entry = is_single_char_entry;
361 window_width: terminal.cols / 2,
367 mode_waiting_for_server: new Mode('waiting_for_server',
369 mode_login: new Mode('login', true, false, true),
370 mode_post_login_wait: new Mode('post_login_wait'),
371 mode_chat: new Mode('chat', true),
372 mode_annotate: new Mode('annotate', true, true),
373 mode_play: new Mode('play'),
374 mode_study: new Mode('study', false, true),
375 mode_edit: new Mode('edit', false, false, false, true),
376 mode_control_pw_type: new Mode('control_pw_type',
377 false, false, false, true),
378 mode_portal: new Mode('portal', true, true),
379 mode_password: new Mode('password', true),
380 mode_admin: new Mode('admin', true),
381 mode_control_pw_pw: new Mode('control_pw_pw', true),
383 this.mode = this.mode_waiting_for_server;
384 this.inputEl = document.getElementById("input");
385 this.inputEl.focus();
386 this.recalc_input_lines();
387 this.height_header = this.height_turn_line + this.height_mode_line;
388 this.log_msg("@ waiting for server connection ...");
391 init_keys: function() {
393 for (let key_selector of key_selectors) {
394 this.keys[key_selector.id.slice(4)] = key_selector.value;
396 this.movement_keys = {
397 [this.keys.square_move_up]: 'UP',
398 [this.keys.square_move_left]: 'LEFT',
399 [this.keys.square_move_down]: 'DOWN',
400 [this.keys.square_move_right]: 'RIGHT'
402 if (game.map_geometry == 'Hex') {
403 this.movement_keys = {
404 [this.keys.hex_move_upleft]: 'UPLEFT',
405 [this.keys.hex_move_upright]: 'UPRIGHT',
406 [this.keys.hex_move_right]: 'RIGHT',
407 [this.keys.hex_move_downright]: 'DOWNRIGHT',
408 [this.keys.hex_move_downleft]: 'DOWNLEFT',
409 [this.keys.hex_move_left]: 'LEFT'
413 switch_mode: function(mode_name) {
414 this.inputEl.focus();
415 //this.show_help = false;
416 this.map_mode = 'terrain';
417 if (this.mode.shows_info && game.player_id in game.things) {
418 explorer.position = game.things[game.player_id].position;
419 explorer.query_info();
421 this.mode = this['mode_' + mode_name];
423 this.restore_input_values();
424 document.getElementById("take_thing").disabled = true;
425 document.getElementById("drop_thing").disabled = true;
426 document.getElementById("flatten").disabled = true;
427 document.getElementById("teleport").disabled = true;
428 document.getElementById("toggle_map_mode").disabled = true;
429 document.getElementById("switch_to_chat").disabled = true;
430 document.getElementById("switch_to_play").disabled = true;
431 document.getElementById("switch_to_study").disabled = true;
432 document.getElementById("switch_to_edit").disabled = true;
433 document.getElementById("switch_to_portal").disabled = true;
434 document.getElementById("switch_to_annotate").disabled = true;
435 document.getElementById("switch_to_password").disabled = true;
436 document.getElementById("switch_to_admin").disabled = true;
437 document.getElementById("switch_to_control_pw").disabled = true;
438 document.getElementById("move_left").disabled = true;
439 document.getElementById("move_upleft").disabled = true;
440 document.getElementById("move_up").disabled = true;
441 document.getElementById("move_upright").disabled = true;
442 document.getElementById("move_downleft").disabled = true;
443 document.getElementById("move_down").disabled = true;
444 document.getElementById("move_downright").disabled = true;
445 document.getElementById("move_right").disabled = true;
446 if (this.mode.name == 'play' || this.mode.name == 'study') {
447 document.getElementById("move_left").disabled = false;
448 document.getElementById("move_right").disabled = false;
449 if (game.map_geometry == 'Hex') {
450 document.getElementById("move_upleft").disabled = false;
451 document.getElementById("move_upright").disabled = false;
452 document.getElementById("move_downleft").disabled = false;
453 document.getElementById("move_downright").disabled = false;
455 document.getElementById("move_up").disabled = false;
456 document.getElementById("move_down").disabled = false;
459 if (!this.mode.is_intro && this.mode != this.mode_play) {
460 document.getElementById("switch_to_play").disabled = false;
462 if (!this.mode.is_intro && this.mode != this.mode_study) {
463 document.getElementById("switch_to_study").disabled = false;
465 if (!this.mode.is_intro && this.mode != this.mode_chat) {
466 document.getElementById("switch_to_chat").disabled = false;
468 if (this.mode.name == 'login') {
469 if (this.login_name) {
470 server.send(['LOGIN', this.login_name]);
472 this.log_msg("? need login name");
474 } else if (this.mode.name == 'play') {
475 if (game.tasks.includes('PICK_UP')) {
476 document.getElementById("take_thing").disabled = false;
478 if (game.tasks.includes('DROP')) {
479 document.getElementById("drop_thing").disabled = false;
481 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
482 document.getElementById("flatten").disabled = false;
484 if (game.tasks.includes('MOVE')) {
486 document.getElementById("teleport").disabled = false;
487 document.getElementById("switch_to_annotate").disabled = false;
488 document.getElementById("switch_to_edit").disabled = false;
489 document.getElementById("switch_to_portal").disabled = false;
490 document.getElementById("switch_to_password").disabled = false;
491 document.getElementById("switch_to_admin").disabled = false;
492 document.getElementById("switch_to_control_pw").disabled = false;
493 } else if (this.mode.name == 'study') {
494 document.getElementById("toggle_map_mode").disabled = false;
495 } else if (this.mode.is_single_char_entry) {
496 this.show_help = true;
497 } else if (this.mode.name == 'admin') {
498 this.log_msg('@ enter admin password:')
499 } else if (this.mode.name == 'control_pw_pw') {
500 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
504 restore_input_values: function() {
505 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
506 let info = explorer.info_db[explorer.position];
507 if (info != "(none)") {
508 this.inputEl.value = info;
509 this.recalc_input_lines();
511 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
512 let portal = game.portals[explorer.position]
513 this.inputEl.value = portal;
514 this.recalc_input_lines();
515 } else if (this.mode.name == 'password') {
516 this.inputEl.value = this.password;
517 this.recalc_input_lines();
520 empty_input: function(str) {
521 this.inputEl.value = "";
522 if (this.mode.has_input_prompt) {
523 this.recalc_input_lines();
525 this.height_input = 0;
528 recalc_input_lines: function() {
529 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
530 this.height_input = this.input_lines.length;
532 msg_into_lines_of_width: function(msg, width) {
535 for (let i = 0, x = 0; i < msg.length; i++, x++) {
536 if (x >= width || msg[i] == "\n") {
541 if (msg[i] != "\n") {
548 log_msg: function(msg) {
550 while (this.log.length > 100) {
555 draw_map: function() {
556 let map_lines_split = [];
558 let map_content = game.map;
559 if (this.map_mode == 'control') {
560 map_content = game.map_control;
562 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
563 if (j == game.map_size[1]) {
564 map_lines_split.push(line);
568 line.push(map_content[i] + ' ');
570 map_lines_split.push(line);
571 if (this.map_mode == 'annotations') {
572 for (const coordinate of explorer.info_hints) {
573 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
575 } else if (this.map_mode == 'terrain') {
576 for (const p in game.portals) {
577 let coordinate = p.split(',')
578 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
580 let used_positions = [];
581 for (const thing_id in game.things) {
582 let t = game.things[thing_id];
583 let symbol = game.thing_types[t.type_];
586 meta_char = t.player_char;
588 if (used_positions.includes(t.position.toString())) {
591 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
592 used_positions.push(t.position.toString());
595 if (tui.mode.shows_info) {
596 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
599 if (game.map_geometry == 'Square') {
600 for (let line_split of map_lines_split) {
601 map_lines.push(line_split.join(''));
603 } else if (game.map_geometry == 'Hex') {
605 for (let line_split of map_lines_split) {
606 map_lines.push(' '.repeat(indent) + line_split.join(''));
614 let window_center = [terminal.rows / 2, this.window_width / 2];
615 let player = game.things[game.player_id];
616 let center_position = [player.position[0], player.position[1]];
617 if (tui.mode.shows_info) {
618 center_position = [explorer.position[0], explorer.position[1]];
620 center_position[1] = center_position[1] * 2;
621 let offset = [center_position[0] - window_center[0],
622 center_position[1] - window_center[1]]
623 if (game.map_geometry == 'Hex' && offset[0] % 2) {
626 let term_y = Math.max(0, -offset[0]);
627 let term_x = Math.max(0, -offset[1]);
628 let map_y = Math.max(0, offset[0]);
629 let map_x = Math.max(0, offset[1]);
630 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
631 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
632 terminal.write(term_y, term_x, to_draw);
635 draw_mode_line: function() {
636 let help = 'hit [' + this.keys.help + '] for help';
637 if (this.mode.has_input_prompt) {
638 help = 'enter /help for help';
640 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
642 draw_turn_line: function(n) {
643 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
645 draw_history: function() {
646 let log_display_lines = [];
647 for (let line of this.log) {
648 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
650 for (let y = terminal.rows - 1 - this.height_input,
651 i = log_display_lines.length - 1;
652 y >= this.height_header && i >= 0;
654 terminal.write(y, this.window_width, log_display_lines[i]);
657 draw_info: function() {
658 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
659 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
660 terminal.write(y, this.window_width, lines[i]);
663 draw_input: function() {
664 if (this.mode.has_input_prompt) {
665 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
666 terminal.write(y, this.window_width, this.input_lines[i]);
670 draw_help: function() {
671 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
672 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
673 if (this.mode.name == 'play') {
674 content += "Available actions:\n";
675 if (game.tasks.includes('MOVE')) {
676 content += "[" + movement_keys_desc + "] – move player\n";
678 if (game.tasks.includes('PICK_UP')) {
679 content += "[" + this.keys.take_thing + "] – take thing under player\n";
681 if (game.tasks.includes('DROP')) {
682 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
684 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
685 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
687 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
688 content += '\nOther modes available from here:\n';
689 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
690 content += '[' + this.keys.switch_to_study + '] – study mode\n';
691 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
692 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
693 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
694 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
695 content += '[' + this.keys.switch_to_admin + '] – become admin\n';
696 content += '[' + this.keys.switch_to_control_pw + '] – change tile control password\n';
697 } else if (this.mode.name == 'study') {
698 content += "Available actions:\n";
699 content += '[' + movement_keys_desc + '] – move question mark\n';
700 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
701 content += '\nOther modes available from here:\n';
702 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
703 content += '[' + this.keys.switch_to_play + '] – play mode\n';
704 } else if (this.mode.name == 'chat') {
705 content += '/nick NAME – re-name yourself to NAME\n';
706 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
707 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
710 if (!this.mode.has_input_prompt) {
711 start_x = this.window_width
713 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
714 let lines = this.msg_into_lines_of_width(content, this.window_width);
715 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
716 terminal.write(y, start_x, lines[i]);
719 full_refresh: function() {
720 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
721 if (this.mode.is_intro) {
725 if (game.turn_complete) {
727 this.draw_turn_line();
729 this.draw_mode_line();
730 if (this.mode.shows_info) {
737 if (this.show_help) {
749 this.map_control = "";
750 this.map_size = [0,0];
755 get_thing: function(id_, create_if_not_found=false) {
756 if (id_ in game.things) {
757 return game.things[id_];
758 } else if (create_if_not_found) {
759 let t = new Thing([0,0]);
760 game.things[id_] = t;
764 move: function(start_position, direction) {
765 let target = [start_position[0], start_position[1]];
766 if (direction == 'LEFT') {
768 } else if (direction == 'RIGHT') {
770 } else if (game.map_geometry == 'Square') {
771 if (direction == 'UP') {
773 } else if (direction == 'DOWN') {
776 } else if (game.map_geometry == 'Hex') {
777 let start_indented = start_position[0] % 2;
778 if (direction == 'UPLEFT') {
780 if (!start_indented) {
783 } else if (direction == 'UPRIGHT') {
785 if (start_indented) {
788 } else if (direction == 'DOWNLEFT') {
790 if (!start_indented) {
793 } else if (direction == 'DOWNRIGHT') {
795 if (start_indented) {
800 if (target[0] < 0 || target[1] < 0 ||
801 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
806 teleport: function() {
807 let player = this.get_thing(game.player_id);
808 if (player.position in this.portals) {
809 server.reconnect_to(this.portals[player.position]);
811 terminal.blink_screen();
812 tui.log_msg('? not standing on portal')
820 server.init(websocket_location);
826 move: function(direction) {
827 let target = game.move(this.position, direction);
829 this.position = target
832 terminal.blink_screen();
835 update_info_db: function(yx, str) {
836 this.info_db[yx] = str;
837 if (tui.mode.name == 'study') {
841 empty_info_db: function() {
843 this.info_hints = [];
844 if (tui.mode.name == 'study') {
848 query_info: function() {
849 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
851 get_info: function() {
852 let position_i = this.position[0] * game.map_size[1] + this.position[1];
853 if (game.fov[position_i] != '.') {
854 return 'outside field of view';
857 let terrain_char = game.map[position_i]
858 let terrain_desc = '?'
859 if (game.terrains[terrain_char]) {
860 terrain_desc = game.terrains[terrain_char];
862 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
863 let protection = game.map_control[position_i];
864 if (protection == '.') {
865 protection = 'unprotected';
867 info += 'PROTECTION: ' + protection + '\n';
868 for (let t_id in game.things) {
869 let t = game.things[t_id];
870 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
871 let symbol = game.thing_types[t.type_];
872 info += "THING: " + t.type_ + " / " + symbol;
874 info += t.player_char;
877 info += " (" + t.name_ + ")";
882 if (this.position in game.portals) {
883 info += "PORTAL: " + game.portals[this.position] + "\n";
885 if (this.position in this.info_db) {
886 info += "ANNOTATIONS: " + this.info_db[this.position];
892 annotate: function(msg) {
893 if (msg.length == 0) {
894 msg = " "; // triggers annotation deletion
896 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
898 set_portal: function(msg) {
899 if (msg.length == 0) {
900 msg = " "; // triggers portal deletion
902 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
906 tui.inputEl.addEventListener('input', (event) => {
907 if (tui.mode.has_input_prompt) {
908 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
909 if (tui.inputEl.value.length > max_length) {
910 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
912 tui.recalc_input_lines();
913 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
914 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
915 tui.switch_mode('play');
916 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
917 tui.tile_control_char = tui.inputEl.value[0];
918 tui.switch_mode('control_pw_pw');
922 tui.inputEl.addEventListener('keydown', (event) => {
923 tui.show_help = false;
924 if (event.key == 'Enter') {
925 event.preventDefault();
927 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
928 tui.show_help = true;
930 tui.restore_input_values();
931 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
932 && !tui.mode.is_single_char_entry) {
933 tui.show_help = true;
934 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
935 tui.login_name = tui.inputEl.value;
936 server.send(['LOGIN', tui.inputEl.value]);
938 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
939 if (tui.inputEl.value.length == 0) {
940 tui.log_msg('@ aborted');
942 server.send(['SET_MAP_CONTROL_PASSWORD',
943 tui.tile_control_char, tui.inputEl.value]);
945 tui.switch_mode('play');
946 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
947 explorer.set_portal(tui.inputEl.value);
948 tui.switch_mode('play');
949 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
950 explorer.annotate(tui.inputEl.value);
951 tui.switch_mode('play');
952 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
953 if (tui.inputEl.value.length == 0) {
954 tui.inputEl.value = " ";
956 tui.password = tui.inputEl.value
957 tui.switch_mode('play');
958 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
959 server.send(['BECOME_ADMIN', tui.inputEl.value]);
960 tui.switch_mode('play');
961 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
962 let tokens = parser.tokenize(tui.inputEl.value);
963 if (tokens.length > 0 && tokens[0].length > 0) {
964 if (tui.inputEl.value[0][0] == '/') {
965 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
966 tui.switch_mode('play');
967 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
968 tui.switch_mode('study');
969 } else if (tokens[0].slice(1) == 'nick') {
970 if (tokens.length > 1) {
971 server.send(['NICK', tokens[1]]);
973 tui.log_msg('? need new name');
976 tui.log_msg('? unknown command');
979 server.send(['ALL', tui.inputEl.value]);
981 } else if (tui.inputEl.valuelength > 0) {
982 server.send(['ALL', tui.inputEl.value]);
985 } else if (tui.mode.name == 'play') {
986 if (event.key === tui.keys.switch_to_chat) {
987 event.preventDefault();
988 tui.switch_mode('chat');
989 } else if (event.key === tui.keys.switch_to_edit
990 && game.tasks.includes('WRITE')) {
991 event.preventDefault();
992 tui.switch_mode('edit');
993 } else if (event.key === tui.keys.switch_to_study) {
994 tui.switch_mode('study');
995 } else if (event.key === tui.keys.switch_to_admin) {
996 event.preventDefault();
997 tui.switch_mode('admin');
998 } else if (event.key === tui.keys.switch_to_control_pw) {
999 event.preventDefault();
1000 tui.switch_mode('control_pw_type');
1001 } else if (event.key === tui.keys.switch_to_password) {
1002 event.preventDefault();
1003 tui.switch_mode('password');
1004 } else if (event.key === tui.keys.flatten
1005 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1006 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1007 } else if (event.key === tui.keys.take_thing
1008 && game.tasks.includes('PICK_UP')) {
1009 server.send(["TASK:PICK_UP"]);
1010 } else if (event.key === tui.keys.drop_thing
1011 && game.tasks.includes('DROP')) {
1012 server.send(["TASK:DROP"]);
1013 } else if (event.key in tui.movement_keys
1014 && game.tasks.includes('MOVE')) {
1015 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1016 } else if (event.key === tui.keys.teleport) {
1018 } else if (event.key === tui.keys.switch_to_portal) {
1019 event.preventDefault();
1020 tui.switch_mode('portal');
1021 } else if (event.key === tui.keys.switch_to_annotate) {
1022 event.preventDefault();
1023 tui.switch_mode('annotate');
1025 } else if (tui.mode.name == 'study') {
1026 if (event.key === tui.keys.switch_to_chat) {
1027 event.preventDefault();
1028 tui.switch_mode('chat');
1029 } else if (event.key == tui.keys.switch_to_play) {
1030 tui.switch_mode('play');
1031 } else if (event.key in tui.movement_keys) {
1032 explorer.move(tui.movement_keys[event.key]);
1033 } else if (event.key == tui.keys.toggle_map_mode) {
1034 if (tui.map_mode == 'terrain') {
1035 tui.map_mode = 'annotations';
1036 } else if (tui.map_mode == 'annotations') {
1037 tui.map_mode = 'control';
1039 tui.map_mode = 'terrain';
1046 rows_selector.addEventListener('input', function() {
1047 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1050 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1051 terminal.initialize();
1054 cols_selector.addEventListener('input', function() {
1055 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1058 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1059 terminal.initialize();
1060 tui.window_width = terminal.cols / 2,
1063 for (let key_selector of key_selectors) {
1064 key_selector.addEventListener('input', function() {
1065 window.localStorage.setItem(key_selector.id, key_selector.value);
1069 window.setInterval(function() {
1070 if (server.connected) {
1071 server.send(['PING']);
1073 server.reconnect_to(server.url);
1074 tui.log_msg('@ attempting reconnect …')
1077 document.getElementById("terminal").onclick = function() {
1078 tui.inputEl.focus();
1080 document.getElementById("help").onclick = function() {
1081 tui.show_help = true;
1084 document.getElementById("switch_to_play").onclick = function() {
1085 tui.switch_mode('play');
1088 document.getElementById("switch_to_study").onclick = function() {
1089 tui.switch_mode('study');
1092 document.getElementById("switch_to_chat").onclick = function() {
1093 tui.switch_mode('chat');
1096 document.getElementById("switch_to_password").onclick = function() {
1097 tui.switch_mode('password');
1100 document.getElementById("switch_to_edit").onclick = function() {
1101 tui.switch_mode('edit');
1104 document.getElementById("switch_to_annotate").onclick = function() {
1105 tui.switch_mode('annotate');
1108 document.getElementById("switch_to_portal").onclick = function() {
1109 tui.switch_mode('portal');
1112 document.getElementById("switch_to_admin").onclick = function() {
1113 tui.switch_mode('admin');
1116 document.getElementById("switch_to_control_pw").onclick = function() {
1117 tui.switch_mode('control_pw_type');
1120 document.getElementById("toggle_map_mode").onclick = function() {
1121 if (tui.map_mode == 'terrain') {
1122 tui.map_mode = 'annotations';
1123 } else if (tui.map_mode == 'annotations') {
1124 tui.map_mode = 'control';
1126 tui.map_mode = 'terrain';
1130 document.getElementById("take_thing").onclick = function() {
1131 server.send(['TASK:PICK_UP']);
1133 document.getElementById("drop_thing").onclick = function() {
1134 server.send(['TASK:DROP']);
1136 document.getElementById("flatten").onclick = function() {
1137 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1139 document.getElementById("teleport").onclick = function() {
1142 document.getElementById("move_upleft").onclick = function() {
1143 if (tui.mode.name == 'play') {
1144 server.send(['TASK:MOVE', 'UPLEFT']);
1146 explorer.move('UPLEFT');
1149 document.getElementById("move_left").onclick = function() {
1150 if (tui.mode.name == 'play') {
1151 server.send(['TASK:MOVE', 'LEFT']);
1153 explorer.move('LEFT');
1156 document.getElementById("move_downleft").onclick = function() {
1157 if (tui.mode.name == 'play') {
1158 server.send(['TASK:MOVE', 'DOWNLEFT']);
1160 explorer.move('DOWNLEFT');
1163 document.getElementById("move_down").onclick = function() {
1164 if (tui.mode.name == 'play') {
1165 server.send(['TASK:MOVE', 'DOWN']);
1167 explorer.move('DOWN');
1170 document.getElementById("move_up").onclick = function() {
1171 if (tui.mode.name == 'play') {
1172 server.send(['TASK:MOVE', 'UP']);
1174 explorer.move('UP');
1177 document.getElementById("move_upright").onclick = function() {
1178 if (tui.mode.name == 'play') {
1179 server.send(['TASK:MOVE', 'UPRIGHT']);
1181 explorer.move('UPRIGHT');
1184 document.getElementById("move_right").onclick = function() {
1185 if (tui.mode.name == 'play') {
1186 server.send(['TASK:MOVE', 'RIGHT']);
1188 explorer.move('RIGHT');
1191 document.getElementById("move_downright").onclick = function() {
1192 if (tui.mode.name == 'play') {
1193 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1195 explorer.move('DOWNRIGHT');