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) {
150 for (let i = 0; i < str.length; i++) {
156 } else if (c == '\\') {
158 } else if (c == '"') {
163 } else if (c == '"') {
165 } else if (c === ' ') {
166 if (token.length > 0) {
175 if (token.length > 0) {
178 let token_starts = [];
179 for (let i = 0; i < token_ends.length; i++) {
180 token_starts.push(token_ends[i] - tokens[i].length);
182 return [tokens, token_starts];
184 parse_yx: function(position_string) {
185 let coordinate_strings = position_string.split(',')
186 let position = [0, 0];
187 position[0] = parseInt(coordinate_strings[0].slice(2));
188 position[1] = parseInt(coordinate_strings[1].slice(2));
200 init: function(url) {
202 this.websocket = new WebSocket(this.url);
203 this.websocket.onopen = function(event) {
204 server.connected = true;
205 game.thing_types = {};
207 server.send(['TASKS']);
208 server.send(['TERRAINS']);
209 server.send(['THING_TYPES']);
210 tui.log_msg("@ server connected! :)");
211 tui.switch_mode(mode_login);
213 this.websocket.onclose = function(event) {
214 server.connected = false;
215 tui.switch_mode(mode_waiting_for_server);
216 tui.log_msg("@ server disconnected :(");
218 this.websocket.onmessage = this.handle_event;
220 reconnect_to: function(url) {
221 this.websocket.close();
224 send: function(tokens) {
225 this.websocket.send(unparser.untokenize(tokens));
227 handle_event: function(event) {
228 let tokens = parser.tokenize(event.data)[0];
229 if (tokens[0] === 'TURN') {
230 game.turn_complete = false;
233 game.turn = parseInt(tokens[1]);
234 } else if (tokens[0] === 'THING') {
235 let t = game.get_thing(tokens[3], true);
236 t.position = parser.parse_yx(tokens[1]);
238 } else if (tokens[0] === 'THING_NAME') {
239 let t = game.get_thing(tokens[1], false);
243 } else if (tokens[0] === 'THING_CHAR') {
244 let t = game.get_thing(tokens[1], false);
246 t.player_char = tokens[2];
248 } else if (tokens[0] === 'TASKS') {
249 game.tasks = tokens[1].split(',')
250 } else if (tokens[0] === 'THING_TYPE') {
251 game.thing_types[tokens[1]] = tokens[2]
252 } else if (tokens[0] === 'TERRAIN') {
253 game.terrains[tokens[1]] = tokens[2]
254 } else if (tokens[0] === 'MAP') {
255 game.map_geometry = tokens[1];
257 game.map_size = parser.parse_yx(tokens[2]);
259 } else if (tokens[0] === 'FOV') {
261 } else if (tokens[0] === 'MAP_CONTROL') {
262 game.map_control = tokens[1]
263 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
264 game.turn_complete = true;
265 explorer.empty_info_db();
266 if (tui.mode == mode_post_login_wait) {
267 tui.switch_mode(mode_play);
268 } else if (tui.mode == mode_study) {
269 explorer.query_info();
272 } else if (tokens[0] === 'CHAT') {
273 tui.log_msg('# ' + tokens[1], 1);
274 } else if (tokens[0] === 'PLAYER_ID') {
275 game.player_id = parseInt(tokens[1]);
276 } else if (tokens[0] === 'LOGIN_OK') {
277 this.send(['GET_GAMESTATE']);
278 tui.switch_mode(mode_post_login_wait);
279 } else if (tokens[0] === 'PORTAL') {
280 let position = parser.parse_yx(tokens[1]);
281 game.portals[position] = tokens[2];
282 } else if (tokens[0] === 'ANNOTATION') {
283 let position = parser.parse_yx(tokens[1]);
284 explorer.update_info_db(position, tokens[2]);
285 } else if (tokens[0] === 'UNHANDLED_INPUT') {
286 tui.log_msg('? unknown command');
287 } else if (tokens[0] === 'PLAY_ERROR') {
288 terminal.blink_screen();
289 } else if (tokens[0] === 'ARGUMENT_ERROR') {
290 tui.log_msg('? syntax error: ' + tokens[1]);
291 } else if (tokens[0] === 'GAME_ERROR') {
292 tui.log_msg('? game error: ' + tokens[1]);
293 } else if (tokens[0] === 'PONG') {
296 tui.log_msg('? unhandled input: ' + event.data);
302 quote: function(str) {
304 for (let i = 0; i < str.length; i++) {
306 if (['"', '\\'].includes(c)) {
312 return quoted.join('');
314 to_yx: function(yx_coordinate) {
315 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
317 untokenize: function(tokens) {
318 let quoted_tokens = [];
319 for (let token of tokens) {
320 quoted_tokens.push(this.quote(token));
322 return quoted_tokens.join(" ");
327 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
329 this.has_input_prompt = has_input_prompt;
330 this.shows_info= shows_info;
331 this.is_intro = is_intro;
332 this.help_intro = help_intro;
335 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
336 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
337 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
338 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);
339 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);
340 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
341 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);
342 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);
343 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);
344 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);
347 mode: mode_waiting_for_server,
351 window_width: terminal.cols / 2,
358 this.inputEl = document.getElementById("input");
359 this.inputEl.focus();
360 this.recalc_input_lines();
361 this.height_header = this.height_turn_line + this.height_mode_line;
362 this.log_msg("@ waiting for server connection ...");
365 init_keys: function() {
367 for (let key_selector of key_selectors) {
368 this.keys[key_selector.id.slice(4)] = key_selector.value;
370 this.movement_keys = {
371 [this.keys.square_move_up]: 'UP',
372 [this.keys.square_move_left]: 'LEFT',
373 [this.keys.square_move_down]: 'DOWN',
374 [this.keys.square_move_right]: 'RIGHT'
376 if (game.map_geometry == 'Hex') {
377 this.movement_keys = {
378 [this.keys.hex_move_upleft]: 'UPLEFT',
379 [this.keys.hex_move_upright]: 'UPRIGHT',
380 [this.keys.hex_move_right]: 'RIGHT',
381 [this.keys.hex_move_downright]: 'DOWNRIGHT',
382 [this.keys.hex_move_downleft]: 'DOWNLEFT',
383 [this.keys.hex_move_left]: 'LEFT'
387 switch_mode: function(mode) {
388 this.show_help = false;
389 this.map_mode = 'terrain';
390 if (mode.shows_info && game.player_id in game.things) {
391 explorer.position = game.things[game.player_id].position;
395 this.restore_input_values();
396 document.getElementById("take_thing").disabled = true;
397 document.getElementById("drop_thing").disabled = true;
398 document.getElementById("flatten").disabled = true;
399 document.getElementById("teleport").disabled = true;
400 document.getElementById("toggle_map_mode").disabled = true;
401 document.getElementById("switch_to_chat").disabled = true;
402 document.getElementById("switch_to_play").disabled = true;
403 document.getElementById("switch_to_study").disabled = true;
404 document.getElementById("switch_to_edit").disabled = true;
405 document.getElementById("switch_to_portal").disabled = true;
406 document.getElementById("switch_to_annotate").disabled = true;
407 document.getElementById("switch_to_password").disabled = true;
408 document.getElementById("move_left").disabled = true;
409 document.getElementById("move_upleft").disabled = true;
410 document.getElementById("move_up").disabled = true;
411 document.getElementById("move_upright").disabled = true;
412 document.getElementById("move_downleft").disabled = true;
413 document.getElementById("move_down").disabled = true;
414 document.getElementById("move_downright").disabled = true;
415 document.getElementById("move_right").disabled = true;
416 if (mode == mode_play || mode == mode_study) {
417 document.getElementById("move_left").disabled = false;
418 document.getElementById("move_right").disabled = false;
419 if (game.map_geometry == 'Hex') {
420 document.getElementById("move_upleft").disabled = false;
421 document.getElementById("move_upright").disabled = false;
422 document.getElementById("move_downleft").disabled = false;
423 document.getElementById("move_downright").disabled = false;
425 document.getElementById("move_up").disabled = false;
426 document.getElementById("move_down").disabled = false;
429 if (!mode.is_intro && mode != mode_play) {
430 document.getElementById("switch_to_play").disabled = false;
432 if (!mode.is_intro && mode != mode_study) {
433 document.getElementById("switch_to_study").disabled = false;
435 if (!mode.is_intro && mode != mode_chat) {
436 document.getElementById("switch_to_chat").disabled = false;
438 if (mode == mode_login) {
439 if (this.login_name) {
440 server.send(['LOGIN', this.login_name]);
442 this.log_msg("? need login name");
444 } else if (mode == mode_play) {
445 if (game.tasks.includes('PICK_UP')) {
446 document.getElementById("take_thing").disabled = false;
448 if (game.tasks.includes('DROP')) {
449 document.getElementById("drop_thing").disabled = false;
451 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
452 document.getElementById("flatten").disabled = false;
454 if (game.tasks.includes('MOVE')) {
456 document.getElementById("teleport").disabled = false;
457 document.getElementById("switch_to_annotate").disabled = false;
458 document.getElementById("switch_to_edit").disabled = false;
459 document.getElementById("switch_to_portal").disabled = false;
460 document.getElementById("switch_to_password").disabled = false;
461 } else if (mode == mode_study) {
462 document.getElementById("toggle_map_mode").disabled = false;
463 } else if (mode == mode_edit) {
464 this.show_help = true;
468 restore_input_values: function() {
469 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
470 let info = explorer.info_db[explorer.position];
471 if (info != "(none)") {
472 this.inputEl.value = info;
473 this.recalc_input_lines();
475 } else if (this.mode == mode_portal && explorer.position in game.portals) {
476 let portal = game.portals[explorer.position]
477 this.inputEl.value = portal;
478 this.recalc_input_lines();
479 } else if (this.mode == mode_password) {
480 this.inputEl.value = this.password;
481 this.recalc_input_lines();
484 empty_input: function(str) {
485 this.inputEl.value = "";
486 if (this.mode.has_input_prompt) {
487 this.recalc_input_lines();
489 this.height_input = 0;
492 recalc_input_lines: function() {
493 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
494 this.height_input = this.input_lines.length;
496 msg_into_lines_of_width: function(msg, width) {
499 for (let i = 0, x = 0; i < msg.length; i++, x++) {
500 if (x >= width || msg[i] == "\n") {
505 if (msg[i] != "\n") {
512 log_msg: function(msg) {
514 while (this.log.length > 100) {
519 draw_map: function() {
520 let map_lines_split = [];
522 let map_content = game.map;
523 if (this.map_mode == 'control') {
524 map_content = game.map_control;
526 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
527 if (j == game.map_size[1]) {
528 map_lines_split.push(line);
532 line.push(map_content[i] + ' ');
534 map_lines_split.push(line);
535 if (this.map_mode == 'terrain') {
536 for (const p in game.portals) {
537 let coordinate = p.split(',')
538 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
540 let used_positions = [];
541 for (const thing_id in game.things) {
542 let t = game.things[thing_id];
543 let symbol = game.thing_types[t.type_];
546 meta_char = t.player_char;
548 if (used_positions.includes(t.position.toString())) {
551 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
552 used_positions.push(t.position.toString());
555 if (tui.mode.shows_info) {
556 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
559 if (game.map_geometry == 'Square') {
560 for (let line_split of map_lines_split) {
561 map_lines.push(line_split.join(''));
563 } else if (game.map_geometry == 'Hex') {
565 for (let line_split of map_lines_split) {
566 map_lines.push(' '.repeat(indent) + line_split.join(''));
574 let window_center = [terminal.rows / 2, this.window_width / 2];
575 let player = game.things[game.player_id];
576 let center_position = [player.position[0], player.position[1]];
577 if (tui.mode.shows_info) {
578 center_position = [explorer.position[0], explorer.position[1]];
580 center_position[1] = center_position[1] * 2;
581 let offset = [center_position[0] - window_center[0],
582 center_position[1] - window_center[1]]
583 if (game.map_geometry == 'Hex' && offset[0] % 2) {
586 let term_y = Math.max(0, -offset[0]);
587 let term_x = Math.max(0, -offset[1]);
588 let map_y = Math.max(0, offset[0]);
589 let map_x = Math.max(0, offset[1]);
590 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
591 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
592 terminal.write(term_y, term_x, to_draw);
595 draw_mode_line: function() {
596 let help = 'hit [' + this.keys.help + '] for help';
597 if (this.mode.has_input_prompt) {
598 help = 'enter /help for help';
600 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
602 draw_turn_line: function(n) {
603 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
605 draw_history: function() {
606 let log_display_lines = [];
607 for (let line of this.log) {
608 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
610 for (let y = terminal.rows - 1 - this.height_input,
611 i = log_display_lines.length - 1;
612 y >= this.height_header && i >= 0;
614 terminal.write(y, this.window_width, log_display_lines[i]);
617 draw_info: function() {
618 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
619 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
620 terminal.write(y, this.window_width, lines[i]);
623 draw_input: function() {
624 if (this.mode.has_input_prompt) {
625 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
626 terminal.write(y, this.window_width, this.input_lines[i]);
630 draw_help: function() {
631 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
632 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
633 if (this.mode == mode_play) {
634 content += "Available actions:\n";
635 if (game.tasks.includes('MOVE')) {
636 content += "[" + movement_keys_desc + "] – move player\n";
638 if (game.tasks.includes('PICK_UP')) {
639 content += "[" + this.keys.take_thing + "] – take thing under player\n";
641 if (game.tasks.includes('DROP')) {
642 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
644 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
645 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
647 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
648 content += '\nOther modes available from here:\n';
649 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
650 content += '[' + this.keys.switch_to_study + '] – study mode\n';
651 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
652 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
653 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
654 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
655 } else if (this.mode == mode_study) {
656 content += "Available actions:\n";
657 content += '[' + movement_keys_desc + '] – move question mark\n';
658 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
659 content += '\nOther modes available from here:\n';
660 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
661 content += '[' + this.keys.switch_to_play + '] – play mode\n';
662 } else if (this.mode == mode_chat) {
663 content += '/nick NAME – re-name yourself to NAME\n';
664 //content += '/msg USER TEXT – send TEXT to USER\n';
665 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
666 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
669 if (!this.mode.has_input_prompt) {
670 start_x = this.window_width
672 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
673 let lines = this.msg_into_lines_of_width(content, this.window_width);
674 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
675 terminal.write(y, start_x, lines[i]);
678 full_refresh: function() {
679 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
680 if (this.mode.is_intro) {
684 if (game.turn_complete) {
686 this.draw_turn_line();
688 this.draw_mode_line();
689 if (this.mode.shows_info) {
696 if (this.show_help) {
708 this.map_control = "";
709 this.map_size = [0,0];
714 get_thing: function(id_, create_if_not_found=false) {
715 if (id_ in game.things) {
716 return game.things[id_];
717 } else if (create_if_not_found) {
718 let t = new Thing([0,0]);
719 game.things[id_] = t;
723 move: function(start_position, direction) {
724 let target = [start_position[0], start_position[1]];
725 if (direction == 'LEFT') {
727 } else if (direction == 'RIGHT') {
729 } else if (game.map_geometry == 'Square') {
730 if (direction == 'UP') {
732 } else if (direction == 'DOWN') {
735 } else if (game.map_geometry == 'Hex') {
736 let start_indented = start_position[0] % 2;
737 if (direction == 'UPLEFT') {
739 if (!start_indented) {
742 } else if (direction == 'UPRIGHT') {
744 if (start_indented) {
747 } else if (direction == 'DOWNLEFT') {
749 if (!start_indented) {
752 } else if (direction == 'DOWNRIGHT') {
754 if (start_indented) {
759 if (target[0] < 0 || target[1] < 0 ||
760 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
765 teleport: function() {
766 let player = this.get_thing(game.player_id);
767 if (player.position in this.portals) {
768 server.reconnect_to(this.portals[player.position]);
770 terminal.blink_screen();
771 tui.log_msg('? not standing on portal')
779 server.init(websocket_location);
784 move: function(direction) {
785 let target = game.move(this.position, direction);
787 this.position = target
790 terminal.blink_screen();
793 update_info_db: function(yx, str) {
794 this.info_db[yx] = str;
795 if (tui.mode == mode_study) {
799 empty_info_db: function() {
801 if (tui.mode == mode_study) {
805 query_info: function() {
806 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
808 get_info: function() {
809 let position_i = this.position[0] * game.map_size[1] + this.position[1];
810 if (game.fov[position_i] != '.') {
811 return 'outside field of view';
814 let terrain_char = game.map[position_i]
815 let terrain_desc = '?'
816 if (game.terrains[terrain_char]) {
817 terrain_desc = game.terrains[terrain_char];
819 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
820 for (let t_id in game.things) {
821 let t = game.things[t_id];
822 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
823 let symbol = game.thing_types[t.type_];
824 info += "THING: " + t.type_ + " / " + symbol;
826 info += t.player_char;
829 info += " (" + t.name_ + ")";
834 if (this.position in game.portals) {
835 info += "PORTAL: " + game.portals[this.position] + "\n";
837 if (this.position in this.info_db) {
838 info += "ANNOTATIONS: " + this.info_db[this.position];
844 annotate: function(msg) {
845 if (msg.length == 0) {
846 msg = " "; // triggers annotation deletion
848 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
850 set_portal: function(msg) {
851 if (msg.length == 0) {
852 msg = " "; // triggers portal deletion
854 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
858 tui.inputEl.addEventListener('input', (event) => {
859 if (tui.mode.has_input_prompt) {
860 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
861 if (tui.inputEl.value.length > max_length) {
862 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
864 tui.recalc_input_lines();
865 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
866 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
867 tui.switch_mode(mode_play);
872 tui.inputEl.addEventListener('keydown', (event) => {
873 tui.show_help = false;
874 if (event.key == 'Enter') {
875 event.preventDefault();
877 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
878 tui.show_help = true;
880 tui.restore_input_values();
881 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
882 tui.show_help = true;
883 } else if (tui.mode == mode_login && event.key == 'Enter') {
884 tui.login_name = tui.inputEl.value;
885 server.send(['LOGIN', tui.inputEl.value]);
887 } else if (tui.mode == mode_portal && event.key == 'Enter') {
888 explorer.set_portal(tui.inputEl.value);
889 tui.switch_mode(mode_play);
890 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
891 explorer.annotate(tui.inputEl.value);
892 tui.switch_mode(mode_play);
893 } else if (tui.mode == mode_password && event.key == 'Enter') {
894 if (tui.inputEl.value.length == 0) {
895 tui.inputEl.value = " ";
897 tui.password = tui.inputEl.value
898 tui.switch_mode(mode_play);
899 } else if (tui.mode == mode_chat && event.key == 'Enter') {
900 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
901 if (tokens.length > 0 && tokens[0].length > 0) {
902 if (tui.inputEl.value[0][0] == '/') {
903 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
904 tui.switch_mode(mode_play);
905 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
906 tui.switch_mode(mode_study);
907 } else if (tokens[0].slice(1) == 'nick') {
908 if (tokens.length > 1) {
909 server.send(['NICK', tokens[1]]);
911 tui.log_msg('? need new name');
913 //} else if (tokens[0].slice(1) == 'msg') {
914 // if (tokens.length > 2) {
915 // let msg = tui.inputEl.value.slice(token_starts[2]);
916 // server.send(['QUERY', tokens[1], msg]);
918 // tui.log_msg('? need message target and message');
921 tui.log_msg('? unknown command');
924 server.send(['ALL', tui.inputEl.value]);
926 } else if (tui.inputEl.valuelength > 0) {
927 server.send(['ALL', tui.inputEl.value]);
930 } else if (tui.mode == mode_play) {
931 if (event.key === tui.keys.switch_to_chat) {
932 event.preventDefault();
933 tui.switch_mode(mode_chat);
934 } else if (event.key === tui.keys.switch_to_edit
935 && game.tasks.includes('WRITE')) {
936 event.preventDefault();
937 tui.switch_mode(mode_edit);
938 } else if (event.key === tui.keys.switch_to_study) {
939 tui.switch_mode(mode_study);
940 } else if (event.key === tui.keys.switch_to_password) {
941 event.preventDefault();
942 tui.switch_mode(mode_password);
943 } else if (event.key === tui.keys.flatten
944 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
945 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
946 } else if (event.key === tui.keys.take_thing
947 && game.tasks.includes('PICK_UP')) {
948 server.send(["TASK:PICK_UP"]);
949 } else if (event.key === tui.keys.drop_thing
950 && game.tasks.includes('DROP')) {
951 server.send(["TASK:DROP"]);
952 } else if (event.key in tui.movement_keys
953 && game.tasks.includes('MOVE')) {
954 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
955 } else if (event.key === tui.keys.teleport) {
957 } else if (event.key === tui.keys.switch_to_portal) {
958 event.preventDefault();
959 tui.switch_mode(mode_portal);
960 } else if (event.key === tui.keys.switch_to_annotate) {
961 event.preventDefault();
962 tui.switch_mode(mode_annotate);
964 } else if (tui.mode == mode_study) {
965 if (event.key === tui.keys.switch_to_chat) {
966 event.preventDefault();
967 tui.switch_mode(mode_chat);
968 } else if (event.key == tui.keys.switch_to_play) {
969 tui.switch_mode(mode_play);
970 } else if (event.key in tui.movement_keys) {
971 explorer.move(tui.movement_keys[event.key]);
972 } else if (event.key == tui.keys.toggle_map_mode) {
973 if (tui.map_mode == 'terrain') {
974 tui.map_mode = 'control';
976 tui.map_mode = 'terrain';
983 rows_selector.addEventListener('input', function() {
984 if (rows_selector.value % 4 != 0) {
987 window.localStorage.setItem(rows_selector.id, rows_selector.value);
988 terminal.initialize();
991 cols_selector.addEventListener('input', function() {
992 if (cols_selector.value % 4 != 0) {
995 window.localStorage.setItem(cols_selector.id, cols_selector.value);
996 terminal.initialize();
997 tui.window_width = terminal.cols / 2,
1000 for (let key_selector of key_selectors) {
1001 key_selector.addEventListener('input', function() {
1002 window.localStorage.setItem(key_selector.id, key_selector.value);
1006 window.setInterval(function() {
1007 if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
1008 || document.activeElement.id.startsWith('key_'))) {
1009 tui.inputEl.focus();
1012 window.setInterval(function() {
1013 if (server.connected) {
1014 server.send(['PING']);
1016 server.reconnect_to(server.url);
1017 tui.log_msg('@ attempting reconnect …')
1021 document.getElementById("help").onclick = function() {
1022 tui.show_help = true;
1025 document.getElementById("switch_to_play").onclick = function() {
1026 tui.switch_mode(mode_play);
1029 document.getElementById("switch_to_study").onclick = function() {
1030 tui.switch_mode(mode_study);
1033 document.getElementById("switch_to_chat").onclick = function() {
1034 tui.switch_mode(mode_chat);
1037 document.getElementById("switch_to_password").onclick = function() {
1038 tui.switch_mode(mode_password);
1041 document.getElementById("switch_to_edit").onclick = function() {
1042 tui.switch_mode(mode_edit);
1045 document.getElementById("switch_to_annotate").onclick = function() {
1046 tui.switch_mode(mode_annotate);
1049 document.getElementById("switch_to_portal").onclick = function() {
1050 tui.switch_mode(mode_portal);
1053 document.getElementById("toggle_map_mode").onclick = function() {
1054 if (tui.map_mode == 'terrain') {
1055 tui.map_mode = 'control';
1057 tui.map_mode = 'terrain';
1061 document.getElementById("take_thing").onclick = function() {
1062 server.send(['TASK:PICK_UP']);
1064 document.getElementById("drop_thing").onclick = function() {
1065 server.send(['TASK:DROP']);
1067 document.getElementById("flatten").onclick = function() {
1068 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1070 document.getElementById("teleport").onclick = function() {
1073 document.getElementById("move_upleft").onclick = function() {
1074 if (tui.mode == mode_play) {
1075 server.send(['TASK:MOVE', 'UPLEFT']);
1077 explorer.move('UPLEFT');
1080 document.getElementById("move_left").onclick = function() {
1081 if (tui.mode == mode_play) {
1082 server.send(['TASK:MOVE', 'LEFT']);
1084 explorer.move('LEFT');
1087 document.getElementById("move_downleft").onclick = function() {
1088 if (tui.mode == mode_play) {
1089 server.send(['TASK:MOVE', 'DOWNLEFT']);
1091 explorer.move('DOWNLEFT');
1094 document.getElementById("move_down").onclick = function() {
1095 if (tui.mode == mode_play) {
1096 server.send(['TASK:MOVE', 'DOWN']);
1098 explorer.move('DOWN');
1101 document.getElementById("move_up").onclick = function() {
1102 if (tui.mode == mode_play) {
1103 server.send(['TASK:MOVE', 'UP']);
1105 explorer.move('UP');
1108 document.getElementById("move_upright").onclick = function() {
1109 if (tui.mode == mode_play) {
1110 server.send(['TASK:MOVE', 'UPRIGHT']);
1112 explorer.move('UPRIGHT');
1115 document.getElementById("move_right").onclick = function() {
1116 if (tui.mode == mode_play) {
1117 server.send(['TASK:MOVE', 'RIGHT']);
1119 explorer.move('RIGHT');
1122 document.getElementById("move_downright").onclick = function() {
1123 if (tui.mode == mode_play) {
1124 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1126 explorer.move('DOWNRIGHT');