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.show_help = false;
383 this.map_mode = 'terrain';
384 if (mode.shows_info && game.player_id in game.things) {
385 explorer.position = game.things[game.player_id].position;
389 this.restore_input_values();
390 document.getElementById("take_thing").disabled = true;
391 document.getElementById("drop_thing").disabled = true;
392 document.getElementById("flatten").disabled = true;
393 document.getElementById("teleport").disabled = true;
394 document.getElementById("toggle_map_mode").disabled = true;
395 document.getElementById("switch_to_chat").disabled = true;
396 document.getElementById("switch_to_play").disabled = true;
397 document.getElementById("switch_to_study").disabled = true;
398 document.getElementById("switch_to_edit").disabled = true;
399 document.getElementById("switch_to_portal").disabled = true;
400 document.getElementById("switch_to_annotate").disabled = true;
401 document.getElementById("switch_to_password").disabled = true;
402 document.getElementById("move_left").disabled = true;
403 document.getElementById("move_upleft").disabled = true;
404 document.getElementById("move_up").disabled = true;
405 document.getElementById("move_upright").disabled = true;
406 document.getElementById("move_downleft").disabled = true;
407 document.getElementById("move_down").disabled = true;
408 document.getElementById("move_downright").disabled = true;
409 document.getElementById("move_right").disabled = true;
410 if (mode == mode_play || mode == mode_study) {
411 document.getElementById("move_left").disabled = false;
412 document.getElementById("move_right").disabled = false;
413 if (game.map_geometry == 'Hex') {
414 document.getElementById("move_upleft").disabled = false;
415 document.getElementById("move_upright").disabled = false;
416 document.getElementById("move_downleft").disabled = false;
417 document.getElementById("move_downright").disabled = false;
419 document.getElementById("move_up").disabled = false;
420 document.getElementById("move_down").disabled = false;
423 if (!mode.is_intro && mode != mode_play) {
424 document.getElementById("switch_to_play").disabled = false;
426 if (!mode.is_intro && mode != mode_study) {
427 document.getElementById("switch_to_study").disabled = false;
429 if (!mode.is_intro && mode != mode_chat) {
430 document.getElementById("switch_to_chat").disabled = false;
432 if (mode == mode_login) {
433 if (this.login_name) {
434 server.send(['LOGIN', this.login_name]);
436 this.log_msg("? need login name");
438 } else if (mode == mode_play) {
439 if (game.tasks.includes('PICK_UP')) {
440 document.getElementById("take_thing").disabled = false;
442 if (game.tasks.includes('DROP')) {
443 document.getElementById("drop_thing").disabled = false;
445 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
446 document.getElementById("flatten").disabled = false;
448 if (game.tasks.includes('MOVE')) {
450 document.getElementById("teleport").disabled = false;
451 document.getElementById("switch_to_annotate").disabled = false;
452 document.getElementById("switch_to_edit").disabled = false;
453 document.getElementById("switch_to_portal").disabled = false;
454 document.getElementById("switch_to_password").disabled = false;
455 } else if (mode == mode_study) {
456 document.getElementById("toggle_map_mode").disabled = false;
457 } else if (mode == mode_edit) {
458 this.show_help = true;
462 restore_input_values: function() {
463 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
464 let info = explorer.info_db[explorer.position];
465 if (info != "(none)") {
466 this.inputEl.value = info;
467 this.recalc_input_lines();
469 } else if (this.mode == mode_portal && explorer.position in game.portals) {
470 let portal = game.portals[explorer.position]
471 this.inputEl.value = portal;
472 this.recalc_input_lines();
473 } else if (this.mode == mode_password) {
474 this.inputEl.value = this.password;
475 this.recalc_input_lines();
478 empty_input: function(str) {
479 this.inputEl.value = "";
480 if (this.mode.has_input_prompt) {
481 this.recalc_input_lines();
483 this.height_input = 0;
486 recalc_input_lines: function() {
487 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
488 this.height_input = this.input_lines.length;
490 msg_into_lines_of_width: function(msg, width) {
493 for (let i = 0, x = 0; i < msg.length; i++, x++) {
494 if (x >= width || msg[i] == "\n") {
499 if (msg[i] != "\n") {
506 log_msg: function(msg) {
508 while (this.log.length > 100) {
513 draw_map: function() {
514 let map_lines_split = [];
516 let map_content = game.map;
517 if (this.map_mode == 'control') {
518 map_content = game.map_control;
520 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
521 if (j == game.map_size[1]) {
522 map_lines_split.push(line);
526 line.push(map_content[i] + ' ');
528 map_lines_split.push(line);
529 if (this.map_mode == 'terrain') {
530 for (const p in game.portals) {
531 let coordinate = p.split(',')
532 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
534 let used_positions = [];
535 for (const thing_id in game.things) {
536 let t = game.things[thing_id];
537 let symbol = game.thing_types[t.type_];
540 meta_char = t.player_char;
542 if (used_positions.includes(t.position.toString())) {
545 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
546 used_positions.push(t.position.toString());
549 if (tui.mode.shows_info) {
550 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
553 if (game.map_geometry == 'Square') {
554 for (let line_split of map_lines_split) {
555 map_lines.push(line_split.join(''));
557 } else if (game.map_geometry == 'Hex') {
559 for (let line_split of map_lines_split) {
560 map_lines.push(' '.repeat(indent) + line_split.join(''));
568 let window_center = [terminal.rows / 2, this.window_width / 2];
569 let player = game.things[game.player_id];
570 let center_position = [player.position[0], player.position[1]];
571 if (tui.mode.shows_info) {
572 center_position = [explorer.position[0], explorer.position[1]];
574 center_position[1] = center_position[1] * 2;
575 let offset = [center_position[0] - window_center[0],
576 center_position[1] - window_center[1]]
577 if (game.map_geometry == 'Hex' && offset[0] % 2) {
580 let term_y = Math.max(0, -offset[0]);
581 let term_x = Math.max(0, -offset[1]);
582 let map_y = Math.max(0, offset[0]);
583 let map_x = Math.max(0, offset[1]);
584 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
585 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
586 terminal.write(term_y, term_x, to_draw);
589 draw_mode_line: function() {
590 let help = 'hit [' + this.keys.help + '] for help';
591 if (this.mode.has_input_prompt) {
592 help = 'enter /help for help';
594 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
596 draw_turn_line: function(n) {
597 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
599 draw_history: function() {
600 let log_display_lines = [];
601 for (let line of this.log) {
602 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
604 for (let y = terminal.rows - 1 - this.height_input,
605 i = log_display_lines.length - 1;
606 y >= this.height_header && i >= 0;
608 terminal.write(y, this.window_width, log_display_lines[i]);
611 draw_info: function() {
612 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
613 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
614 terminal.write(y, this.window_width, lines[i]);
617 draw_input: function() {
618 if (this.mode.has_input_prompt) {
619 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
620 terminal.write(y, this.window_width, this.input_lines[i]);
624 draw_help: function() {
625 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
626 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
627 if (this.mode == mode_play) {
628 content += "Available actions:\n";
629 if (game.tasks.includes('MOVE')) {
630 content += "[" + movement_keys_desc + "] – move player\n";
632 if (game.tasks.includes('PICK_UP')) {
633 content += "[" + this.keys.take_thing + "] – take thing under player\n";
635 if (game.tasks.includes('DROP')) {
636 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
638 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
639 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
641 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
642 content += '\nOther modes available from here:\n';
643 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
644 content += '[' + this.keys.switch_to_study + '] – study mode\n';
645 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
646 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
647 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
648 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
649 } else if (this.mode == mode_study) {
650 content += "Available actions:\n";
651 content += '[' + movement_keys_desc + '] – move question mark\n';
652 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
653 content += '\nOther modes available from here:\n';
654 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
655 content += '[' + this.keys.switch_to_play + '] – play mode\n';
656 } else if (this.mode == mode_chat) {
657 content += '/nick NAME – re-name yourself to NAME\n';
658 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
659 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
662 if (!this.mode.has_input_prompt) {
663 start_x = this.window_width
665 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
666 let lines = this.msg_into_lines_of_width(content, this.window_width);
667 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
668 terminal.write(y, start_x, lines[i]);
671 full_refresh: function() {
672 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
673 if (this.mode.is_intro) {
677 if (game.turn_complete) {
679 this.draw_turn_line();
681 this.draw_mode_line();
682 if (this.mode.shows_info) {
689 if (this.show_help) {
701 this.map_control = "";
702 this.map_size = [0,0];
707 get_thing: function(id_, create_if_not_found=false) {
708 if (id_ in game.things) {
709 return game.things[id_];
710 } else if (create_if_not_found) {
711 let t = new Thing([0,0]);
712 game.things[id_] = t;
716 move: function(start_position, direction) {
717 let target = [start_position[0], start_position[1]];
718 if (direction == 'LEFT') {
720 } else if (direction == 'RIGHT') {
722 } else if (game.map_geometry == 'Square') {
723 if (direction == 'UP') {
725 } else if (direction == 'DOWN') {
728 } else if (game.map_geometry == 'Hex') {
729 let start_indented = start_position[0] % 2;
730 if (direction == 'UPLEFT') {
732 if (!start_indented) {
735 } else if (direction == 'UPRIGHT') {
737 if (start_indented) {
740 } else if (direction == 'DOWNLEFT') {
742 if (!start_indented) {
745 } else if (direction == 'DOWNRIGHT') {
747 if (start_indented) {
752 if (target[0] < 0 || target[1] < 0 ||
753 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
758 teleport: function() {
759 let player = this.get_thing(game.player_id);
760 if (player.position in this.portals) {
761 server.reconnect_to(this.portals[player.position]);
763 terminal.blink_screen();
764 tui.log_msg('? not standing on portal')
772 server.init(websocket_location);
777 move: function(direction) {
778 let target = game.move(this.position, direction);
780 this.position = target
783 terminal.blink_screen();
786 update_info_db: function(yx, str) {
787 this.info_db[yx] = str;
788 if (tui.mode == mode_study) {
792 empty_info_db: function() {
794 if (tui.mode == mode_study) {
798 query_info: function() {
799 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
801 get_info: function() {
802 let position_i = this.position[0] * game.map_size[1] + this.position[1];
803 if (game.fov[position_i] != '.') {
804 return 'outside field of view';
807 let terrain_char = game.map[position_i]
808 let terrain_desc = '?'
809 if (game.terrains[terrain_char]) {
810 terrain_desc = game.terrains[terrain_char];
812 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
813 for (let t_id in game.things) {
814 let t = game.things[t_id];
815 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
816 let symbol = game.thing_types[t.type_];
817 info += "THING: " + t.type_ + " / " + symbol;
819 info += t.player_char;
822 info += " (" + t.name_ + ")";
827 if (this.position in game.portals) {
828 info += "PORTAL: " + game.portals[this.position] + "\n";
830 if (this.position in this.info_db) {
831 info += "ANNOTATIONS: " + this.info_db[this.position];
837 annotate: function(msg) {
838 if (msg.length == 0) {
839 msg = " "; // triggers annotation deletion
841 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
843 set_portal: function(msg) {
844 if (msg.length == 0) {
845 msg = " "; // triggers portal deletion
847 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
851 tui.inputEl.addEventListener('input', (event) => {
852 if (tui.mode.has_input_prompt) {
853 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
854 if (tui.inputEl.value.length > max_length) {
855 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
857 tui.recalc_input_lines();
858 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
859 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
860 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 (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
994 || document.activeElement.id.startsWith('key_'))) {
998 window.setInterval(function() {
999 if (server.connected) {
1000 server.send(['PING']);
1002 server.reconnect_to(server.url);
1003 tui.log_msg('@ attempting reconnect …')
1007 document.getElementById("help").onclick = function() {
1008 tui.show_help = true;
1011 document.getElementById("switch_to_play").onclick = function() {
1012 tui.switch_mode(mode_play);
1015 document.getElementById("switch_to_study").onclick = function() {
1016 tui.switch_mode(mode_study);
1019 document.getElementById("switch_to_chat").onclick = function() {
1020 tui.switch_mode(mode_chat);
1023 document.getElementById("switch_to_password").onclick = function() {
1024 tui.switch_mode(mode_password);
1027 document.getElementById("switch_to_edit").onclick = function() {
1028 tui.switch_mode(mode_edit);
1031 document.getElementById("switch_to_annotate").onclick = function() {
1032 tui.switch_mode(mode_annotate);
1035 document.getElementById("switch_to_portal").onclick = function() {
1036 tui.switch_mode(mode_portal);
1039 document.getElementById("toggle_map_mode").onclick = function() {
1040 if (tui.map_mode == 'terrain') {
1041 tui.map_mode = 'control';
1043 tui.map_mode = 'terrain';
1047 document.getElementById("take_thing").onclick = function() {
1048 server.send(['TASK:PICK_UP']);
1050 document.getElementById("drop_thing").onclick = function() {
1051 server.send(['TASK:DROP']);
1053 document.getElementById("flatten").onclick = function() {
1054 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1056 document.getElementById("teleport").onclick = function() {
1059 document.getElementById("move_upleft").onclick = function() {
1060 if (tui.mode == mode_play) {
1061 server.send(['TASK:MOVE', 'UPLEFT']);
1063 explorer.move('UPLEFT');
1066 document.getElementById("move_left").onclick = function() {
1067 if (tui.mode == mode_play) {
1068 server.send(['TASK:MOVE', 'LEFT']);
1070 explorer.move('LEFT');
1073 document.getElementById("move_downleft").onclick = function() {
1074 if (tui.mode == mode_play) {
1075 server.send(['TASK:MOVE', 'DOWNLEFT']);
1077 explorer.move('DOWNLEFT');
1080 document.getElementById("move_down").onclick = function() {
1081 if (tui.mode == mode_play) {
1082 server.send(['TASK:MOVE', 'DOWN']);
1084 explorer.move('DOWN');
1087 document.getElementById("move_up").onclick = function() {
1088 if (tui.mode == mode_play) {
1089 server.send(['TASK:MOVE', 'UP']);
1091 explorer.move('UP');
1094 document.getElementById("move_upright").onclick = function() {
1095 if (tui.mode == mode_play) {
1096 server.send(['TASK:MOVE', 'UPRIGHT']);
1098 explorer.move('UPRIGHT');
1101 document.getElementById("move_right").onclick = function() {
1102 if (tui.mode == mode_play) {
1103 server.send(['TASK:MOVE', 'RIGHT']);
1105 explorer.move('RIGHT');
1108 document.getElementById("move_downright").onclick = function() {
1109 if (tui.mode == mode_play) {
1110 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1112 explorer.move('DOWNRIGHT');