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="switch_to_edit">change tile</button><br />
28 <button id="switch_to_password">change tile editing password</button>
29 <button id="switch_to_annotate">annotate tile</button>
30 <button id="switch_to_portal">edit portal link</button>
31 <button id="toggle_map_mode">toggle terrain/control view</button>
33 <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 />
35 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
36 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
37 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
38 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
39 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
40 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
41 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
42 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
43 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
44 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
45 <li>help: <input id="key_help" type="text" value="h" />
46 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
47 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
48 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
49 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
50 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
51 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
52 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
53 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
54 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
55 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
56 <li>toggle terrain/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
61 let websocket_location = "wss://plomlompom.com/rogue_chat/";
63 let rows_selector = document.getElementById("n_rows");
64 let cols_selector = document.getElementById("n_cols");
65 let key_selectors = document.querySelectorAll('[id^="key_"]');
67 function restore_selector_value(selector) {
68 let stored_selection = window.localStorage.getItem(selector.id);
69 if (stored_selection) {
70 selector.value = stored_selection;
73 restore_selector_value(rows_selector);
74 restore_selector_value(cols_selector);
75 for (let key_selector of key_selectors) {
76 restore_selector_value(key_selector);
82 initialize: function() {
83 this.rows = rows_selector.value;
84 this.cols = cols_selector.value;
85 this.pre_el = document.getElementById("terminal");
86 this.pre_el.style.color = this.foreground;
87 this.pre_el.style.backgroundColor = this.background;
90 for (let y = 0, x = 0; y <= this.rows; x++) {
94 this.content.push(line);
103 blink_screen: function() {
104 this.pre_el.style.color = this.background;
105 this.pre_el.style.backgroundColor = this.foreground;
107 this.pre_el.style.color = this.foreground;
108 this.pre_el.style.backgroundColor = this.background;
111 refresh: function() {
113 for (let y = 0; y < this.rows; y++) {
114 let line = this.content[y].join('');
115 pre_string += line + '\n';
117 this.pre_el.textContent = pre_string;
119 write: function(start_y, start_x, msg) {
120 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
121 this.content[start_y][x] = msg[i];
124 drawBox: function(start_y, start_x, height, width) {
125 let end_y = start_y + height;
126 let end_x = start_x + width;
127 for (let y = start_y, x = start_x; y < this.rows; x++) {
135 this.content[y][x] = ' ';
139 terminal.initialize();
142 tokenize: function(str) {
148 for (let i = 0; i < str.length; i++) {
154 } else if (c == '\\') {
156 } else if (c == '"') {
161 } else if (c == '"') {
163 } else if (c === ' ') {
164 if (token.length > 0) {
173 if (token.length > 0) {
176 let token_starts = [];
177 for (let i = 0; i < token_ends.length; i++) {
178 token_starts.push(token_ends[i] - tokens[i].length);
180 return [tokens, token_starts];
182 parse_yx: function(position_string) {
183 let coordinate_strings = position_string.split(',')
184 let position = [0, 0];
185 position[0] = parseInt(coordinate_strings[0].slice(2));
186 position[1] = parseInt(coordinate_strings[1].slice(2));
198 init: function(url) {
200 this.websocket = new WebSocket(this.url);
201 this.websocket.onopen = function(event) {
202 server.connected = true;
203 game.thing_types = {};
205 server.send(['TASKS']);
206 server.send(['TERRAINS']);
207 server.send(['THING_TYPES']);
208 tui.log_msg("@ server connected! :)");
209 tui.switch_mode(mode_login);
211 this.websocket.onclose = function(event) {
212 server.connected = false;
213 tui.switch_mode(mode_waiting_for_server);
214 tui.log_msg("@ server disconnected :(");
216 this.websocket.onmessage = this.handle_event;
218 reconnect_to: function(url) {
219 this.websocket.close();
222 send: function(tokens) {
223 this.websocket.send(unparser.untokenize(tokens));
225 handle_event: function(event) {
226 let tokens = parser.tokenize(event.data)[0];
227 if (tokens[0] === 'TURN') {
228 game.turn_complete = false;
231 game.turn = parseInt(tokens[1]);
232 } else if (tokens[0] === 'THING') {
233 let t = game.get_thing(tokens[3], true);
234 t.position = parser.parse_yx(tokens[1]);
236 } else if (tokens[0] === 'THING_NAME') {
237 let t = game.get_thing(tokens[1], false);
241 } else if (tokens[0] === 'THING_CHAR') {
242 let t = game.get_thing(tokens[1], false);
244 t.player_char = tokens[2];
246 } else if (tokens[0] === 'TASKS') {
247 game.tasks = tokens[1].split(',')
248 } else if (tokens[0] === 'THING_TYPE') {
249 game.thing_types[tokens[1]] = tokens[2]
250 } else if (tokens[0] === 'TERRAIN') {
251 game.terrains[tokens[1]] = tokens[2]
252 } else if (tokens[0] === 'MAP') {
253 game.map_geometry = tokens[1];
255 game.map_size = parser.parse_yx(tokens[2]);
257 } else if (tokens[0] === 'FOV') {
259 } else if (tokens[0] === 'MAP_CONTROL') {
260 game.map_control = tokens[1]
261 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
262 game.turn_complete = true;
263 explorer.empty_info_db();
264 if (tui.mode == mode_post_login_wait) {
265 tui.switch_mode(mode_play);
266 } else if (tui.mode == mode_study) {
267 explorer.query_info();
269 let t = game.get_thing(game.player_id);
270 if (t.position in game.portals) {
271 tui.teleport_target = game.portals[t.position];
272 tui.switch_mode(mode_teleport);
276 } else if (tokens[0] === 'CHAT') {
277 tui.log_msg('# ' + tokens[1], 1);
278 } else if (tokens[0] === 'PLAYER_ID') {
279 game.player_id = parseInt(tokens[1]);
280 } else if (tokens[0] === 'LOGIN_OK') {
281 this.send(['GET_GAMESTATE']);
282 tui.switch_mode(mode_post_login_wait);
283 } else if (tokens[0] === 'PORTAL') {
284 let position = parser.parse_yx(tokens[1]);
285 game.portals[position] = tokens[2];
286 } else if (tokens[0] === 'ANNOTATION') {
287 let position = parser.parse_yx(tokens[1]);
288 explorer.update_info_db(position, tokens[2]);
289 } else if (tokens[0] === 'UNHANDLED_INPUT') {
290 tui.log_msg('? unknown command');
291 } else if (tokens[0] === 'PLAY_ERROR') {
292 terminal.blink_screen();
293 } else if (tokens[0] === 'ARGUMENT_ERROR') {
294 tui.log_msg('? syntax error: ' + tokens[1]);
295 } else if (tokens[0] === 'GAME_ERROR') {
296 tui.log_msg('? game error: ' + tokens[1]);
297 } else if (tokens[0] === 'PONG') {
300 tui.log_msg('? unhandled input: ' + event.data);
306 quote: function(str) {
308 for (let i = 0; i < str.length; i++) {
310 if (['"', '\\'].includes(c)) {
316 return quoted.join('');
318 to_yx: function(yx_coordinate) {
319 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
321 untokenize: function(tokens) {
322 let quoted_tokens = [];
323 for (let token of tokens) {
324 quoted_tokens.push(this.quote(token));
326 return quoted_tokens.join(" ");
331 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
333 this.has_input_prompt = has_input_prompt;
334 this.shows_info= shows_info;
335 this.is_intro = is_intro;
336 this.help_intro = help_intro;
339 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
340 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
341 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
342 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);
343 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);
344 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
345 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);
346 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);
347 let mode_teleport = new Mode('teleport', 'Follow the instructions to re-connect and log-in to another server, or enter anything else to abort.', true);
348 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);
349 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);
352 mode: mode_waiting_for_server,
356 window_width: terminal.cols / 2,
363 this.inputEl = document.getElementById("input");
364 this.inputEl.focus();
365 this.recalc_input_lines();
366 this.height_header = this.height_turn_line + this.height_mode_line;
367 this.log_msg("@ waiting for server connection ...");
370 init_keys: function() {
372 for (let key_selector of key_selectors) {
373 this.keys[key_selector.id.slice(4)] = key_selector.value;
375 this.movement_keys = {
376 [this.keys.square_move_up]: 'UP',
377 [this.keys.square_move_left]: 'LEFT',
378 [this.keys.square_move_down]: 'DOWN',
379 [this.keys.square_move_right]: 'RIGHT'
381 if (game.map_geometry == 'Hex') {
382 this.movement_keys = {
383 [this.keys.hex_move_upleft]: 'UPLEFT',
384 [this.keys.hex_move_upright]: 'UPRIGHT',
385 [this.keys.hex_move_right]: 'RIGHT',
386 [this.keys.hex_move_downright]: 'DOWNRIGHT',
387 [this.keys.hex_move_downleft]: 'DOWNLEFT',
388 [this.keys.hex_move_left]: 'LEFT'
392 switch_mode: function(mode) {
393 this.show_help = false;
394 this.map_mode = 'terrain';
395 if (mode.shows_info && game.player_id in game.things) {
396 explorer.position = game.things[game.player_id].position;
400 this.restore_input_values();
401 document.getElementById("take_thing").disabled = true;
402 document.getElementById("drop_thing").disabled = true;
403 document.getElementById("flatten").disabled = true;
404 document.getElementById("toggle_map_mode").disabled = true;
405 document.getElementById("switch_to_chat").disabled = true;
406 document.getElementById("switch_to_play").disabled = true;
407 document.getElementById("switch_to_study").disabled = true;
408 document.getElementById("switch_to_edit").disabled = true;
409 document.getElementById("switch_to_portal").disabled = true;
410 document.getElementById("switch_to_annotate").disabled = true;
411 document.getElementById("switch_to_password").disabled = true;
412 document.getElementById("move_left").disabled = true;
413 document.getElementById("move_upleft").disabled = true;
414 document.getElementById("move_up").disabled = true;
415 document.getElementById("move_upright").disabled = true;
416 document.getElementById("move_downleft").disabled = true;
417 document.getElementById("move_down").disabled = true;
418 document.getElementById("move_downright").disabled = true;
419 document.getElementById("move_right").disabled = true;
420 if (mode == mode_play || mode == mode_study) {
421 document.getElementById("move_left").disabled = false;
422 document.getElementById("move_right").disabled = false;
423 if (game.map_geometry == 'Hex') {
424 document.getElementById("move_upleft").disabled = false;
425 document.getElementById("move_upright").disabled = false;
426 document.getElementById("move_downleft").disabled = false;
427 document.getElementById("move_downright").disabled = false;
429 document.getElementById("move_up").disabled = false;
430 document.getElementById("move_down").disabled = false;
433 if (!mode.is_intro && mode != mode_play) {
434 document.getElementById("switch_to_play").disabled = false;
436 if (!mode.is_intro && mode != mode_study) {
437 document.getElementById("switch_to_study").disabled = false;
439 if (!mode.is_intro && mode != mode_chat) {
440 document.getElementById("switch_to_chat").disabled = false;
442 if (mode == mode_login) {
443 if (this.login_name) {
444 server.send(['LOGIN', this.login_name]);
446 this.log_msg("? need login name");
448 } else if (mode == mode_play) {
449 if (game.tasks.includes('PICK_UP')) {
450 document.getElementById("take_thing").disabled = false;
452 if (game.tasks.includes('DROP')) {
453 document.getElementById("drop_thing").disabled = false;
455 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
456 document.getElementById("flatten").disabled = false;
458 if (game.tasks.includes('MOVE')) {
460 document.getElementById("switch_to_annotate").disabled = false;
461 document.getElementById("switch_to_edit").disabled = false;
462 document.getElementById("switch_to_portal").disabled = false;
463 document.getElementById("switch_to_password").disabled = false;
464 } else if (mode == mode_study) {
465 document.getElementById("toggle_map_mode").disabled = false;
466 } else if (mode == mode_edit) {
467 this.show_help = true;
468 } else if (mode == mode_teleport) {
469 tui.log_msg("@ May teleport to: " + tui.teleport_target);
470 tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
474 restore_input_values: function() {
475 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
476 let info = explorer.info_db[explorer.position];
477 if (info != "(none)") {
478 this.inputEl.value = info;
479 this.recalc_input_lines();
481 } else if (this.mode == mode_portal && explorer.position in game.portals) {
482 let portal = game.portals[explorer.position]
483 this.inputEl.value = portal;
484 this.recalc_input_lines();
485 } else if (this.mode == mode_password) {
486 this.inputEl.value = this.password;
487 this.recalc_input_lines();
490 empty_input: function(str) {
491 this.inputEl.value = "";
492 if (this.mode.has_input_prompt) {
493 this.recalc_input_lines();
495 this.height_input = 0;
498 recalc_input_lines: function() {
499 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
500 this.height_input = this.input_lines.length;
502 msg_into_lines_of_width: function(msg, width) {
505 for (let i = 0, x = 0; i < msg.length; i++, x++) {
506 if (x >= width || msg[i] == "\n") {
511 if (msg[i] != "\n") {
518 log_msg: function(msg) {
520 while (this.log.length > 100) {
525 draw_map: function() {
526 let map_lines_split = [];
528 let map_content = game.map;
529 if (this.map_mode == 'control') {
530 map_content = game.map_control;
532 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
533 if (j == game.map_size[1]) {
534 map_lines_split.push(line);
538 line.push(map_content[i] + ' ');
540 map_lines_split.push(line);
541 if (this.map_mode == 'terrain') {
542 let used_positions = [];
543 for (const thing_id in game.things) {
544 let t = game.things[thing_id];
545 let symbol = game.thing_types[t.type_];
548 meta_char = t.player_char;
550 if (used_positions.includes(t.position.toString())) {
553 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
554 used_positions.push(t.position.toString());
557 if (tui.mode.shows_info) {
558 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
561 if (game.map_geometry == 'Square') {
562 for (let line_split of map_lines_split) {
563 map_lines.push(line_split.join(''));
565 } else if (game.map_geometry == 'Hex') {
567 for (let line_split of map_lines_split) {
568 map_lines.push(' '.repeat(indent) + line_split.join(''));
576 let window_center = [terminal.rows / 2, this.window_width / 2];
577 let player = game.things[game.player_id];
578 let center_position = [player.position[0], player.position[1]];
579 if (tui.mode.shows_info) {
580 center_position = [explorer.position[0], explorer.position[1]];
582 center_position[1] = center_position[1] * 2;
583 let offset = [center_position[0] - window_center[0],
584 center_position[1] - window_center[1]]
585 if (game.map_geometry == 'Hex' && offset[0] % 2) {
588 let term_y = Math.max(0, -offset[0]);
589 let term_x = Math.max(0, -offset[1]);
590 let map_y = Math.max(0, offset[0]);
591 let map_x = Math.max(0, offset[1]);
592 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
593 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
594 terminal.write(term_y, term_x, to_draw);
597 draw_mode_line: function() {
598 let help = 'hit [' + this.keys.help + '] for help';
599 if (this.mode.has_input_prompt) {
600 help = 'enter /help for help';
602 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
604 draw_turn_line: function(n) {
605 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
607 draw_history: function() {
608 let log_display_lines = [];
609 for (let line of this.log) {
610 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
612 for (let y = terminal.rows - 1 - this.height_input,
613 i = log_display_lines.length - 1;
614 y >= this.height_header && i >= 0;
616 terminal.write(y, this.window_width, log_display_lines[i]);
619 draw_info: function() {
620 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
621 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
622 terminal.write(y, this.window_width, lines[i]);
625 draw_input: function() {
626 if (this.mode.has_input_prompt) {
627 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
628 terminal.write(y, this.window_width, this.input_lines[i]);
632 draw_help: function() {
633 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
634 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
635 if (this.mode == mode_play) {
636 content += "Available actions:\n";
637 if (game.tasks.includes('MOVE')) {
638 content += "[" + movement_keys_desc + "] – move player\n";
640 if (game.tasks.includes('PICK_UP')) {
641 content += "[" + this.keys.take_thing + "] – take thing under player\n";
643 if (game.tasks.includes('DROP')) {
644 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
646 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
647 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
649 content += '\nOther modes available from here:\n';
650 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
651 content += '[' + this.keys.switch_to_study + '] – study mode\n';
652 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
653 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
654 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
655 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
656 } else if (this.mode == mode_study) {
657 content += "Available actions:\n";
658 content += '[' + movement_keys_desc + '] – move question mark\n';
659 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
660 content += '\nOther modes available from here:\n';
661 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
662 content += '[' + this.keys.switch_to_play + '] – play mode\n';
663 } else if (this.mode == mode_chat) {
664 content += '/nick NAME – re-name yourself to NAME\n';
665 //content += '/msg USER TEXT – send TEXT to USER\n';
666 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
667 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
670 if (!this.mode.has_input_prompt) {
671 start_x = this.window_width
673 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
674 let lines = this.msg_into_lines_of_width(content, this.window_width);
675 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
676 terminal.write(y, start_x, lines[i]);
679 full_refresh: function() {
680 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
681 if (this.mode.is_intro) {
685 if (game.turn_complete) {
687 this.draw_turn_line();
689 this.draw_mode_line();
690 if (this.mode.shows_info) {
697 if (this.show_help) {
709 this.map_control = "";
710 this.map_size = [0,0];
715 get_thing: function(id_, create_if_not_found=false) {
716 if (id_ in game.things) {
717 return game.things[id_];
718 } else if (create_if_not_found) {
719 let t = new Thing([0,0]);
720 game.things[id_] = t;
724 move: function(start_position, direction) {
725 let target = [start_position[0], start_position[1]];
726 if (direction == 'LEFT') {
728 } else if (direction == 'RIGHT') {
730 } else if (game.map_geometry == 'Square') {
731 if (direction == 'UP') {
733 } else if (direction == 'DOWN') {
736 } else if (game.map_geometry == 'Hex') {
737 let start_indented = start_position[0] % 2;
738 if (direction == 'UPLEFT') {
740 if (!start_indented) {
743 } else if (direction == 'UPRIGHT') {
745 if (start_indented) {
748 } else if (direction == 'DOWNLEFT') {
750 if (!start_indented) {
753 } else if (direction == 'DOWNRIGHT') {
755 if (start_indented) {
760 if (target[0] < 0 || target[1] < 0 ||
761 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
771 server.init(websocket_location);
776 move: function(direction) {
777 let target = game.move(this.position, direction);
779 this.position = target
782 terminal.blink_screen();
785 update_info_db: function(yx, str) {
786 this.info_db[yx] = str;
787 if (tui.mode == mode_study) {
791 empty_info_db: function() {
793 if (tui.mode == mode_study) {
797 query_info: function() {
798 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
800 get_info: function() {
801 let position_i = this.position[0] * game.map_size[1] + this.position[1];
802 if (game.fov[position_i] != '.') {
803 return 'outside field of view';
806 let terrain_char = game.map[position_i]
807 let terrain_desc = '?'
808 if (game.terrains[terrain_char]) {
809 terrain_desc = game.terrains[terrain_char];
811 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
812 for (let t_id in game.things) {
813 let t = game.things[t_id];
814 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
815 let symbol = game.thing_types[t.type_];
816 info += "THING: " + t.type_ + " / " + symbol;
818 info += t.player_char;
821 info += " (" + t.name_ + ")";
826 if (this.position in game.portals) {
827 info += "PORTAL: " + game.portals[this.position] + "\n";
829 if (this.position in this.info_db) {
830 info += "ANNOTATIONS: " + this.info_db[this.position];
836 annotate: function(msg) {
837 if (msg.length == 0) {
838 msg = " "; // triggers annotation deletion
840 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
842 set_portal: function(msg) {
843 if (msg.length == 0) {
844 msg = " "; // triggers portal deletion
846 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
850 tui.inputEl.addEventListener('input', (event) => {
851 if (tui.mode.has_input_prompt) {
852 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
853 if (tui.inputEl.value.length > max_length) {
854 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
856 tui.recalc_input_lines();
857 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
858 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
859 tui.switch_mode(mode_play);
864 tui.inputEl.addEventListener('keydown', (event) => {
865 tui.show_help = false;
866 if (event.key == 'Enter') {
867 event.preventDefault();
869 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
870 tui.show_help = true;
872 tui.restore_input_values();
873 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
874 tui.show_help = true;
875 } else if (tui.mode == mode_login && event.key == 'Enter') {
876 tui.login_name = tui.inputEl.value;
877 server.send(['LOGIN', tui.inputEl.value]);
879 } else if (tui.mode == mode_portal && event.key == 'Enter') {
880 explorer.set_portal(tui.inputEl.value);
881 tui.switch_mode(mode_play);
882 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
883 explorer.annotate(tui.inputEl.value);
884 tui.switch_mode(mode_play);
885 } else if (tui.mode == mode_password && event.key == 'Enter') {
886 if (tui.inputEl.value.length == 0) {
887 tui.inputEl.value = " ";
889 tui.password = tui.inputEl.value
890 tui.switch_mode(mode_play);
891 } else if (tui.mode == mode_teleport && event.key == 'Enter') {
892 if (tui.inputEl.value == 'YES!') {
893 server.reconnect_to(tui.teleport_target);
895 tui.log_msg('@ teleport aborted');
896 tui.switch_mode(mode_play);
898 } else if (tui.mode == mode_chat && event.key == 'Enter') {
899 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
900 if (tokens.length > 0 && tokens[0].length > 0) {
901 if (tui.inputEl.value[0][0] == '/') {
902 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
903 tui.switch_mode(mode_play);
904 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
905 tui.switch_mode(mode_study);
906 } else if (tokens[0].slice(1) == 'nick') {
907 if (tokens.length > 1) {
908 server.send(['NICK', tokens[1]]);
910 tui.log_msg('? need new name');
912 //} else if (tokens[0].slice(1) == 'msg') {
913 // if (tokens.length > 2) {
914 // let msg = tui.inputEl.value.slice(token_starts[2]);
915 // server.send(['QUERY', tokens[1], msg]);
917 // tui.log_msg('? need message target and message');
920 tui.log_msg('? unknown command');
923 server.send(['ALL', tui.inputEl.value]);
925 } else if (tui.inputEl.valuelength > 0) {
926 server.send(['ALL', tui.inputEl.value]);
929 } else if (tui.mode == mode_play) {
930 if (event.key === tui.keys.switch_to_chat) {
931 event.preventDefault();
932 tui.switch_mode(mode_chat);
933 } else if (event.key === tui.keys.switch_to_edit
934 && game.tasks.includes('WRITE')) {
935 event.preventDefault();
936 tui.switch_mode(mode_edit);
937 } else if (event.key === tui.keys.switch_to_study) {
938 tui.switch_mode(mode_study);
939 } else if (event.key === tui.keys.switch_to_password) {
940 event.preventDefault();
941 tui.switch_mode(mode_password);
942 } else if (event.key === tui.keys.flatten
943 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
944 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
945 } else if (event.key === tui.keys.take_thing
946 && game.tasks.includes('PICK_UP')) {
947 server.send(["TASK:PICK_UP"]);
948 } else if (event.key === tui.keys.drop_thing
949 && game.tasks.includes('DROP')) {
950 server.send(["TASK:DROP"]);
951 } else if (event.key in tui.movement_keys
952 && game.tasks.includes('MOVE')) {
953 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
954 } else if (event.key === tui.keys.switch_to_portal) {
955 event.preventDefault();
956 tui.switch_mode(mode_portal);
957 } else if (event.key === tui.keys.switch_to_annotate) {
958 event.preventDefault();
959 tui.switch_mode(mode_annotate);
961 } else if (tui.mode == mode_study) {
962 if (event.key === tui.keys.switch_to_chat) {
963 event.preventDefault();
964 tui.switch_mode(mode_chat);
965 } else if (event.key == tui.keys.switch_to_play) {
966 tui.switch_mode(mode_play);
967 } else if (event.key in tui.movement_keys) {
968 explorer.move(tui.movement_keys[event.key]);
969 } else if (event.key == tui.keys.toggle_map_mode) {
970 if (tui.map_mode == 'terrain') {
971 tui.map_mode = 'control';
973 tui.map_mode = 'terrain';
980 rows_selector.addEventListener('input', function() {
981 if (rows_selector.value % 4 != 0) {
984 window.localStorage.setItem(rows_selector.id, rows_selector.value);
985 terminal.initialize();
988 cols_selector.addEventListener('input', function() {
989 if (cols_selector.value % 4 != 0) {
992 window.localStorage.setItem(cols_selector.id, cols_selector.value);
993 terminal.initialize();
994 tui.window_width = terminal.cols / 2,
997 for (let key_selector of key_selectors) {
998 key_selector.addEventListener('input', function() {
999 window.localStorage.setItem(key_selector.id, key_selector.value);
1003 window.setInterval(function() {
1004 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
1005 || document.activeElement.id.startsWith('key_'))) {
1006 tui.inputEl.focus();
1009 window.setInterval(function() {
1010 if (server.connected) {
1011 server.send(['PING']);
1013 server.reconnect_to(server.url);
1014 tui.log_msg('@ attempting reconnect …')
1018 document.getElementById("help").onclick = function() {
1019 tui.show_help = true;
1022 document.getElementById("switch_to_play").onclick = function() {
1023 tui.switch_mode(mode_play);
1026 document.getElementById("switch_to_study").onclick = function() {
1027 tui.switch_mode(mode_study);
1030 document.getElementById("switch_to_chat").onclick = function() {
1031 tui.switch_mode(mode_chat);
1034 document.getElementById("switch_to_password").onclick = function() {
1035 tui.switch_mode(mode_password);
1038 document.getElementById("switch_to_edit").onclick = function() {
1039 tui.switch_mode(mode_edit);
1042 document.getElementById("switch_to_annotate").onclick = function() {
1043 tui.switch_mode(mode_annotate);
1046 document.getElementById("switch_to_portal").onclick = function() {
1047 tui.switch_mode(mode_portal);
1050 document.getElementById("toggle_map_mode").onclick = function() {
1051 if (tui.map_mode == 'terrain') {
1052 tui.map_mode = 'control';
1054 tui.map_mode = 'terrain';
1058 document.getElementById("take_thing").onclick = function() {
1059 server.send(['TASK:PICK_UP']);
1061 document.getElementById("drop_thing").onclick = function() {
1062 server.send(['TASK:DROP']);
1064 document.getElementById("flatten").onclick = function() {
1065 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1067 document.getElementById("move_upleft").onclick = function() {
1068 if (tui.mode == mode_play) {
1069 server.send(['TASK:MOVE', 'UPLEFT']);
1071 explorer.move('UPLEFT');
1074 document.getElementById("move_left").onclick = function() {
1075 if (tui.mode == mode_play) {
1076 server.send(['TASK:MOVE', 'LEFT']);
1078 explorer.move('LEFT');
1081 document.getElementById("move_downleft").onclick = function() {
1082 if (tui.mode == mode_play) {
1083 server.send(['TASK:MOVE', 'DOWNLEFT']);
1085 explorer.move('DOWNLEFT');
1088 document.getElementById("move_down").onclick = function() {
1089 if (tui.mode == mode_play) {
1090 server.send(['TASK:MOVE', 'DOWN']);
1092 explorer.move('DOWN');
1095 document.getElementById("move_up").onclick = function() {
1096 if (tui.mode == mode_play) {
1097 server.send(['TASK:MOVE', 'UP']);
1099 explorer.move('UP');
1102 document.getElementById("move_upright").onclick = function() {
1103 if (tui.mode == mode_play) {
1104 server.send(['TASK:MOVE', 'UPRIGHT']);
1106 explorer.move('UPRIGHT');
1109 document.getElementById("move_right").onclick = function() {
1110 if (tui.mode == mode_play) {
1111 server.send(['TASK:MOVE', 'RIGHT']);
1113 explorer.move('RIGHT');
1116 document.getElementById("move_downright").onclick = function() {
1117 if (tui.mode == mode_play) {
1118 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1120 explorer.move('DOWNRIGHT');