7 terminal rows: <input id="n_rows" type="number" step=4 min=8 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=20 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/control view</button>
34 <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 />
36 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
37 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
38 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
39 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
40 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
41 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
42 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
43 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
44 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
45 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
46 <li>help: <input id="key_help" type="text" value="h" />
47 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
48 <li>teleport: <input id="key_teleport" type="text" value="p" />
49 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
50 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
51 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
52 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
53 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
54 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
55 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
56 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
57 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
58 <li>toggle terrain/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
63 let websocket_location = "wss://plomlompom.com/rogue_chat/";
65 let rows_selector = document.getElementById("n_rows");
66 let cols_selector = document.getElementById("n_cols");
67 let key_selectors = document.querySelectorAll('[id^="key_"]');
69 function restore_selector_value(selector) {
70 let stored_selection = window.localStorage.getItem(selector.id);
71 if (stored_selection) {
72 selector.value = stored_selection;
75 restore_selector_value(rows_selector);
76 restore_selector_value(cols_selector);
77 for (let key_selector of key_selectors) {
78 restore_selector_value(key_selector);
84 initialize: function() {
85 this.rows = rows_selector.value;
86 this.cols = cols_selector.value;
87 this.pre_el = document.getElementById("terminal");
88 this.pre_el.style.color = this.foreground;
89 this.pre_el.style.backgroundColor = this.background;
92 for (let y = 0, x = 0; y <= this.rows; x++) {
96 this.content.push(line);
105 blink_screen: function() {
106 this.pre_el.style.color = this.background;
107 this.pre_el.style.backgroundColor = this.foreground;
109 this.pre_el.style.color = this.foreground;
110 this.pre_el.style.backgroundColor = this.background;
113 refresh: function() {
115 for (let y = 0; y < this.rows; y++) {
116 let line = this.content[y].join('');
117 pre_string += line + '\n';
119 this.pre_el.textContent = pre_string;
121 write: function(start_y, start_x, msg) {
122 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
123 this.content[start_y][x] = msg[i];
126 drawBox: function(start_y, start_x, height, width) {
127 let end_y = start_y + height;
128 let end_x = start_x + width;
129 for (let y = start_y, x = start_x; y < this.rows; x++) {
137 this.content[y][x] = ' ';
141 terminal.initialize();
144 tokenize: function(str) {
149 for (let i = 0; i < str.length; i++) {
155 } else if (c == '\\') {
157 } else if (c == '"') {
162 } else if (c == '"') {
164 } else if (c === ' ') {
165 if (token.length > 0) {
173 if (token.length > 0) {
178 parse_yx: function(position_string) {
179 let coordinate_strings = position_string.split(',')
180 let position = [0, 0];
181 position[0] = parseInt(coordinate_strings[0].slice(2));
182 position[1] = parseInt(coordinate_strings[1].slice(2));
194 init: function(url) {
196 this.websocket = new WebSocket(this.url);
197 this.websocket.onopen = function(event) {
198 server.connected = true;
199 game.thing_types = {};
201 server.send(['TASKS']);
202 server.send(['TERRAINS']);
203 server.send(['THING_TYPES']);
204 tui.log_msg("@ server connected! :)");
205 tui.switch_mode(mode_login);
207 this.websocket.onclose = function(event) {
208 server.connected = false;
209 tui.switch_mode(mode_waiting_for_server);
210 tui.log_msg("@ server disconnected :(");
212 this.websocket.onmessage = this.handle_event;
214 reconnect_to: function(url) {
215 this.websocket.close();
218 send: function(tokens) {
219 this.websocket.send(unparser.untokenize(tokens));
221 handle_event: function(event) {
222 let tokens = parser.tokenize(event.data);
223 if (tokens[0] === 'TURN') {
224 game.turn_complete = false;
227 game.turn = parseInt(tokens[1]);
228 } else if (tokens[0] === 'THING') {
229 let t = game.get_thing(tokens[3], true);
230 t.position = parser.parse_yx(tokens[1]);
232 } else if (tokens[0] === 'THING_NAME') {
233 let t = game.get_thing(tokens[1], false);
237 } else if (tokens[0] === 'THING_CHAR') {
238 let t = game.get_thing(tokens[1], false);
240 t.player_char = tokens[2];
242 } else if (tokens[0] === 'TASKS') {
243 game.tasks = tokens[1].split(',')
244 } else if (tokens[0] === 'THING_TYPE') {
245 game.thing_types[tokens[1]] = tokens[2]
246 } else if (tokens[0] === 'TERRAIN') {
247 game.terrains[tokens[1]] = tokens[2]
248 } else if (tokens[0] === 'MAP') {
249 game.map_geometry = tokens[1];
251 game.map_size = parser.parse_yx(tokens[2]);
253 } else if (tokens[0] === 'FOV') {
255 } else if (tokens[0] === 'MAP_CONTROL') {
256 game.map_control = tokens[1]
257 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
258 game.turn_complete = true;
259 explorer.empty_info_db();
260 if (tui.mode == mode_post_login_wait) {
261 tui.switch_mode(mode_play);
262 } else if (tui.mode == mode_study) {
263 explorer.query_info();
266 } else if (tokens[0] === 'CHAT') {
267 tui.log_msg('# ' + tokens[1], 1);
268 } else if (tokens[0] === 'PLAYER_ID') {
269 game.player_id = parseInt(tokens[1]);
270 } else if (tokens[0] === 'LOGIN_OK') {
271 this.send(['GET_GAMESTATE']);
272 tui.switch_mode(mode_post_login_wait);
273 } else if (tokens[0] === 'PORTAL') {
274 let position = parser.parse_yx(tokens[1]);
275 game.portals[position] = tokens[2];
276 } else if (tokens[0] === 'ANNOTATION') {
277 let position = parser.parse_yx(tokens[1]);
278 explorer.update_info_db(position, tokens[2]);
279 } else if (tokens[0] === 'UNHANDLED_INPUT') {
280 tui.log_msg('? unknown command');
281 } else if (tokens[0] === 'PLAY_ERROR') {
282 terminal.blink_screen();
283 } else if (tokens[0] === 'ARGUMENT_ERROR') {
284 tui.log_msg('? syntax error: ' + tokens[1]);
285 } else if (tokens[0] === 'GAME_ERROR') {
286 tui.log_msg('? game error: ' + tokens[1]);
287 } else if (tokens[0] === 'PONG') {
290 tui.log_msg('? unhandled input: ' + event.data);
296 quote: function(str) {
298 for (let i = 0; i < str.length; i++) {
300 if (['"', '\\'].includes(c)) {
306 return quoted.join('');
308 to_yx: function(yx_coordinate) {
309 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
311 untokenize: function(tokens) {
312 let quoted_tokens = [];
313 for (let token of tokens) {
314 quoted_tokens.push(this.quote(token));
316 return quoted_tokens.join(" ");
321 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
323 this.has_input_prompt = has_input_prompt;
324 this.shows_info= shows_info;
325 this.is_intro = is_intro;
326 this.help_intro = help_intro;
329 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
330 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
331 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
332 let mode_chat = new Mode('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:', true, false);
333 let mode_annotate = new Mode('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.', true, true);
334 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
335 let mode_study = new Mode('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.', false, true);
336 let mode_edit = new Mode('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.', false, false);
337 let mode_portal = new Mode('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.', true, true);
338 let mode_password = new Mode('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.', true, false, false);
341 mode: mode_waiting_for_server,
345 window_width: terminal.cols / 2,
352 this.inputEl = document.getElementById("input");
353 this.inputEl.focus();
354 this.recalc_input_lines();
355 this.height_header = this.height_turn_line + this.height_mode_line;
356 this.log_msg("@ waiting for server connection ...");
359 init_keys: function() {
361 for (let key_selector of key_selectors) {
362 this.keys[key_selector.id.slice(4)] = key_selector.value;
364 this.movement_keys = {
365 [this.keys.square_move_up]: 'UP',
366 [this.keys.square_move_left]: 'LEFT',
367 [this.keys.square_move_down]: 'DOWN',
368 [this.keys.square_move_right]: 'RIGHT'
370 if (game.map_geometry == 'Hex') {
371 this.movement_keys = {
372 [this.keys.hex_move_upleft]: 'UPLEFT',
373 [this.keys.hex_move_upright]: 'UPRIGHT',
374 [this.keys.hex_move_right]: 'RIGHT',
375 [this.keys.hex_move_downright]: 'DOWNRIGHT',
376 [this.keys.hex_move_downleft]: 'DOWNLEFT',
377 [this.keys.hex_move_left]: 'LEFT'
381 switch_mode: function(mode) {
382 this.inputEl.focus();
383 this.show_help = false;
384 this.map_mode = 'terrain';
385 if (mode.shows_info && game.player_id in game.things) {
386 explorer.position = game.things[game.player_id].position;
390 this.restore_input_values();
391 document.getElementById("take_thing").disabled = true;
392 document.getElementById("drop_thing").disabled = true;
393 document.getElementById("flatten").disabled = true;
394 document.getElementById("teleport").disabled = true;
395 document.getElementById("toggle_map_mode").disabled = true;
396 document.getElementById("switch_to_chat").disabled = true;
397 document.getElementById("switch_to_play").disabled = true;
398 document.getElementById("switch_to_study").disabled = true;
399 document.getElementById("switch_to_edit").disabled = true;
400 document.getElementById("switch_to_portal").disabled = true;
401 document.getElementById("switch_to_annotate").disabled = true;
402 document.getElementById("switch_to_password").disabled = true;
403 document.getElementById("move_left").disabled = true;
404 document.getElementById("move_upleft").disabled = true;
405 document.getElementById("move_up").disabled = true;
406 document.getElementById("move_upright").disabled = true;
407 document.getElementById("move_downleft").disabled = true;
408 document.getElementById("move_down").disabled = true;
409 document.getElementById("move_downright").disabled = true;
410 document.getElementById("move_right").disabled = true;
411 if (mode == mode_play || mode == mode_study) {
412 document.getElementById("move_left").disabled = false;
413 document.getElementById("move_right").disabled = false;
414 if (game.map_geometry == 'Hex') {
415 document.getElementById("move_upleft").disabled = false;
416 document.getElementById("move_upright").disabled = false;
417 document.getElementById("move_downleft").disabled = false;
418 document.getElementById("move_downright").disabled = false;
420 document.getElementById("move_up").disabled = false;
421 document.getElementById("move_down").disabled = false;
424 if (!mode.is_intro && mode != mode_play) {
425 document.getElementById("switch_to_play").disabled = false;
427 if (!mode.is_intro && mode != mode_study) {
428 document.getElementById("switch_to_study").disabled = false;
430 if (!mode.is_intro && mode != mode_chat) {
431 document.getElementById("switch_to_chat").disabled = false;
433 if (mode == mode_login) {
434 if (this.login_name) {
435 server.send(['LOGIN', this.login_name]);
437 this.log_msg("? need login name");
439 } else if (mode == mode_play) {
440 if (game.tasks.includes('PICK_UP')) {
441 document.getElementById("take_thing").disabled = false;
443 if (game.tasks.includes('DROP')) {
444 document.getElementById("drop_thing").disabled = false;
446 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
447 document.getElementById("flatten").disabled = false;
449 if (game.tasks.includes('MOVE')) {
451 document.getElementById("teleport").disabled = false;
452 document.getElementById("switch_to_annotate").disabled = false;
453 document.getElementById("switch_to_edit").disabled = false;
454 document.getElementById("switch_to_portal").disabled = false;
455 document.getElementById("switch_to_password").disabled = false;
456 } else if (mode == mode_study) {
457 document.getElementById("toggle_map_mode").disabled = false;
458 } else if (mode == mode_edit) {
459 this.show_help = true;
463 restore_input_values: function() {
464 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
465 let info = explorer.info_db[explorer.position];
466 if (info != "(none)") {
467 this.inputEl.value = info;
468 this.recalc_input_lines();
470 } else if (this.mode == mode_portal && explorer.position in game.portals) {
471 let portal = game.portals[explorer.position]
472 this.inputEl.value = portal;
473 this.recalc_input_lines();
474 } else if (this.mode == mode_password) {
475 this.inputEl.value = this.password;
476 this.recalc_input_lines();
479 empty_input: function(str) {
480 this.inputEl.value = "";
481 if (this.mode.has_input_prompt) {
482 this.recalc_input_lines();
484 this.height_input = 0;
487 recalc_input_lines: function() {
488 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
489 this.height_input = this.input_lines.length;
491 msg_into_lines_of_width: function(msg, width) {
494 for (let i = 0, x = 0; i < msg.length; i++, x++) {
495 if (x >= width || msg[i] == "\n") {
500 if (msg[i] != "\n") {
507 log_msg: function(msg) {
509 while (this.log.length > 100) {
514 draw_map: function() {
515 let map_lines_split = [];
517 let map_content = game.map;
518 if (this.map_mode == 'control') {
519 map_content = game.map_control;
521 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
522 if (j == game.map_size[1]) {
523 map_lines_split.push(line);
527 line.push(map_content[i] + ' ');
529 map_lines_split.push(line);
530 if (this.map_mode == 'terrain') {
531 for (const p in game.portals) {
532 let coordinate = p.split(',')
533 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
535 let used_positions = [];
536 for (const thing_id in game.things) {
537 let t = game.things[thing_id];
538 let symbol = game.thing_types[t.type_];
541 meta_char = t.player_char;
543 if (used_positions.includes(t.position.toString())) {
546 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
547 used_positions.push(t.position.toString());
550 if (tui.mode.shows_info) {
551 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
554 if (game.map_geometry == 'Square') {
555 for (let line_split of map_lines_split) {
556 map_lines.push(line_split.join(''));
558 } else if (game.map_geometry == 'Hex') {
560 for (let line_split of map_lines_split) {
561 map_lines.push(' '.repeat(indent) + line_split.join(''));
569 let window_center = [terminal.rows / 2, this.window_width / 2];
570 let player = game.things[game.player_id];
571 let center_position = [player.position[0], player.position[1]];
572 if (tui.mode.shows_info) {
573 center_position = [explorer.position[0], explorer.position[1]];
575 center_position[1] = center_position[1] * 2;
576 let offset = [center_position[0] - window_center[0],
577 center_position[1] - window_center[1]]
578 if (game.map_geometry == 'Hex' && offset[0] % 2) {
581 let term_y = Math.max(0, -offset[0]);
582 let term_x = Math.max(0, -offset[1]);
583 let map_y = Math.max(0, offset[0]);
584 let map_x = Math.max(0, offset[1]);
585 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
586 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
587 terminal.write(term_y, term_x, to_draw);
590 draw_mode_line: function() {
591 let help = 'hit [' + this.keys.help + '] for help';
592 if (this.mode.has_input_prompt) {
593 help = 'enter /help for help';
595 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
597 draw_turn_line: function(n) {
598 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
600 draw_history: function() {
601 let log_display_lines = [];
602 for (let line of this.log) {
603 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
605 for (let y = terminal.rows - 1 - this.height_input,
606 i = log_display_lines.length - 1;
607 y >= this.height_header && i >= 0;
609 terminal.write(y, this.window_width, log_display_lines[i]);
612 draw_info: function() {
613 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
614 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
615 terminal.write(y, this.window_width, lines[i]);
618 draw_input: function() {
619 if (this.mode.has_input_prompt) {
620 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
621 terminal.write(y, this.window_width, this.input_lines[i]);
625 draw_help: function() {
626 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
627 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
628 if (this.mode == mode_play) {
629 content += "Available actions:\n";
630 if (game.tasks.includes('MOVE')) {
631 content += "[" + movement_keys_desc + "] – move player\n";
633 if (game.tasks.includes('PICK_UP')) {
634 content += "[" + this.keys.take_thing + "] – take thing under player\n";
636 if (game.tasks.includes('DROP')) {
637 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
639 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
640 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
642 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
643 content += '\nOther modes available from here:\n';
644 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
645 content += '[' + this.keys.switch_to_study + '] – study mode\n';
646 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
647 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
648 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
649 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
650 } else if (this.mode == mode_study) {
651 content += "Available actions:\n";
652 content += '[' + movement_keys_desc + '] – move question mark\n';
653 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
654 content += '\nOther modes available from here:\n';
655 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
656 content += '[' + this.keys.switch_to_play + '] – play mode\n';
657 } else if (this.mode == mode_chat) {
658 content += '/nick NAME – re-name yourself to NAME\n';
659 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
660 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
663 if (!this.mode.has_input_prompt) {
664 start_x = this.window_width
666 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
667 let lines = this.msg_into_lines_of_width(content, this.window_width);
668 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
669 terminal.write(y, start_x, lines[i]);
672 full_refresh: function() {
673 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
674 if (this.mode.is_intro) {
678 if (game.turn_complete) {
680 this.draw_turn_line();
682 this.draw_mode_line();
683 if (this.mode.shows_info) {
690 if (this.show_help) {
702 this.map_control = "";
703 this.map_size = [0,0];
708 get_thing: function(id_, create_if_not_found=false) {
709 if (id_ in game.things) {
710 return game.things[id_];
711 } else if (create_if_not_found) {
712 let t = new Thing([0,0]);
713 game.things[id_] = t;
717 move: function(start_position, direction) {
718 let target = [start_position[0], start_position[1]];
719 if (direction == 'LEFT') {
721 } else if (direction == 'RIGHT') {
723 } else if (game.map_geometry == 'Square') {
724 if (direction == 'UP') {
726 } else if (direction == 'DOWN') {
729 } else if (game.map_geometry == 'Hex') {
730 let start_indented = start_position[0] % 2;
731 if (direction == 'UPLEFT') {
733 if (!start_indented) {
736 } else if (direction == 'UPRIGHT') {
738 if (start_indented) {
741 } else if (direction == 'DOWNLEFT') {
743 if (!start_indented) {
746 } else if (direction == 'DOWNRIGHT') {
748 if (start_indented) {
753 if (target[0] < 0 || target[1] < 0 ||
754 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
759 teleport: function() {
760 let player = this.get_thing(game.player_id);
761 if (player.position in this.portals) {
762 server.reconnect_to(this.portals[player.position]);
764 terminal.blink_screen();
765 tui.log_msg('? not standing on portal')
773 server.init(websocket_location);
778 move: function(direction) {
779 let target = game.move(this.position, direction);
781 this.position = target
784 terminal.blink_screen();
787 update_info_db: function(yx, str) {
788 this.info_db[yx] = str;
789 if (tui.mode == mode_study) {
793 empty_info_db: function() {
795 if (tui.mode == mode_study) {
799 query_info: function() {
800 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
802 get_info: function() {
803 let position_i = this.position[0] * game.map_size[1] + this.position[1];
804 if (game.fov[position_i] != '.') {
805 return 'outside field of view';
808 let terrain_char = game.map[position_i]
809 let terrain_desc = '?'
810 if (game.terrains[terrain_char]) {
811 terrain_desc = game.terrains[terrain_char];
813 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
814 for (let t_id in game.things) {
815 let t = game.things[t_id];
816 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
817 let symbol = game.thing_types[t.type_];
818 info += "THING: " + t.type_ + " / " + symbol;
820 info += t.player_char;
823 info += " (" + t.name_ + ")";
828 if (this.position in game.portals) {
829 info += "PORTAL: " + game.portals[this.position] + "\n";
831 if (this.position in this.info_db) {
832 info += "ANNOTATIONS: " + this.info_db[this.position];
838 annotate: function(msg) {
839 if (msg.length == 0) {
840 msg = " "; // triggers annotation deletion
842 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
844 set_portal: function(msg) {
845 if (msg.length == 0) {
846 msg = " "; // triggers portal deletion
848 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
852 tui.inputEl.addEventListener('input', (event) => {
853 if (tui.mode.has_input_prompt) {
854 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
855 if (tui.inputEl.value.length > max_length) {
856 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
858 tui.recalc_input_lines();
859 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
860 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
861 tui.switch_mode(mode_play);
865 tui.inputEl.addEventListener('keydown', (event) => {
866 tui.show_help = false;
867 if (event.key == 'Enter') {
868 event.preventDefault();
870 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
871 tui.show_help = true;
873 tui.restore_input_values();
874 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
875 tui.show_help = true;
876 } else if (tui.mode == mode_login && event.key == 'Enter') {
877 tui.login_name = tui.inputEl.value;
878 server.send(['LOGIN', tui.inputEl.value]);
880 } else if (tui.mode == mode_portal && event.key == 'Enter') {
881 explorer.set_portal(tui.inputEl.value);
882 tui.switch_mode(mode_play);
883 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
884 explorer.annotate(tui.inputEl.value);
885 tui.switch_mode(mode_play);
886 } else if (tui.mode == mode_password && event.key == 'Enter') {
887 if (tui.inputEl.value.length == 0) {
888 tui.inputEl.value = " ";
890 tui.password = tui.inputEl.value
891 tui.switch_mode(mode_play);
892 } else if (tui.mode == mode_chat && event.key == 'Enter') {
893 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
894 if (tokens.length > 0 && tokens[0].length > 0) {
895 if (tui.inputEl.value[0][0] == '/') {
896 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
897 tui.switch_mode(mode_play);
898 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
899 tui.switch_mode(mode_study);
900 } else if (tokens[0].slice(1) == 'nick') {
901 if (tokens.length > 1) {
902 server.send(['NICK', tokens[1]]);
904 tui.log_msg('? need new name');
907 tui.log_msg('? unknown command');
910 server.send(['ALL', tui.inputEl.value]);
912 } else if (tui.inputEl.valuelength > 0) {
913 server.send(['ALL', tui.inputEl.value]);
916 } else if (tui.mode == mode_play) {
917 if (event.key === tui.keys.switch_to_chat) {
918 event.preventDefault();
919 tui.switch_mode(mode_chat);
920 } else if (event.key === tui.keys.switch_to_edit
921 && game.tasks.includes('WRITE')) {
922 event.preventDefault();
923 tui.switch_mode(mode_edit);
924 } else if (event.key === tui.keys.switch_to_study) {
925 tui.switch_mode(mode_study);
926 } else if (event.key === tui.keys.switch_to_password) {
927 event.preventDefault();
928 tui.switch_mode(mode_password);
929 } else if (event.key === tui.keys.flatten
930 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
931 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
932 } else if (event.key === tui.keys.take_thing
933 && game.tasks.includes('PICK_UP')) {
934 server.send(["TASK:PICK_UP"]);
935 } else if (event.key === tui.keys.drop_thing
936 && game.tasks.includes('DROP')) {
937 server.send(["TASK:DROP"]);
938 } else if (event.key in tui.movement_keys
939 && game.tasks.includes('MOVE')) {
940 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
941 } else if (event.key === tui.keys.teleport) {
943 } else if (event.key === tui.keys.switch_to_portal) {
944 event.preventDefault();
945 tui.switch_mode(mode_portal);
946 } else if (event.key === tui.keys.switch_to_annotate) {
947 event.preventDefault();
948 tui.switch_mode(mode_annotate);
950 } else if (tui.mode == mode_study) {
951 if (event.key === tui.keys.switch_to_chat) {
952 event.preventDefault();
953 tui.switch_mode(mode_chat);
954 } else if (event.key == tui.keys.switch_to_play) {
955 tui.switch_mode(mode_play);
956 } else if (event.key in tui.movement_keys) {
957 explorer.move(tui.movement_keys[event.key]);
958 } else if (event.key == tui.keys.toggle_map_mode) {
959 if (tui.map_mode == 'terrain') {
960 tui.map_mode = 'control';
962 tui.map_mode = 'terrain';
969 rows_selector.addEventListener('input', function() {
970 if (rows_selector.value % 4 != 0) {
973 window.localStorage.setItem(rows_selector.id, rows_selector.value);
974 terminal.initialize();
977 cols_selector.addEventListener('input', function() {
978 if (cols_selector.value % 4 != 0) {
981 window.localStorage.setItem(cols_selector.id, cols_selector.value);
982 terminal.initialize();
983 tui.window_width = terminal.cols / 2,
986 for (let key_selector of key_selectors) {
987 key_selector.addEventListener('input', function() {
988 window.localStorage.setItem(key_selector.id, key_selector.value);
992 window.setInterval(function() {
993 if (server.connected) {
994 server.send(['PING']);
996 server.reconnect_to(server.url);
997 tui.log_msg('@ attempting reconnect …')
1000 document.getElementById("terminal").onclick = function() {
1001 tui.inputEl.focus();
1003 document.getElementById("help").onclick = function() {
1004 tui.show_help = true;
1007 document.getElementById("switch_to_play").onclick = function() {
1008 tui.switch_mode(mode_play);
1011 document.getElementById("switch_to_study").onclick = function() {
1012 tui.switch_mode(mode_study);
1015 document.getElementById("switch_to_chat").onclick = function() {
1016 tui.switch_mode(mode_chat);
1019 document.getElementById("switch_to_password").onclick = function() {
1020 tui.switch_mode(mode_password);
1023 document.getElementById("switch_to_edit").onclick = function() {
1024 tui.switch_mode(mode_edit);
1027 document.getElementById("switch_to_annotate").onclick = function() {
1028 tui.switch_mode(mode_annotate);
1031 document.getElementById("switch_to_portal").onclick = function() {
1032 tui.switch_mode(mode_portal);
1035 document.getElementById("toggle_map_mode").onclick = function() {
1036 if (tui.map_mode == 'terrain') {
1037 tui.map_mode = 'control';
1039 tui.map_mode = 'terrain';
1043 document.getElementById("take_thing").onclick = function() {
1044 server.send(['TASK:PICK_UP']);
1046 document.getElementById("drop_thing").onclick = function() {
1047 server.send(['TASK:DROP']);
1049 document.getElementById("flatten").onclick = function() {
1050 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1052 document.getElementById("teleport").onclick = function() {
1055 document.getElementById("move_upleft").onclick = function() {
1056 if (tui.mode == mode_play) {
1057 server.send(['TASK:MOVE', 'UPLEFT']);
1059 explorer.move('UPLEFT');
1062 document.getElementById("move_left").onclick = function() {
1063 if (tui.mode == mode_play) {
1064 server.send(['TASK:MOVE', 'LEFT']);
1066 explorer.move('LEFT');
1069 document.getElementById("move_downleft").onclick = function() {
1070 if (tui.mode == mode_play) {
1071 server.send(['TASK:MOVE', 'DOWNLEFT']);
1073 explorer.move('DOWNLEFT');
1076 document.getElementById("move_down").onclick = function() {
1077 if (tui.mode == mode_play) {
1078 server.send(['TASK:MOVE', 'DOWN']);
1080 explorer.move('DOWN');
1083 document.getElementById("move_up").onclick = function() {
1084 if (tui.mode == mode_play) {
1085 server.send(['TASK:MOVE', 'UP']);
1087 explorer.move('UP');
1090 document.getElementById("move_upright").onclick = function() {
1091 if (tui.mode == mode_play) {
1092 server.send(['TASK:MOVE', 'UPRIGHT']);
1094 explorer.move('UPRIGHT');
1097 document.getElementById("move_right").onclick = function() {
1098 if (tui.mode == mode_play) {
1099 server.send(['TASK:MOVE', 'RIGHT']);
1101 explorer.move('RIGHT');
1104 document.getElementById("move_downright").onclick = function() {
1105 if (tui.mode == mode_play) {
1106 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1108 explorer.move('DOWNRIGHT');