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/";
70 let rows_selector = document.getElementById("n_rows");
71 let cols_selector = document.getElementById("n_cols");
72 let key_selectors = document.querySelectorAll('[id^="key_"]');
74 function restore_selector_value(selector) {
75 let stored_selection = window.localStorage.getItem(selector.id);
76 if (stored_selection) {
77 selector.value = stored_selection;
80 restore_selector_value(rows_selector);
81 restore_selector_value(cols_selector);
82 for (let key_selector of key_selectors) {
83 restore_selector_value(key_selector);
89 initialize: function() {
90 this.rows = rows_selector.value;
91 this.cols = cols_selector.value;
92 this.pre_el = document.getElementById("terminal");
93 this.pre_el.style.color = this.foreground;
94 this.pre_el.style.backgroundColor = this.background;
97 for (let y = 0, x = 0; y <= this.rows; x++) {
101 this.content.push(line);
103 if (y == this.rows) {
110 blink_screen: function() {
111 this.pre_el.style.color = this.background;
112 this.pre_el.style.backgroundColor = this.foreground;
114 this.pre_el.style.color = this.foreground;
115 this.pre_el.style.backgroundColor = this.background;
118 refresh: function() {
120 for (let y = 0; y < this.rows; y++) {
121 let line = this.content[y].join('');
122 pre_string += line + '\n';
124 this.pre_el.textContent = pre_string;
126 write: function(start_y, start_x, msg) {
127 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
128 this.content[start_y][x] = msg[i];
131 drawBox: function(start_y, start_x, height, width) {
132 let end_y = start_y + height;
133 let end_x = start_x + width;
134 for (let y = start_y, x = start_x; y < this.rows; x++) {
142 this.content[y][x] = ' ';
146 terminal.initialize();
149 tokenize: function(str) {
154 for (let i = 0; i < str.length; i++) {
160 } else if (c == '\\') {
162 } else if (c == '"') {
167 } else if (c == '"') {
169 } else if (c === ' ') {
170 if (token.length > 0) {
178 if (token.length > 0) {
183 parse_yx: function(position_string) {
184 let coordinate_strings = position_string.split(',')
185 let position = [0, 0];
186 position[0] = parseInt(coordinate_strings[0].slice(2));
187 position[1] = parseInt(coordinate_strings[1].slice(2));
199 init: function(url) {
201 this.websocket = new WebSocket(this.url);
202 this.websocket.onopen = function(event) {
203 server.connected = true;
204 game.thing_types = {};
206 server.send(['TASKS']);
207 server.send(['TERRAINS']);
208 server.send(['THING_TYPES']);
209 tui.log_msg("@ server connected! :)");
210 tui.switch_mode(mode_login);
212 this.websocket.onclose = function(event) {
213 server.connected = false;
214 tui.switch_mode(mode_waiting_for_server);
215 tui.log_msg("@ server disconnected :(");
217 this.websocket.onmessage = this.handle_event;
219 reconnect_to: function(url) {
220 this.websocket.close();
223 send: function(tokens) {
224 this.websocket.send(unparser.untokenize(tokens));
226 handle_event: function(event) {
227 let tokens = parser.tokenize(event.data);
228 if (tokens[0] === 'TURN') {
229 game.turn_complete = false;
230 explorer.empty_info_db();
233 game.turn = parseInt(tokens[1]);
234 } else if (tokens[0] === 'THING') {
235 let t = game.get_thing(tokens[3], true);
236 t.position = parser.parse_yx(tokens[1]);
238 } else if (tokens[0] === 'THING_NAME') {
239 let t = game.get_thing(tokens[1], false);
243 } else if (tokens[0] === 'THING_CHAR') {
244 let t = game.get_thing(tokens[1], false);
246 t.player_char = tokens[2];
248 } else if (tokens[0] === 'TASKS') {
249 game.tasks = tokens[1].split(',')
250 } else if (tokens[0] === 'THING_TYPE') {
251 game.thing_types[tokens[1]] = tokens[2]
252 } else if (tokens[0] === 'TERRAIN') {
253 game.terrains[tokens[1]] = tokens[2]
254 } else if (tokens[0] === 'MAP') {
255 game.map_geometry = tokens[1];
257 game.map_size = parser.parse_yx(tokens[2]);
259 } else if (tokens[0] === 'FOV') {
261 } else if (tokens[0] === 'MAP_CONTROL') {
262 game.map_control = tokens[1]
263 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
264 game.turn_complete = true;
265 if (tui.mode == mode_post_login_wait) {
266 tui.switch_mode(mode_play);
267 } else if (tui.mode == mode_study) {
268 explorer.query_info();
271 } else if (tokens[0] === 'CHAT') {
272 tui.log_msg('# ' + tokens[1], 1);
273 } else if (tokens[0] === 'PLAYER_ID') {
274 game.player_id = parseInt(tokens[1]);
275 } else if (tokens[0] === 'LOGIN_OK') {
276 this.send(['GET_GAMESTATE']);
277 tui.switch_mode(mode_post_login_wait);
278 } else if (tokens[0] === 'PORTAL') {
279 let position = parser.parse_yx(tokens[1]);
280 game.portals[position] = tokens[2];
281 } else if (tokens[0] === 'ANNOTATION_HINT') {
282 let position = parser.parse_yx(tokens[1]);
283 explorer.info_hints = explorer.info_hints.concat([position]);
284 } else if (tokens[0] === 'ANNOTATION') {
285 let position = parser.parse_yx(tokens[1]);
286 explorer.update_info_db(position, tokens[2]);
287 tui.restore_input_values();
289 } else if (tokens[0] === 'UNHANDLED_INPUT') {
290 tui.log_msg('? unknown command');
291 } else if (tokens[0] === 'PLAY_ERROR') {
292 tui.log_msg('? ' + tokens[1]);
293 terminal.blink_screen();
294 } else if (tokens[0] === 'ARGUMENT_ERROR') {
295 tui.log_msg('? syntax error: ' + tokens[1]);
296 } else if (tokens[0] === 'GAME_ERROR') {
297 tui.log_msg('? game error: ' + tokens[1]);
298 } else if (tokens[0] === 'PONG') {
301 tui.log_msg('? unhandled input: ' + event.data);
307 quote: function(str) {
309 for (let i = 0; i < str.length; i++) {
311 if (['"', '\\'].includes(c)) {
317 return quoted.join('');
319 to_yx: function(yx_coordinate) {
320 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
322 untokenize: function(tokens) {
323 let quoted_tokens = [];
324 for (let token of tokens) {
325 quoted_tokens.push(this.quote(token));
327 return quoted_tokens.join(" ");
332 constructor(name, help_intro, has_input_prompt=false, shows_info=false,
333 is_intro=false, is_single_char_entry=false) {
335 this.has_input_prompt = has_input_prompt;
336 this.shows_info= shows_info;
337 this.is_intro = is_intro;
338 this.help_intro = help_intro;
339 this.is_single_char_entry = is_single_char_entry;
342 let mode_waiting_for_server = new Mode('waiting_for_server',
343 'Waiting for a server response.',
345 let mode_login = new Mode('login',
346 'Pick your player name.',
348 let mode_post_login_wait = new Mode('waiting for game world',
349 'Waiting for a server response.')
350 let mode_chat = new Mode('chat',
351 '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:',
353 let mode_annotate = new Mode('annotate',
354 '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.',
356 let mode_play = new Mode('play',
357 'This mode allows you to interact with the map.')
358 let mode_study = new Mode('study',
359 '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.',
361 let mode_edit = new Mode('edit',
362 '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.',
363 false, false, false, true);
364 let mode_control_pw_type = new Mode('control_pw_type',
365 '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!',
366 false, false, false, true);
367 let mode_portal = new Mode('portal',
368 '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.',
370 let mode_password = new Mode('password',
371 '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.',
373 let mode_admin = new Mode('admin',
374 'This mode allows you to become admin if you know an admin password.',
376 let mode_control_pw_pw = new Mode('control_pw_pw',
377 '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.',
381 mode: mode_waiting_for_server,
385 window_width: terminal.cols / 2,
392 this.inputEl = document.getElementById("input");
393 this.inputEl.focus();
394 this.recalc_input_lines();
395 this.height_header = this.height_turn_line + this.height_mode_line;
396 this.log_msg("@ waiting for server connection ...");
399 init_keys: function() {
401 for (let key_selector of key_selectors) {
402 this.keys[key_selector.id.slice(4)] = key_selector.value;
404 this.movement_keys = {
405 [this.keys.square_move_up]: 'UP',
406 [this.keys.square_move_left]: 'LEFT',
407 [this.keys.square_move_down]: 'DOWN',
408 [this.keys.square_move_right]: 'RIGHT'
410 if (game.map_geometry == 'Hex') {
411 this.movement_keys = {
412 [this.keys.hex_move_upleft]: 'UPLEFT',
413 [this.keys.hex_move_upright]: 'UPRIGHT',
414 [this.keys.hex_move_right]: 'RIGHT',
415 [this.keys.hex_move_downright]: 'DOWNRIGHT',
416 [this.keys.hex_move_downleft]: 'DOWNLEFT',
417 [this.keys.hex_move_left]: 'LEFT'
421 switch_mode: function(mode) {
422 this.inputEl.focus();
423 //this.show_help = false;
424 this.map_mode = 'terrain';
425 if (mode.shows_info && game.player_id in game.things) {
426 explorer.position = game.things[game.player_id].position;
427 explorer.query_info();
431 this.restore_input_values();
432 document.getElementById("take_thing").disabled = true;
433 document.getElementById("drop_thing").disabled = true;
434 document.getElementById("flatten").disabled = true;
435 document.getElementById("teleport").disabled = true;
436 document.getElementById("toggle_map_mode").disabled = true;
437 document.getElementById("switch_to_chat").disabled = true;
438 document.getElementById("switch_to_play").disabled = true;
439 document.getElementById("switch_to_study").disabled = true;
440 document.getElementById("switch_to_edit").disabled = true;
441 document.getElementById("switch_to_portal").disabled = true;
442 document.getElementById("switch_to_annotate").disabled = true;
443 document.getElementById("switch_to_password").disabled = true;
444 document.getElementById("switch_to_admin").disabled = true;
445 document.getElementById("switch_to_control_pw").disabled = true;
446 document.getElementById("move_left").disabled = true;
447 document.getElementById("move_upleft").disabled = true;
448 document.getElementById("move_up").disabled = true;
449 document.getElementById("move_upright").disabled = true;
450 document.getElementById("move_downleft").disabled = true;
451 document.getElementById("move_down").disabled = true;
452 document.getElementById("move_downright").disabled = true;
453 document.getElementById("move_right").disabled = true;
454 if (mode == mode_play || mode == mode_study) {
455 document.getElementById("move_left").disabled = false;
456 document.getElementById("move_right").disabled = false;
457 if (game.map_geometry == 'Hex') {
458 document.getElementById("move_upleft").disabled = false;
459 document.getElementById("move_upright").disabled = false;
460 document.getElementById("move_downleft").disabled = false;
461 document.getElementById("move_downright").disabled = false;
463 document.getElementById("move_up").disabled = false;
464 document.getElementById("move_down").disabled = false;
467 if (!mode.is_intro && mode != mode_play) {
468 document.getElementById("switch_to_play").disabled = false;
470 if (!mode.is_intro && mode != mode_study) {
471 document.getElementById("switch_to_study").disabled = false;
473 if (!mode.is_intro && mode != mode_chat) {
474 document.getElementById("switch_to_chat").disabled = false;
476 if (mode == mode_login) {
477 if (this.login_name) {
478 server.send(['LOGIN', this.login_name]);
480 this.log_msg("? need login name");
482 } else if (mode == mode_play) {
483 if (game.tasks.includes('PICK_UP')) {
484 document.getElementById("take_thing").disabled = false;
486 if (game.tasks.includes('DROP')) {
487 document.getElementById("drop_thing").disabled = false;
489 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
490 document.getElementById("flatten").disabled = false;
492 if (game.tasks.includes('MOVE')) {
494 document.getElementById("teleport").disabled = false;
495 document.getElementById("switch_to_annotate").disabled = false;
496 document.getElementById("switch_to_edit").disabled = false;
497 document.getElementById("switch_to_portal").disabled = false;
498 document.getElementById("switch_to_password").disabled = false;
499 document.getElementById("switch_to_admin").disabled = false;
500 document.getElementById("switch_to_control_pw").disabled = false;
501 } else if (mode == mode_study) {
502 document.getElementById("toggle_map_mode").disabled = false;
503 } else if (mode.is_single_char_entry) {
504 this.show_help = true;
505 } else if (mode == mode_admin) {
506 this.log_msg('@ enter admin password:')
507 } else if (mode == mode_control_pw_pw) {
508 this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
512 restore_input_values: function() {
513 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
514 let info = explorer.info_db[explorer.position];
515 if (info != "(none)") {
516 this.inputEl.value = info;
517 this.recalc_input_lines();
519 } else if (this.mode == mode_portal && explorer.position in game.portals) {
520 let portal = game.portals[explorer.position]
521 this.inputEl.value = portal;
522 this.recalc_input_lines();
523 } else if (this.mode == mode_password) {
524 this.inputEl.value = this.password;
525 this.recalc_input_lines();
528 empty_input: function(str) {
529 this.inputEl.value = "";
530 if (this.mode.has_input_prompt) {
531 this.recalc_input_lines();
533 this.height_input = 0;
536 recalc_input_lines: function() {
537 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
538 this.height_input = this.input_lines.length;
540 msg_into_lines_of_width: function(msg, width) {
543 for (let i = 0, x = 0; i < msg.length; i++, x++) {
544 if (x >= width || msg[i] == "\n") {
549 if (msg[i] != "\n") {
556 log_msg: function(msg) {
558 while (this.log.length > 100) {
563 draw_map: function() {
564 let map_lines_split = [];
566 let map_content = game.map;
567 if (this.map_mode == 'control') {
568 map_content = game.map_control;
570 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
571 if (j == game.map_size[1]) {
572 map_lines_split.push(line);
576 line.push(map_content[i] + ' ');
578 map_lines_split.push(line);
579 if (this.map_mode == 'annotations') {
580 for (const coordinate of explorer.info_hints) {
581 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
583 } else if (this.map_mode == 'terrain') {
584 for (const p in game.portals) {
585 let coordinate = p.split(',')
586 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
588 let used_positions = [];
589 for (const thing_id in game.things) {
590 let t = game.things[thing_id];
591 let symbol = game.thing_types[t.type_];
594 meta_char = t.player_char;
596 if (used_positions.includes(t.position.toString())) {
599 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
600 used_positions.push(t.position.toString());
603 if (tui.mode.shows_info) {
604 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
607 if (game.map_geometry == 'Square') {
608 for (let line_split of map_lines_split) {
609 map_lines.push(line_split.join(''));
611 } else if (game.map_geometry == 'Hex') {
613 for (let line_split of map_lines_split) {
614 map_lines.push(' '.repeat(indent) + line_split.join(''));
622 let window_center = [terminal.rows / 2, this.window_width / 2];
623 let player = game.things[game.player_id];
624 let center_position = [player.position[0], player.position[1]];
625 if (tui.mode.shows_info) {
626 center_position = [explorer.position[0], explorer.position[1]];
628 center_position[1] = center_position[1] * 2;
629 let offset = [center_position[0] - window_center[0],
630 center_position[1] - window_center[1]]
631 if (game.map_geometry == 'Hex' && offset[0] % 2) {
634 let term_y = Math.max(0, -offset[0]);
635 let term_x = Math.max(0, -offset[1]);
636 let map_y = Math.max(0, offset[0]);
637 let map_x = Math.max(0, offset[1]);
638 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
639 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
640 terminal.write(term_y, term_x, to_draw);
643 draw_mode_line: function() {
644 let help = 'hit [' + this.keys.help + '] for help';
645 if (this.mode.has_input_prompt) {
646 help = 'enter /help for help';
648 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
650 draw_turn_line: function(n) {
651 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
653 draw_history: function() {
654 let log_display_lines = [];
655 for (let line of this.log) {
656 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
658 for (let y = terminal.rows - 1 - this.height_input,
659 i = log_display_lines.length - 1;
660 y >= this.height_header && i >= 0;
662 terminal.write(y, this.window_width, log_display_lines[i]);
665 draw_info: function() {
666 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
667 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
668 terminal.write(y, this.window_width, lines[i]);
671 draw_input: function() {
672 if (this.mode.has_input_prompt) {
673 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
674 terminal.write(y, this.window_width, this.input_lines[i]);
678 draw_help: function() {
679 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
680 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
681 if (this.mode == mode_play) {
682 content += "Available actions:\n";
683 if (game.tasks.includes('MOVE')) {
684 content += "[" + movement_keys_desc + "] – move player\n";
686 if (game.tasks.includes('PICK_UP')) {
687 content += "[" + this.keys.take_thing + "] – take thing under player\n";
689 if (game.tasks.includes('DROP')) {
690 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
692 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
693 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
695 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
696 content += '\nOther modes available from here:\n';
697 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
698 content += '[' + this.keys.switch_to_study + '] – study mode\n';
699 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
700 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
701 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
702 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
703 content += '[' + this.keys.switch_to_admin + '] – become admin\n';
704 content += '[' + this.keys.switch_to_control_pw + '] – change tile control password\n';
705 } else if (this.mode == mode_study) {
706 content += "Available actions:\n";
707 content += '[' + movement_keys_desc + '] – move question mark\n';
708 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
709 content += '\nOther modes available from here:\n';
710 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
711 content += '[' + this.keys.switch_to_play + '] – play mode\n';
712 } else if (this.mode == mode_chat) {
713 content += '/nick NAME – re-name yourself to NAME\n';
714 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
715 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
718 if (!this.mode.has_input_prompt) {
719 start_x = this.window_width
721 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
722 let lines = this.msg_into_lines_of_width(content, this.window_width);
723 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
724 terminal.write(y, start_x, lines[i]);
727 full_refresh: function() {
728 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
729 if (this.mode.is_intro) {
733 if (game.turn_complete) {
735 this.draw_turn_line();
737 this.draw_mode_line();
738 if (this.mode.shows_info) {
745 if (this.show_help) {
757 this.map_control = "";
758 this.map_size = [0,0];
763 get_thing: function(id_, create_if_not_found=false) {
764 if (id_ in game.things) {
765 return game.things[id_];
766 } else if (create_if_not_found) {
767 let t = new Thing([0,0]);
768 game.things[id_] = t;
772 move: function(start_position, direction) {
773 let target = [start_position[0], start_position[1]];
774 if (direction == 'LEFT') {
776 } else if (direction == 'RIGHT') {
778 } else if (game.map_geometry == 'Square') {
779 if (direction == 'UP') {
781 } else if (direction == 'DOWN') {
784 } else if (game.map_geometry == 'Hex') {
785 let start_indented = start_position[0] % 2;
786 if (direction == 'UPLEFT') {
788 if (!start_indented) {
791 } else if (direction == 'UPRIGHT') {
793 if (start_indented) {
796 } else if (direction == 'DOWNLEFT') {
798 if (!start_indented) {
801 } else if (direction == 'DOWNRIGHT') {
803 if (start_indented) {
808 if (target[0] < 0 || target[1] < 0 ||
809 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
814 teleport: function() {
815 let player = this.get_thing(game.player_id);
816 if (player.position in this.portals) {
817 server.reconnect_to(this.portals[player.position]);
819 terminal.blink_screen();
820 tui.log_msg('? not standing on portal')
828 server.init(websocket_location);
834 move: function(direction) {
835 let target = game.move(this.position, direction);
837 this.position = target
840 terminal.blink_screen();
843 update_info_db: function(yx, str) {
844 this.info_db[yx] = str;
845 if (tui.mode == mode_study) {
849 empty_info_db: function() {
851 this.info_hints = [];
852 if (tui.mode == mode_study) {
856 query_info: function() {
857 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
859 get_info: function() {
860 let position_i = this.position[0] * game.map_size[1] + this.position[1];
861 if (game.fov[position_i] != '.') {
862 return 'outside field of view';
865 let terrain_char = game.map[position_i]
866 let terrain_desc = '?'
867 if (game.terrains[terrain_char]) {
868 terrain_desc = game.terrains[terrain_char];
870 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
871 let protection = game.map_control[position_i];
872 if (protection == '.') {
873 protection = 'unprotected';
875 info += 'PROTECTION: ' + protection + '\n';
876 for (let t_id in game.things) {
877 let t = game.things[t_id];
878 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
879 let symbol = game.thing_types[t.type_];
880 info += "THING: " + t.type_ + " / " + symbol;
882 info += t.player_char;
885 info += " (" + t.name_ + ")";
890 if (this.position in game.portals) {
891 info += "PORTAL: " + game.portals[this.position] + "\n";
893 if (this.position in this.info_db) {
894 info += "ANNOTATIONS: " + this.info_db[this.position];
900 annotate: function(msg) {
901 if (msg.length == 0) {
902 msg = " "; // triggers annotation deletion
904 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
906 set_portal: function(msg) {
907 if (msg.length == 0) {
908 msg = " "; // triggers portal deletion
910 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
914 tui.inputEl.addEventListener('input', (event) => {
915 if (tui.mode.has_input_prompt) {
916 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
917 if (tui.inputEl.value.length > max_length) {
918 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
920 tui.recalc_input_lines();
921 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
922 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
923 tui.switch_mode(mode_play);
924 } else if (tui.mode == mode_control_pw_type && tui.inputEl.value.length > 0) {
925 tui.tile_control_char = tui.inputEl.value[0];
926 tui.switch_mode(mode_control_pw_pw);
930 tui.inputEl.addEventListener('keydown', (event) => {
931 tui.show_help = false;
932 if (event.key == 'Enter') {
933 event.preventDefault();
935 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
936 tui.show_help = true;
938 tui.restore_input_values();
939 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
940 && !tui.mode.is_single_char_entry) {
941 tui.show_help = true;
942 } else if (tui.mode == mode_login && event.key == 'Enter') {
943 tui.login_name = tui.inputEl.value;
944 server.send(['LOGIN', tui.inputEl.value]);
946 } else if (tui.mode == mode_control_pw_pw && event.key == 'Enter') {
947 if (tui.inputEl.value.length == 0) {
948 tui.log_msg('@ aborted');
950 server.send(['SET_MAP_CONTROL_PASSWORD',
951 tui.tile_control_char, tui.inputEl.value]);
953 tui.switch_mode(mode_play);
954 } else if (tui.mode == mode_portal && event.key == 'Enter') {
955 explorer.set_portal(tui.inputEl.value);
956 tui.switch_mode(mode_play);
957 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
958 explorer.annotate(tui.inputEl.value);
959 tui.switch_mode(mode_play);
960 } else if (tui.mode == mode_password && event.key == 'Enter') {
961 if (tui.inputEl.value.length == 0) {
962 tui.inputEl.value = " ";
964 tui.password = tui.inputEl.value
965 tui.switch_mode(mode_play);
966 } else if (tui.mode == mode_admin && event.key == 'Enter') {
967 server.send(['BECOME_ADMIN', tui.inputEl.value]);
968 tui.switch_mode(mode_play);
969 } else if (tui.mode == mode_chat && event.key == 'Enter') {
970 let tokens = parser.tokenize(tui.inputEl.value);
971 if (tokens.length > 0 && tokens[0].length > 0) {
972 if (tui.inputEl.value[0][0] == '/') {
973 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
974 tui.switch_mode(mode_play);
975 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
976 tui.switch_mode(mode_study);
977 } else if (tokens[0].slice(1) == 'nick') {
978 if (tokens.length > 1) {
979 server.send(['NICK', tokens[1]]);
981 tui.log_msg('? need new name');
984 tui.log_msg('? unknown command');
987 server.send(['ALL', tui.inputEl.value]);
989 } else if (tui.inputEl.valuelength > 0) {
990 server.send(['ALL', tui.inputEl.value]);
993 } else if (tui.mode == mode_play) {
994 if (event.key === tui.keys.switch_to_chat) {
995 event.preventDefault();
996 tui.switch_mode(mode_chat);
997 } else if (event.key === tui.keys.switch_to_edit
998 && game.tasks.includes('WRITE')) {
999 event.preventDefault();
1000 tui.switch_mode(mode_edit);
1001 } else if (event.key === tui.keys.switch_to_study) {
1002 tui.switch_mode(mode_study);
1003 } else if (event.key === tui.keys.switch_to_admin) {
1004 event.preventDefault();
1005 tui.switch_mode(mode_admin);
1006 } else if (event.key === tui.keys.switch_to_control_pw) {
1007 event.preventDefault();
1008 tui.switch_mode(mode_control_pw_type);
1009 } else if (event.key === tui.keys.switch_to_password) {
1010 event.preventDefault();
1011 tui.switch_mode(mode_password);
1012 } else if (event.key === tui.keys.flatten
1013 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1014 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1015 } else if (event.key === tui.keys.take_thing
1016 && game.tasks.includes('PICK_UP')) {
1017 server.send(["TASK:PICK_UP"]);
1018 } else if (event.key === tui.keys.drop_thing
1019 && game.tasks.includes('DROP')) {
1020 server.send(["TASK:DROP"]);
1021 } else if (event.key in tui.movement_keys
1022 && game.tasks.includes('MOVE')) {
1023 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1024 } else if (event.key === tui.keys.teleport) {
1026 } else if (event.key === tui.keys.switch_to_portal) {
1027 event.preventDefault();
1028 tui.switch_mode(mode_portal);
1029 } else if (event.key === tui.keys.switch_to_annotate) {
1030 event.preventDefault();
1031 tui.switch_mode(mode_annotate);
1033 } else if (tui.mode == mode_study) {
1034 if (event.key === tui.keys.switch_to_chat) {
1035 event.preventDefault();
1036 tui.switch_mode(mode_chat);
1037 } else if (event.key == tui.keys.switch_to_play) {
1038 tui.switch_mode(mode_play);
1039 } else if (event.key in tui.movement_keys) {
1040 explorer.move(tui.movement_keys[event.key]);
1041 } else if (event.key == tui.keys.toggle_map_mode) {
1042 if (tui.map_mode == 'terrain') {
1043 tui.map_mode = 'annotations';
1044 } else if (tui.map_mode == 'annotations') {
1045 tui.map_mode = 'control';
1047 tui.map_mode = 'terrain';
1054 rows_selector.addEventListener('input', function() {
1055 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1058 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1059 terminal.initialize();
1062 cols_selector.addEventListener('input', function() {
1063 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1066 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1067 terminal.initialize();
1068 tui.window_width = terminal.cols / 2,
1071 for (let key_selector of key_selectors) {
1072 key_selector.addEventListener('input', function() {
1073 window.localStorage.setItem(key_selector.id, key_selector.value);
1077 window.setInterval(function() {
1078 if (server.connected) {
1079 server.send(['PING']);
1081 server.reconnect_to(server.url);
1082 tui.log_msg('@ attempting reconnect …')
1085 document.getElementById("terminal").onclick = function() {
1086 tui.inputEl.focus();
1088 document.getElementById("help").onclick = function() {
1089 tui.show_help = true;
1092 document.getElementById("switch_to_play").onclick = function() {
1093 tui.switch_mode(mode_play);
1096 document.getElementById("switch_to_study").onclick = function() {
1097 tui.switch_mode(mode_study);
1100 document.getElementById("switch_to_chat").onclick = function() {
1101 tui.switch_mode(mode_chat);
1104 document.getElementById("switch_to_password").onclick = function() {
1105 tui.switch_mode(mode_password);
1108 document.getElementById("switch_to_edit").onclick = function() {
1109 tui.switch_mode(mode_edit);
1112 document.getElementById("switch_to_annotate").onclick = function() {
1113 tui.switch_mode(mode_annotate);
1116 document.getElementById("switch_to_portal").onclick = function() {
1117 tui.switch_mode(mode_portal);
1120 document.getElementById("switch_to_admin").onclick = function() {
1121 tui.switch_mode(mode_admin);
1124 document.getElementById("switch_to_control_pw").onclick = function() {
1125 tui.switch_mode(mode_control_pw_type);
1128 document.getElementById("toggle_map_mode").onclick = function() {
1129 if (tui.map_mode == 'terrain') {
1130 tui.map_mode = 'annotations';
1131 } else if (tui.map_mode == 'annotations') {
1132 tui.map_mode = 'control';
1134 tui.map_mode = 'terrain';
1138 document.getElementById("take_thing").onclick = function() {
1139 server.send(['TASK:PICK_UP']);
1141 document.getElementById("drop_thing").onclick = function() {
1142 server.send(['TASK:DROP']);
1144 document.getElementById("flatten").onclick = function() {
1145 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1147 document.getElementById("teleport").onclick = function() {
1150 document.getElementById("move_upleft").onclick = function() {
1151 if (tui.mode == mode_play) {
1152 server.send(['TASK:MOVE', 'UPLEFT']);
1154 explorer.move('UPLEFT');
1157 document.getElementById("move_left").onclick = function() {
1158 if (tui.mode == mode_play) {
1159 server.send(['TASK:MOVE', 'LEFT']);
1161 explorer.move('LEFT');
1164 document.getElementById("move_downleft").onclick = function() {
1165 if (tui.mode == mode_play) {
1166 server.send(['TASK:MOVE', 'DOWNLEFT']);
1168 explorer.move('DOWNLEFT');
1171 document.getElementById("move_down").onclick = function() {
1172 if (tui.mode == mode_play) {
1173 server.send(['TASK:MOVE', 'DOWN']);
1175 explorer.move('DOWN');
1178 document.getElementById("move_up").onclick = function() {
1179 if (tui.mode == mode_play) {
1180 server.send(['TASK:MOVE', 'UP']);
1182 explorer.move('UP');
1185 document.getElementById("move_upright").onclick = function() {
1186 if (tui.mode == mode_play) {
1187 server.send(['TASK:MOVE', 'UPRIGHT']);
1189 explorer.move('UPRIGHT');
1192 document.getElementById("move_right").onclick = function() {
1193 if (tui.mode == mode_play) {
1194 server.send(['TASK:MOVE', 'RIGHT']);
1196 explorer.move('RIGHT');
1199 document.getElementById("move_downright").onclick = function() {
1200 if (tui.mode == mode_play) {
1201 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1203 explorer.move('DOWNRIGHT');