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_type">change tile control password</button>
35 <button id="switch_to_control_tile_type">change tiles control</button>
37 <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 />
39 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
40 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
41 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
42 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
43 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
44 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
45 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
46 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
47 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
48 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
49 <li>help: <input id="key_help" type="text" value="h" />
50 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
51 <li>teleport: <input id="key_teleport" type="text" value="p" />
52 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
53 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
54 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
55 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
56 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
57 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
58 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
59 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
60 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw_type" type="text" value="C" />
61 <li>change tiles control (from play mode): <input id="key_switch_to_control_tile_type" type="text" value="Q" />
62 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
63 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
64 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
69 let websocket_location = "wss://plomlompom.com/rogue_chat/";
70 //let websocket_location = "ws://localhost:8000/";
75 'long': 'This mode allows you to interact with the map.'
79 '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.'},
81 'short': 'terrain edit',
82 '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.'
85 'short': 'change tile control password',
86 '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!'
89 'short': 'change tile control password',
90 '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.'
92 'control_tile_type': {
93 'short': 'change tiles control',
94 '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.'
96 'control_tile_draw': {
97 'short': 'change tiles control',
98 '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'
101 'short': 'annotation',
102 '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.'
105 'short': 'edit portal',
106 '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.'
109 'short': 'chat mode',
110 '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:'
114 'long': 'Pick your player name.'
116 'waiting_for_server': {
117 'short': 'waiting for server response',
118 'long': 'Waiting for a server response.'
121 'short': 'waiting for server response',
122 'long': 'Waiting for a server response.'
125 'short': 'password input',
126 '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.'
129 'short': 'become admin',
130 'long': 'This mode allows you to become admin if you know an admin password.'
134 let rows_selector = document.getElementById("n_rows");
135 let cols_selector = document.getElementById("n_cols");
136 let key_selectors = document.querySelectorAll('[id^="key_"]');
138 function restore_selector_value(selector) {
139 let stored_selection = window.localStorage.getItem(selector.id);
140 if (stored_selection) {
141 selector.value = stored_selection;
144 restore_selector_value(rows_selector);
145 restore_selector_value(cols_selector);
146 for (let key_selector of key_selectors) {
147 restore_selector_value(key_selector);
153 initialize: function() {
154 this.rows = rows_selector.value;
155 this.cols = cols_selector.value;
156 this.pre_el = document.getElementById("terminal");
157 this.pre_el.style.color = this.foreground;
158 this.pre_el.style.backgroundColor = this.background;
161 for (let y = 0, x = 0; y <= this.rows; x++) {
162 if (x == this.cols) {
165 this.content.push(line);
167 if (y == this.rows) {
174 blink_screen: function() {
175 this.pre_el.style.color = this.background;
176 this.pre_el.style.backgroundColor = this.foreground;
178 this.pre_el.style.color = this.foreground;
179 this.pre_el.style.backgroundColor = this.background;
182 refresh: function() {
184 for (let y = 0; y < this.rows; y++) {
185 let line = this.content[y].join('');
186 pre_string += line + '\n';
188 this.pre_el.textContent = pre_string;
190 write: function(start_y, start_x, msg) {
191 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
192 this.content[start_y][x] = msg[i];
195 drawBox: function(start_y, start_x, height, width) {
196 let end_y = start_y + height;
197 let end_x = start_x + width;
198 for (let y = start_y, x = start_x; y < this.rows; x++) {
206 this.content[y][x] = ' ';
210 terminal.initialize();
213 tokenize: function(str) {
218 for (let i = 0; i < str.length; i++) {
224 } else if (c == '\\') {
226 } else if (c == '"') {
231 } else if (c == '"') {
233 } else if (c === ' ') {
234 if (token.length > 0) {
242 if (token.length > 0) {
247 parse_yx: function(position_string) {
248 let coordinate_strings = position_string.split(',')
249 let position = [0, 0];
250 position[0] = parseInt(coordinate_strings[0].slice(2));
251 position[1] = parseInt(coordinate_strings[1].slice(2));
263 init: function(url) {
265 this.websocket = new WebSocket(this.url);
266 this.websocket.onopen = function(event) {
267 server.connected = true;
268 game.thing_types = {};
270 server.send(['TASKS']);
271 server.send(['TERRAINS']);
272 server.send(['THING_TYPES']);
273 tui.log_msg("@ server connected! :)");
274 tui.switch_mode('login');
276 this.websocket.onclose = function(event) {
277 server.connected = false;
278 tui.switch_mode('waiting_for_server');
279 tui.log_msg("@ server disconnected :(");
281 this.websocket.onmessage = this.handle_event;
283 reconnect_to: function(url) {
284 this.websocket.close();
287 send: function(tokens) {
288 this.websocket.send(unparser.untokenize(tokens));
290 handle_event: function(event) {
291 let tokens = parser.tokenize(event.data);
292 if (tokens[0] === 'TURN') {
293 game.turn_complete = false;
294 explorer.empty_info_db();
297 game.turn = parseInt(tokens[1]);
298 } else if (tokens[0] === 'THING') {
299 let t = game.get_thing(tokens[3], true);
300 t.position = parser.parse_yx(tokens[1]);
302 } else if (tokens[0] === 'THING_NAME') {
303 let t = game.get_thing(tokens[1], false);
307 } else if (tokens[0] === 'THING_CHAR') {
308 let t = game.get_thing(tokens[1], false);
310 t.player_char = tokens[2];
312 } else if (tokens[0] === 'TASKS') {
313 game.tasks = tokens[1].split(',');
314 tui.mode_edit.legal = game.tasks.includes('WRITE');
315 } else if (tokens[0] === 'THING_TYPE') {
316 game.thing_types[tokens[1]] = tokens[2]
317 } else if (tokens[0] === 'TERRAIN') {
318 game.terrains[tokens[1]] = tokens[2]
319 } else if (tokens[0] === 'MAP') {
320 game.map_geometry = tokens[1];
322 game.map_size = parser.parse_yx(tokens[2]);
324 } else if (tokens[0] === 'FOV') {
326 } else if (tokens[0] === 'MAP_CONTROL') {
327 game.map_control = tokens[1]
328 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
329 game.turn_complete = true;
330 if (tui.mode.name == 'post_login_wait') {
331 tui.switch_mode('play');
332 } else if (tui.mode.name == 'study') {
333 explorer.query_info();
336 } else if (tokens[0] === 'CHAT') {
337 tui.log_msg('# ' + tokens[1], 1);
338 } else if (tokens[0] === 'PLAYER_ID') {
339 game.player_id = parseInt(tokens[1]);
340 } else if (tokens[0] === 'LOGIN_OK') {
341 this.send(['GET_GAMESTATE']);
342 tui.switch_mode('post_login_wait');
343 } else if (tokens[0] === 'PORTAL') {
344 let position = parser.parse_yx(tokens[1]);
345 game.portals[position] = tokens[2];
346 } else if (tokens[0] === 'ANNOTATION_HINT') {
347 let position = parser.parse_yx(tokens[1]);
348 explorer.info_hints = explorer.info_hints.concat([position]);
349 } else if (tokens[0] === 'ANNOTATION') {
350 let position = parser.parse_yx(tokens[1]);
351 explorer.update_info_db(position, tokens[2]);
352 tui.restore_input_values();
354 } else if (tokens[0] === 'UNHANDLED_INPUT') {
355 tui.log_msg('? unknown command');
356 } else if (tokens[0] === 'PLAY_ERROR') {
357 tui.log_msg('? ' + tokens[1]);
358 terminal.blink_screen();
359 } else if (tokens[0] === 'ARGUMENT_ERROR') {
360 tui.log_msg('? syntax error: ' + tokens[1]);
361 } else if (tokens[0] === 'GAME_ERROR') {
362 tui.log_msg('? game error: ' + tokens[1]);
363 } else if (tokens[0] === 'PONG') {
366 tui.log_msg('? unhandled input: ' + event.data);
372 quote: function(str) {
374 for (let i = 0; i < str.length; i++) {
376 if (['"', '\\'].includes(c)) {
382 return quoted.join('');
384 to_yx: function(yx_coordinate) {
385 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
387 untokenize: function(tokens) {
388 let quoted_tokens = [];
389 for (let token of tokens) {
390 quoted_tokens.push(this.quote(token));
392 return quoted_tokens.join(" ");
397 constructor(name, has_input_prompt=false, shows_info=false,
398 is_intro=false, is_single_char_entry=false) {
400 this.short_desc = mode_helps[name].short;
401 this.available_modes = [];
402 this.has_input_prompt = has_input_prompt;
403 this.shows_info= shows_info;
404 this.is_intro = is_intro;
405 this.help_intro = mode_helps[name].long;
406 this.is_single_char_entry = is_single_char_entry;
409 *iter_available_modes() {
410 for (let mode_name of this.available_modes) {
411 let mode = tui['mode_' + mode_name];
415 let key = tui.keys['switch_to_' + mode.name];
419 list_available_modes() {
421 if (this.available_modes.length > 0) {
422 msg += 'Other modes available from here:\n';
423 for (let [mode, key] of this.iter_available_modes()) {
424 msg += '[' + key + '] – ' + mode.short_desc + '\n';
429 mode_switch_on_key(key_event) {
430 for (let [mode, key] of this.iter_available_modes()) {
431 if (key_event.key == key) {
432 event.preventDefault();
433 tui.switch_mode(mode.name);
444 window_width: terminal.cols / 2,
450 mode_waiting_for_server: new Mode('waiting_for_server',
452 mode_login: new Mode('login', true, false, true),
453 mode_post_login_wait: new Mode('post_login_wait'),
454 mode_chat: new Mode('chat', true),
455 mode_annotate: new Mode('annotate', true, true),
456 mode_play: new Mode('play'),
457 mode_study: new Mode('study', false, true),
458 mode_edit: new Mode('edit', false, false, false, true),
459 mode_control_pw_type: new Mode('control_pw_type',
460 false, false, false, true),
461 mode_portal: new Mode('portal', true, true),
462 mode_password: new Mode('password', true),
463 mode_admin: new Mode('admin', true),
464 mode_control_pw_pw: new Mode('control_pw_pw', true),
465 mode_control_tile_type: new Mode('control_tile_type',
466 false, false, false, true),
467 mode_control_tile_draw: new Mode('control_tile_draw'),
469 this.mode_play.available_modes = ["chat", "study", "edit",
470 "annotate", "portal",
474 this.mode_study.available_modes = ["chat", "play"]
475 this.mode_control_tile_draw.available_modes = ["play"]
476 this.mode = this.mode_waiting_for_server;
477 this.inputEl = document.getElementById("input");
478 this.inputEl.focus();
479 this.recalc_input_lines();
480 this.height_header = this.height_turn_line + this.height_mode_line;
481 this.log_msg("@ waiting for server connection ...");
484 init_keys: function() {
486 for (let key_selector of key_selectors) {
487 this.keys[key_selector.id.slice(4)] = key_selector.value;
489 this.movement_keys = {
490 [this.keys.square_move_up]: 'UP',
491 [this.keys.square_move_left]: 'LEFT',
492 [this.keys.square_move_down]: 'DOWN',
493 [this.keys.square_move_right]: 'RIGHT'
495 if (game.map_geometry == 'Hex') {
496 this.movement_keys = {
497 [this.keys.hex_move_upleft]: 'UPLEFT',
498 [this.keys.hex_move_upright]: 'UPRIGHT',
499 [this.keys.hex_move_right]: 'RIGHT',
500 [this.keys.hex_move_downright]: 'DOWNRIGHT',
501 [this.keys.hex_move_downleft]: 'DOWNLEFT',
502 [this.keys.hex_move_left]: 'LEFT'
506 switch_mode: function(mode_name) {
507 this.inputEl.focus();
508 this.map_mode = 'terrain';
509 this.mode = this['mode_' + mode_name];
510 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
511 explorer.position = game.things[game.player_id].position;
512 if (this.mode.shows_info) {
513 explorer.query_info();
514 } else if (this.mode.name == 'control_tile_draw') {
515 explorer.send_tile_control_command();
516 this.map_mode = 'control';
520 this.restore_input_values();
521 document.getElementById("take_thing").disabled = true;
522 document.getElementById("drop_thing").disabled = true;
523 document.getElementById("flatten").disabled = true;
524 document.getElementById("teleport").disabled = true;
525 document.getElementById("toggle_map_mode").disabled = true;
526 document.getElementById("switch_to_chat").disabled = true;
527 document.getElementById("switch_to_play").disabled = true;
528 document.getElementById("switch_to_study").disabled = true;
529 document.getElementById("switch_to_edit").disabled = true;
530 document.getElementById("switch_to_portal").disabled = true;
531 document.getElementById("switch_to_annotate").disabled = true;
532 document.getElementById("switch_to_password").disabled = true;
533 document.getElementById("switch_to_admin").disabled = true;
534 document.getElementById("switch_to_control_pw_type").disabled = true;
535 document.getElementById("switch_to_control_tile_type").disabled = true;
536 document.getElementById("move_left").disabled = true;
537 document.getElementById("move_upleft").disabled = true;
538 document.getElementById("move_up").disabled = true;
539 document.getElementById("move_upright").disabled = true;
540 document.getElementById("move_downleft").disabled = true;
541 document.getElementById("move_down").disabled = true;
542 document.getElementById("move_downright").disabled = true;
543 document.getElementById("move_right").disabled = true;
544 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
545 document.getElementById("move_left").disabled = false;
546 document.getElementById("move_right").disabled = false;
547 if (game.map_geometry == 'Hex') {
548 document.getElementById("move_upleft").disabled = false;
549 document.getElementById("move_upright").disabled = false;
550 document.getElementById("move_downleft").disabled = false;
551 document.getElementById("move_downright").disabled = false;
553 document.getElementById("move_up").disabled = false;
554 document.getElementById("move_down").disabled = false;
557 if (!this.mode.is_intro && this.mode.name != 'play') {
558 document.getElementById("switch_to_play").disabled = false;
560 if (!this.mode.is_intro && this.mode.name != 'study') {
561 document.getElementById("switch_to_study").disabled = false;
563 if (!this.mode.is_intro && this.mode.name != 'chat') {
564 document.getElementById("switch_to_chat").disabled = false;
566 if (this.mode.name == 'login') {
567 if (this.login_name) {
568 server.send(['LOGIN', this.login_name]);
570 this.log_msg("? need login name");
572 } else if (this.mode.name == 'play') {
573 if (game.tasks.includes('PICK_UP')) {
574 document.getElementById("take_thing").disabled = false;
576 if (game.tasks.includes('DROP')) {
577 document.getElementById("drop_thing").disabled = false;
579 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
580 document.getElementById("flatten").disabled = false;
582 if (game.tasks.includes('MOVE')) {
584 document.getElementById("teleport").disabled = false;
585 document.getElementById("switch_to_annotate").disabled = false;
586 document.getElementById("switch_to_edit").disabled = false;
587 document.getElementById("switch_to_portal").disabled = false;
588 document.getElementById("switch_to_password").disabled = false;
589 document.getElementById("switch_to_admin").disabled = false;
590 document.getElementById("switch_to_control_pw_type").disabled = false;
591 document.getElementById("switch_to_control_tile_type").disabled = false;
592 } else if (this.mode.name == 'study') {
593 document.getElementById("toggle_map_mode").disabled = false;
594 } else if (this.mode.is_single_char_entry) {
595 this.show_help = true;
596 } else if (this.mode.name == 'admin') {
597 this.log_msg('@ enter admin password:')
598 } else if (this.mode.name == 'control_pw_pw') {
599 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
600 } else if (this.mode.name == 'control_pw_pw') {
601 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
605 restore_input_values: function() {
606 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
607 let info = explorer.info_db[explorer.position];
608 if (info != "(none)") {
609 this.inputEl.value = info;
610 this.recalc_input_lines();
612 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
613 let portal = game.portals[explorer.position]
614 this.inputEl.value = portal;
615 this.recalc_input_lines();
616 } else if (this.mode.name == 'password') {
617 this.inputEl.value = this.password;
618 this.recalc_input_lines();
621 empty_input: function(str) {
622 this.inputEl.value = "";
623 if (this.mode.has_input_prompt) {
624 this.recalc_input_lines();
626 this.height_input = 0;
629 recalc_input_lines: function() {
630 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
631 this.height_input = this.input_lines.length;
633 msg_into_lines_of_width: function(msg, width) {
636 for (let i = 0, x = 0; i < msg.length; i++, x++) {
637 if (x >= width || msg[i] == "\n") {
642 if (msg[i] != "\n") {
649 log_msg: function(msg) {
651 while (this.log.length > 100) {
656 draw_map: function() {
657 let map_lines_split = [];
659 let map_content = game.map;
660 if (this.map_mode == 'control') {
661 map_content = game.map_control;
663 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
664 if (j == game.map_size[1]) {
665 map_lines_split.push(line);
669 line.push(map_content[i] + ' ');
671 map_lines_split.push(line);
672 if (this.map_mode == 'annotations') {
673 for (const coordinate of explorer.info_hints) {
674 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
676 } else if (this.map_mode == 'terrain') {
677 for (const p in game.portals) {
678 let coordinate = p.split(',')
679 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
681 let used_positions = [];
682 for (const thing_id in game.things) {
683 let t = game.things[thing_id];
684 let symbol = game.thing_types[t.type_];
687 meta_char = t.player_char;
689 if (used_positions.includes(t.position.toString())) {
692 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
693 used_positions.push(t.position.toString());
696 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
697 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
700 if (game.map_geometry == 'Square') {
701 for (let line_split of map_lines_split) {
702 map_lines.push(line_split.join(''));
704 } else if (game.map_geometry == 'Hex') {
706 for (let line_split of map_lines_split) {
707 map_lines.push(' '.repeat(indent) + line_split.join(''));
715 let window_center = [terminal.rows / 2, this.window_width / 2];
716 let player = game.things[game.player_id];
717 let center_position = [player.position[0], player.position[1]];
718 if (tui.mode.shows_info) {
719 center_position = [explorer.position[0], explorer.position[1]];
721 center_position[1] = center_position[1] * 2;
722 let offset = [center_position[0] - window_center[0],
723 center_position[1] - window_center[1]]
724 if (game.map_geometry == 'Hex' && offset[0] % 2) {
727 let term_y = Math.max(0, -offset[0]);
728 let term_x = Math.max(0, -offset[1]);
729 let map_y = Math.max(0, offset[0]);
730 let map_x = Math.max(0, offset[1]);
731 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
732 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
733 terminal.write(term_y, term_x, to_draw);
736 draw_mode_line: function() {
737 let help = 'hit [' + this.keys.help + '] for help';
738 if (this.mode.has_input_prompt) {
739 help = 'enter /help for help';
741 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
743 draw_turn_line: function(n) {
744 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
746 draw_history: function() {
747 let log_display_lines = [];
748 for (let line of this.log) {
749 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
751 for (let y = terminal.rows - 1 - this.height_input,
752 i = log_display_lines.length - 1;
753 y >= this.height_header && i >= 0;
755 terminal.write(y, this.window_width, log_display_lines[i]);
758 draw_info: function() {
759 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
760 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
761 terminal.write(y, this.window_width, lines[i]);
764 draw_input: function() {
765 if (this.mode.has_input_prompt) {
766 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
767 terminal.write(y, this.window_width, this.input_lines[i]);
771 draw_help: function() {
772 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
773 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
774 if (this.mode.name == 'play') {
775 content += "Available actions:\n";
776 if (game.tasks.includes('MOVE')) {
777 content += "[" + movement_keys_desc + "] – move player\n";
779 if (game.tasks.includes('PICK_UP')) {
780 content += "[" + this.keys.take_thing + "] – take thing under player\n";
782 if (game.tasks.includes('DROP')) {
783 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
785 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
786 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
788 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
790 } else if (this.mode.name == 'study') {
791 content += "Available actions:\n";
792 content += '[' + movement_keys_desc + '] – move question mark\n';
793 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
795 } else if (this.mode.name == 'chat') {
796 content += '/nick NAME – re-name yourself to NAME\n';
797 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
798 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
800 content += this.mode.list_available_modes();
802 if (!this.mode.has_input_prompt) {
803 start_x = this.window_width
805 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
806 let lines = this.msg_into_lines_of_width(content, this.window_width);
807 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
808 terminal.write(y, start_x, lines[i]);
811 full_refresh: function() {
812 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
813 if (this.mode.is_intro) {
817 if (game.turn_complete) {
819 this.draw_turn_line();
821 this.draw_mode_line();
822 if (this.mode.shows_info) {
829 if (this.show_help) {
841 this.map_control = "";
842 this.map_size = [0,0];
847 get_thing: function(id_, create_if_not_found=false) {
848 if (id_ in game.things) {
849 return game.things[id_];
850 } else if (create_if_not_found) {
851 let t = new Thing([0,0]);
852 game.things[id_] = t;
856 move: function(start_position, direction) {
857 let target = [start_position[0], start_position[1]];
858 if (direction == 'LEFT') {
860 } else if (direction == 'RIGHT') {
862 } else if (game.map_geometry == 'Square') {
863 if (direction == 'UP') {
865 } else if (direction == 'DOWN') {
868 } else if (game.map_geometry == 'Hex') {
869 let start_indented = start_position[0] % 2;
870 if (direction == 'UPLEFT') {
872 if (!start_indented) {
875 } else if (direction == 'UPRIGHT') {
877 if (start_indented) {
880 } else if (direction == 'DOWNLEFT') {
882 if (!start_indented) {
885 } else if (direction == 'DOWNRIGHT') {
887 if (start_indented) {
892 if (target[0] < 0 || target[1] < 0 ||
893 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
898 teleport: function() {
899 let player = this.get_thing(game.player_id);
900 if (player.position in this.portals) {
901 server.reconnect_to(this.portals[player.position]);
903 terminal.blink_screen();
904 tui.log_msg('? not standing on portal')
912 server.init(websocket_location);
918 move: function(direction) {
919 let target = game.move(this.position, direction);
921 this.position = target
922 if (tui.mode.shows_info) {
924 } else if (tui.mode.name == 'control_tile_draw') {
925 this.send_tile_control_command();
928 terminal.blink_screen();
931 update_info_db: function(yx, str) {
932 this.info_db[yx] = str;
933 if (tui.mode.name == 'study') {
937 empty_info_db: function() {
939 this.info_hints = [];
940 if (tui.mode.name == 'study') {
944 query_info: function() {
945 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
947 get_info: function() {
948 let position_i = this.position[0] * game.map_size[1] + this.position[1];
949 if (game.fov[position_i] != '.') {
950 return 'outside field of view';
953 let terrain_char = game.map[position_i]
954 let terrain_desc = '?'
955 if (game.terrains[terrain_char]) {
956 terrain_desc = game.terrains[terrain_char];
958 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
959 let protection = game.map_control[position_i];
960 if (protection == '.') {
961 protection = 'unprotected';
963 info += 'PROTECTION: ' + protection + '\n';
964 for (let t_id in game.things) {
965 let t = game.things[t_id];
966 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
967 let symbol = game.thing_types[t.type_];
968 info += "THING: " + t.type_ + " / " + symbol;
970 info += t.player_char;
973 info += " (" + t.name_ + ")";
978 if (this.position in game.portals) {
979 info += "PORTAL: " + game.portals[this.position] + "\n";
981 if (this.position in this.info_db) {
982 info += "ANNOTATIONS: " + this.info_db[this.position];
988 annotate: function(msg) {
989 if (msg.length == 0) {
990 msg = " "; // triggers annotation deletion
992 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
994 set_portal: function(msg) {
995 if (msg.length == 0) {
996 msg = " "; // triggers portal deletion
998 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1000 send_tile_control_command: function() {
1001 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1005 tui.inputEl.addEventListener('input', (event) => {
1006 if (tui.mode.has_input_prompt) {
1007 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1008 if (tui.inputEl.value.length > max_length) {
1009 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1011 tui.recalc_input_lines();
1012 } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
1013 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1014 tui.switch_mode('play');
1015 } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1016 tui.tile_control_char = tui.inputEl.value[0];
1017 tui.switch_mode('control_pw_pw');
1018 } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1019 tui.tile_control_char = tui.inputEl.value[0];
1020 tui.switch_mode('control_tile_draw');
1024 tui.inputEl.addEventListener('keydown', (event) => {
1025 tui.show_help = false;
1026 if (event.key == 'Enter') {
1027 event.preventDefault();
1029 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1030 tui.show_help = true;
1032 tui.restore_input_values();
1033 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1034 && !tui.mode.is_single_char_entry) {
1035 tui.show_help = true;
1036 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1037 tui.login_name = tui.inputEl.value;
1038 server.send(['LOGIN', tui.inputEl.value]);
1040 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1041 if (tui.inputEl.value.length == 0) {
1042 tui.log_msg('@ aborted');
1044 server.send(['SET_MAP_CONTROL_PASSWORD',
1045 tui.tile_control_char, tui.inputEl.value]);
1047 tui.switch_mode('play');
1048 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1049 explorer.set_portal(tui.inputEl.value);
1050 tui.switch_mode('play');
1051 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1052 explorer.annotate(tui.inputEl.value);
1053 tui.switch_mode('play');
1054 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1055 if (tui.inputEl.value.length == 0) {
1056 tui.inputEl.value = " ";
1058 tui.password = tui.inputEl.value
1059 tui.switch_mode('play');
1060 } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1061 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1062 tui.switch_mode('play');
1063 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1064 let tokens = parser.tokenize(tui.inputEl.value);
1065 if (tokens.length > 0 && tokens[0].length > 0) {
1066 if (tui.inputEl.value[0][0] == '/') {
1067 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1068 tui.switch_mode('play');
1069 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1070 tui.switch_mode('study');
1071 } else if (tokens[0].slice(1) == 'nick') {
1072 if (tokens.length > 1) {
1073 server.send(['NICK', tokens[1]]);
1075 tui.log_msg('? need new name');
1078 tui.log_msg('? unknown command');
1081 server.send(['ALL', tui.inputEl.value]);
1083 } else if (tui.inputEl.valuelength > 0) {
1084 server.send(['ALL', tui.inputEl.value]);
1087 } else if (tui.mode.name == 'play') {
1088 if (tui.mode.mode_switch_on_key(event)) {
1090 } else if (event.key === tui.keys.flatten
1091 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1092 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1093 } else if (event.key === tui.keys.take_thing
1094 && game.tasks.includes('PICK_UP')) {
1095 server.send(["TASK:PICK_UP"]);
1096 } else if (event.key === tui.keys.drop_thing
1097 && game.tasks.includes('DROP')) {
1098 server.send(["TASK:DROP"]);
1099 } else if (event.key in tui.movement_keys
1100 && game.tasks.includes('MOVE')) {
1101 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1102 } else if (event.key === tui.keys.teleport) {
1104 } else if (event.key === tui.keys.switch_to_portal) {
1105 event.preventDefault();
1106 tui.switch_mode('portal');
1107 } else if (event.key === tui.keys.switch_to_annotate) {
1108 event.preventDefault();
1109 tui.switch_mode('annotate');
1111 } else if (tui.mode.name == 'study') {
1112 if (tui.mode.mode_switch_on_key(event)) {
1114 } else if (event.key in tui.movement_keys) {
1115 explorer.move(tui.movement_keys[event.key]);
1116 } else if (event.key == tui.keys.toggle_map_mode) {
1117 if (tui.map_mode == 'terrain') {
1118 tui.map_mode = 'annotations';
1119 } else if (tui.map_mode == 'annotations') {
1120 tui.map_mode = 'control';
1122 tui.map_mode = 'terrain';
1125 } else if (tui.mode.name == 'control_tile_draw') {
1126 if (tui.mode.mode_switch_on_key(event)) {
1128 } else if (event.key in tui.movement_keys) {
1129 explorer.move(tui.movement_keys[event.key]);
1135 rows_selector.addEventListener('input', function() {
1136 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1139 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1140 terminal.initialize();
1143 cols_selector.addEventListener('input', function() {
1144 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1147 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1148 terminal.initialize();
1149 tui.window_width = terminal.cols / 2,
1152 for (let key_selector of key_selectors) {
1153 key_selector.addEventListener('input', function() {
1154 window.localStorage.setItem(key_selector.id, key_selector.value);
1158 window.setInterval(function() {
1159 if (server.connected) {
1160 server.send(['PING']);
1162 server.reconnect_to(server.url);
1163 tui.log_msg('@ attempting reconnect …')
1166 document.getElementById("terminal").onclick = function() {
1167 tui.inputEl.focus();
1169 document.getElementById("help").onclick = function() {
1170 tui.show_help = true;
1173 document.getElementById("switch_to_play").onclick = function() {
1174 tui.switch_mode('play');
1177 document.getElementById("switch_to_study").onclick = function() {
1178 tui.switch_mode('study');
1181 document.getElementById("switch_to_chat").onclick = function() {
1182 tui.switch_mode('chat');
1185 document.getElementById("switch_to_password").onclick = function() {
1186 tui.switch_mode('password');
1189 document.getElementById("switch_to_edit").onclick = function() {
1190 tui.switch_mode('edit');
1193 document.getElementById("switch_to_annotate").onclick = function() {
1194 tui.switch_mode('annotate');
1197 document.getElementById("switch_to_portal").onclick = function() {
1198 tui.switch_mode('portal');
1201 document.getElementById("switch_to_admin").onclick = function() {
1202 tui.switch_mode('admin');
1205 document.getElementById("switch_to_control_pw_type").onclick = function() {
1206 tui.switch_mode('control_pw_type');
1209 document.getElementById("switch_to_control_tile_type").onclick = function() {
1210 tui.switch_mode('control_tile_type');
1213 document.getElementById("toggle_map_mode").onclick = function() {
1214 if (tui.map_mode == 'terrain') {
1215 tui.map_mode = 'annotations';
1216 } else if (tui.map_mode == 'annotations') {
1217 tui.map_mode = 'control';
1219 tui.map_mode = 'terrain';
1223 document.getElementById("take_thing").onclick = function() {
1224 server.send(['TASK:PICK_UP']);
1226 document.getElementById("drop_thing").onclick = function() {
1227 server.send(['TASK:DROP']);
1229 document.getElementById("flatten").onclick = function() {
1230 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1232 document.getElementById("teleport").onclick = function() {
1235 document.getElementById("move_upleft").onclick = function() {
1236 if (tui.mode.name == 'play') {
1237 server.send(['TASK:MOVE', 'UPLEFT']);
1239 explorer.move('UPLEFT');
1242 document.getElementById("move_left").onclick = function() {
1243 if (tui.mode.name == 'play') {
1244 server.send(['TASK:MOVE', 'LEFT']);
1246 explorer.move('LEFT');
1249 document.getElementById("move_downleft").onclick = function() {
1250 if (tui.mode.name == 'play') {
1251 server.send(['TASK:MOVE', 'DOWNLEFT']);
1253 explorer.move('DOWNLEFT');
1256 document.getElementById("move_down").onclick = function() {
1257 if (tui.mode.name == 'play') {
1258 server.send(['TASK:MOVE', 'DOWN']);
1260 explorer.move('DOWN');
1263 document.getElementById("move_up").onclick = function() {
1264 if (tui.mode.name == 'play') {
1265 server.send(['TASK:MOVE', 'UP']);
1267 explorer.move('UP');
1270 document.getElementById("move_upright").onclick = function() {
1271 if (tui.mode.name == 'play') {
1272 server.send(['TASK:MOVE', 'UPRIGHT']);
1274 explorer.move('UPRIGHT');
1277 document.getElementById("move_right").onclick = function() {
1278 if (tui.mode.name == 'play') {
1279 server.send(['TASK:MOVE', 'RIGHT']);
1281 explorer.move('RIGHT');
1284 document.getElementById("move_downright").onclick = function() {
1285 if (tui.mode.name == 'play') {
1286 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1288 explorer.move('DOWNRIGHT');