7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/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 tui.log_msg('? ' + tokens[1]);
283 terminal.blink_screen();
284 } else if (tokens[0] === 'ARGUMENT_ERROR') {
285 tui.log_msg('? syntax error: ' + tokens[1]);
286 } else if (tokens[0] === 'GAME_ERROR') {
287 tui.log_msg('? game error: ' + tokens[1]);
288 } else if (tokens[0] === 'PONG') {
291 tui.log_msg('? unhandled input: ' + event.data);
297 quote: function(str) {
299 for (let i = 0; i < str.length; i++) {
301 if (['"', '\\'].includes(c)) {
307 return quoted.join('');
309 to_yx: function(yx_coordinate) {
310 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
312 untokenize: function(tokens) {
313 let quoted_tokens = [];
314 for (let token of tokens) {
315 quoted_tokens.push(this.quote(token));
317 return quoted_tokens.join(" ");
322 constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
324 this.has_input_prompt = has_input_prompt;
325 this.shows_info= shows_info;
326 this.is_intro = is_intro;
327 this.help_intro = help_intro;
330 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
331 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
332 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
333 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);
334 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);
335 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
336 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);
337 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);
338 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);
339 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);
342 mode: mode_waiting_for_server,
346 window_width: terminal.cols / 2,
353 this.inputEl = document.getElementById("input");
354 this.inputEl.focus();
355 this.recalc_input_lines();
356 this.height_header = this.height_turn_line + this.height_mode_line;
357 this.log_msg("@ waiting for server connection ...");
360 init_keys: function() {
362 for (let key_selector of key_selectors) {
363 this.keys[key_selector.id.slice(4)] = key_selector.value;
365 this.movement_keys = {
366 [this.keys.square_move_up]: 'UP',
367 [this.keys.square_move_left]: 'LEFT',
368 [this.keys.square_move_down]: 'DOWN',
369 [this.keys.square_move_right]: 'RIGHT'
371 if (game.map_geometry == 'Hex') {
372 this.movement_keys = {
373 [this.keys.hex_move_upleft]: 'UPLEFT',
374 [this.keys.hex_move_upright]: 'UPRIGHT',
375 [this.keys.hex_move_right]: 'RIGHT',
376 [this.keys.hex_move_downright]: 'DOWNRIGHT',
377 [this.keys.hex_move_downleft]: 'DOWNLEFT',
378 [this.keys.hex_move_left]: 'LEFT'
382 switch_mode: function(mode) {
383 this.inputEl.focus();
384 this.show_help = false;
385 this.map_mode = 'terrain';
386 if (mode.shows_info && game.player_id in game.things) {
387 explorer.position = game.things[game.player_id].position;
391 this.restore_input_values();
392 document.getElementById("take_thing").disabled = true;
393 document.getElementById("drop_thing").disabled = true;
394 document.getElementById("flatten").disabled = true;
395 document.getElementById("teleport").disabled = true;
396 document.getElementById("toggle_map_mode").disabled = true;
397 document.getElementById("switch_to_chat").disabled = true;
398 document.getElementById("switch_to_play").disabled = true;
399 document.getElementById("switch_to_study").disabled = true;
400 document.getElementById("switch_to_edit").disabled = true;
401 document.getElementById("switch_to_portal").disabled = true;
402 document.getElementById("switch_to_annotate").disabled = true;
403 document.getElementById("switch_to_password").disabled = true;
404 document.getElementById("move_left").disabled = true;
405 document.getElementById("move_upleft").disabled = true;
406 document.getElementById("move_up").disabled = true;
407 document.getElementById("move_upright").disabled = true;
408 document.getElementById("move_downleft").disabled = true;
409 document.getElementById("move_down").disabled = true;
410 document.getElementById("move_downright").disabled = true;
411 document.getElementById("move_right").disabled = true;
412 if (mode == mode_play || mode == mode_study) {
413 document.getElementById("move_left").disabled = false;
414 document.getElementById("move_right").disabled = false;
415 if (game.map_geometry == 'Hex') {
416 document.getElementById("move_upleft").disabled = false;
417 document.getElementById("move_upright").disabled = false;
418 document.getElementById("move_downleft").disabled = false;
419 document.getElementById("move_downright").disabled = false;
421 document.getElementById("move_up").disabled = false;
422 document.getElementById("move_down").disabled = false;
425 if (!mode.is_intro && mode != mode_play) {
426 document.getElementById("switch_to_play").disabled = false;
428 if (!mode.is_intro && mode != mode_study) {
429 document.getElementById("switch_to_study").disabled = false;
431 if (!mode.is_intro && mode != mode_chat) {
432 document.getElementById("switch_to_chat").disabled = false;
434 if (mode == mode_login) {
435 if (this.login_name) {
436 server.send(['LOGIN', this.login_name]);
438 this.log_msg("? need login name");
440 } else if (mode == mode_play) {
441 if (game.tasks.includes('PICK_UP')) {
442 document.getElementById("take_thing").disabled = false;
444 if (game.tasks.includes('DROP')) {
445 document.getElementById("drop_thing").disabled = false;
447 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
448 document.getElementById("flatten").disabled = false;
450 if (game.tasks.includes('MOVE')) {
452 document.getElementById("teleport").disabled = false;
453 document.getElementById("switch_to_annotate").disabled = false;
454 document.getElementById("switch_to_edit").disabled = false;
455 document.getElementById("switch_to_portal").disabled = false;
456 document.getElementById("switch_to_password").disabled = false;
457 } else if (mode == mode_study) {
458 document.getElementById("toggle_map_mode").disabled = false;
459 } else if (mode == mode_edit) {
460 this.show_help = true;
464 restore_input_values: function() {
465 if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
466 let info = explorer.info_db[explorer.position];
467 if (info != "(none)") {
468 this.inputEl.value = info;
469 this.recalc_input_lines();
471 } else if (this.mode == mode_portal && explorer.position in game.portals) {
472 let portal = game.portals[explorer.position]
473 this.inputEl.value = portal;
474 this.recalc_input_lines();
475 } else if (this.mode == mode_password) {
476 this.inputEl.value = this.password;
477 this.recalc_input_lines();
480 empty_input: function(str) {
481 this.inputEl.value = "";
482 if (this.mode.has_input_prompt) {
483 this.recalc_input_lines();
485 this.height_input = 0;
488 recalc_input_lines: function() {
489 this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
490 this.height_input = this.input_lines.length;
492 msg_into_lines_of_width: function(msg, width) {
495 for (let i = 0, x = 0; i < msg.length; i++, x++) {
496 if (x >= width || msg[i] == "\n") {
501 if (msg[i] != "\n") {
508 log_msg: function(msg) {
510 while (this.log.length > 100) {
515 draw_map: function() {
516 let map_lines_split = [];
518 let map_content = game.map;
519 if (this.map_mode == 'control') {
520 map_content = game.map_control;
522 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
523 if (j == game.map_size[1]) {
524 map_lines_split.push(line);
528 line.push(map_content[i] + ' ');
530 map_lines_split.push(line);
531 if (this.map_mode == 'terrain') {
532 for (const p in game.portals) {
533 let coordinate = p.split(',')
534 map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
536 let used_positions = [];
537 for (const thing_id in game.things) {
538 let t = game.things[thing_id];
539 let symbol = game.thing_types[t.type_];
542 meta_char = t.player_char;
544 if (used_positions.includes(t.position.toString())) {
547 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
548 used_positions.push(t.position.toString());
551 if (tui.mode.shows_info) {
552 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
555 if (game.map_geometry == 'Square') {
556 for (let line_split of map_lines_split) {
557 map_lines.push(line_split.join(''));
559 } else if (game.map_geometry == 'Hex') {
561 for (let line_split of map_lines_split) {
562 map_lines.push(' '.repeat(indent) + line_split.join(''));
570 let window_center = [terminal.rows / 2, this.window_width / 2];
571 let player = game.things[game.player_id];
572 let center_position = [player.position[0], player.position[1]];
573 if (tui.mode.shows_info) {
574 center_position = [explorer.position[0], explorer.position[1]];
576 center_position[1] = center_position[1] * 2;
577 let offset = [center_position[0] - window_center[0],
578 center_position[1] - window_center[1]]
579 if (game.map_geometry == 'Hex' && offset[0] % 2) {
582 let term_y = Math.max(0, -offset[0]);
583 let term_x = Math.max(0, -offset[1]);
584 let map_y = Math.max(0, offset[0]);
585 let map_x = Math.max(0, offset[1]);
586 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
587 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
588 terminal.write(term_y, term_x, to_draw);
591 draw_mode_line: function() {
592 let help = 'hit [' + this.keys.help + '] for help';
593 if (this.mode.has_input_prompt) {
594 help = 'enter /help for help';
596 terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
598 draw_turn_line: function(n) {
599 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
601 draw_history: function() {
602 let log_display_lines = [];
603 for (let line of this.log) {
604 log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
606 for (let y = terminal.rows - 1 - this.height_input,
607 i = log_display_lines.length - 1;
608 y >= this.height_header && i >= 0;
610 terminal.write(y, this.window_width, log_display_lines[i]);
613 draw_info: function() {
614 let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
615 for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
616 terminal.write(y, this.window_width, lines[i]);
619 draw_input: function() {
620 if (this.mode.has_input_prompt) {
621 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
622 terminal.write(y, this.window_width, this.input_lines[i]);
626 draw_help: function() {
627 let movement_keys_desc = Object.keys(this.movement_keys).join(',');
628 let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
629 if (this.mode == mode_play) {
630 content += "Available actions:\n";
631 if (game.tasks.includes('MOVE')) {
632 content += "[" + movement_keys_desc + "] – move player\n";
634 if (game.tasks.includes('PICK_UP')) {
635 content += "[" + this.keys.take_thing + "] – take thing under player\n";
637 if (game.tasks.includes('DROP')) {
638 content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
640 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
641 content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
643 content += "[" + tui.keys.teleport + "] – teleport to other space\n";
644 content += '\nOther modes available from here:\n';
645 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
646 content += '[' + this.keys.switch_to_study + '] – study mode\n';
647 content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
648 content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
649 content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
650 content += '[' + this.keys.switch_to_password + '] – password input mode\n';
651 } else if (this.mode == mode_study) {
652 content += "Available actions:\n";
653 content += '[' + movement_keys_desc + '] – move question mark\n';
654 content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
655 content += '\nOther modes available from here:\n';
656 content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
657 content += '[' + this.keys.switch_to_play + '] – play mode\n';
658 } else if (this.mode == mode_chat) {
659 content += '/nick NAME – re-name yourself to NAME\n';
660 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
661 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
664 if (!this.mode.has_input_prompt) {
665 start_x = this.window_width
667 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
668 let lines = this.msg_into_lines_of_width(content, this.window_width);
669 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
670 terminal.write(y, start_x, lines[i]);
673 full_refresh: function() {
674 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
675 if (this.mode.is_intro) {
679 if (game.turn_complete) {
681 this.draw_turn_line();
683 this.draw_mode_line();
684 if (this.mode.shows_info) {
691 if (this.show_help) {
703 this.map_control = "";
704 this.map_size = [0,0];
709 get_thing: function(id_, create_if_not_found=false) {
710 if (id_ in game.things) {
711 return game.things[id_];
712 } else if (create_if_not_found) {
713 let t = new Thing([0,0]);
714 game.things[id_] = t;
718 move: function(start_position, direction) {
719 let target = [start_position[0], start_position[1]];
720 if (direction == 'LEFT') {
722 } else if (direction == 'RIGHT') {
724 } else if (game.map_geometry == 'Square') {
725 if (direction == 'UP') {
727 } else if (direction == 'DOWN') {
730 } else if (game.map_geometry == 'Hex') {
731 let start_indented = start_position[0] % 2;
732 if (direction == 'UPLEFT') {
734 if (!start_indented) {
737 } else if (direction == 'UPRIGHT') {
739 if (start_indented) {
742 } else if (direction == 'DOWNLEFT') {
744 if (!start_indented) {
747 } else if (direction == 'DOWNRIGHT') {
749 if (start_indented) {
754 if (target[0] < 0 || target[1] < 0 ||
755 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
760 teleport: function() {
761 let player = this.get_thing(game.player_id);
762 if (player.position in this.portals) {
763 server.reconnect_to(this.portals[player.position]);
765 terminal.blink_screen();
766 tui.log_msg('? not standing on portal')
774 server.init(websocket_location);
779 move: function(direction) {
780 let target = game.move(this.position, direction);
782 this.position = target
785 terminal.blink_screen();
788 update_info_db: function(yx, str) {
789 this.info_db[yx] = str;
790 if (tui.mode == mode_study) {
794 empty_info_db: function() {
796 if (tui.mode == mode_study) {
800 query_info: function() {
801 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
803 get_info: function() {
804 let position_i = this.position[0] * game.map_size[1] + this.position[1];
805 if (game.fov[position_i] != '.') {
806 return 'outside field of view';
809 let terrain_char = game.map[position_i]
810 let terrain_desc = '?'
811 if (game.terrains[terrain_char]) {
812 terrain_desc = game.terrains[terrain_char];
814 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
815 for (let t_id in game.things) {
816 let t = game.things[t_id];
817 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
818 let symbol = game.thing_types[t.type_];
819 info += "THING: " + t.type_ + " / " + symbol;
821 info += t.player_char;
824 info += " (" + t.name_ + ")";
829 if (this.position in game.portals) {
830 info += "PORTAL: " + game.portals[this.position] + "\n";
832 if (this.position in this.info_db) {
833 info += "ANNOTATIONS: " + this.info_db[this.position];
839 annotate: function(msg) {
840 if (msg.length == 0) {
841 msg = " "; // triggers annotation deletion
843 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
845 set_portal: function(msg) {
846 if (msg.length == 0) {
847 msg = " "; // triggers portal deletion
849 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
853 tui.inputEl.addEventListener('input', (event) => {
854 if (tui.mode.has_input_prompt) {
855 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
856 if (tui.inputEl.value.length > max_length) {
857 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
859 tui.recalc_input_lines();
860 } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
861 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
862 tui.switch_mode(mode_play);
866 tui.inputEl.addEventListener('keydown', (event) => {
867 tui.show_help = false;
868 if (event.key == 'Enter') {
869 event.preventDefault();
871 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
872 tui.show_help = true;
874 tui.restore_input_values();
875 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
876 tui.show_help = true;
877 } else if (tui.mode == mode_login && event.key == 'Enter') {
878 tui.login_name = tui.inputEl.value;
879 server.send(['LOGIN', tui.inputEl.value]);
881 } else if (tui.mode == mode_portal && event.key == 'Enter') {
882 explorer.set_portal(tui.inputEl.value);
883 tui.switch_mode(mode_play);
884 } else if (tui.mode == mode_annotate && event.key == 'Enter') {
885 explorer.annotate(tui.inputEl.value);
886 tui.switch_mode(mode_play);
887 } else if (tui.mode == mode_password && event.key == 'Enter') {
888 if (tui.inputEl.value.length == 0) {
889 tui.inputEl.value = " ";
891 tui.password = tui.inputEl.value
892 tui.switch_mode(mode_play);
893 } else if (tui.mode == mode_chat && event.key == 'Enter') {
894 let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
895 if (tokens.length > 0 && tokens[0].length > 0) {
896 if (tui.inputEl.value[0][0] == '/') {
897 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
898 tui.switch_mode(mode_play);
899 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
900 tui.switch_mode(mode_study);
901 } else if (tokens[0].slice(1) == 'nick') {
902 if (tokens.length > 1) {
903 server.send(['NICK', tokens[1]]);
905 tui.log_msg('? need new name');
908 tui.log_msg('? unknown command');
911 server.send(['ALL', tui.inputEl.value]);
913 } else if (tui.inputEl.valuelength > 0) {
914 server.send(['ALL', tui.inputEl.value]);
917 } else if (tui.mode == mode_play) {
918 if (event.key === tui.keys.switch_to_chat) {
919 event.preventDefault();
920 tui.switch_mode(mode_chat);
921 } else if (event.key === tui.keys.switch_to_edit
922 && game.tasks.includes('WRITE')) {
923 event.preventDefault();
924 tui.switch_mode(mode_edit);
925 } else if (event.key === tui.keys.switch_to_study) {
926 tui.switch_mode(mode_study);
927 } else if (event.key === tui.keys.switch_to_password) {
928 event.preventDefault();
929 tui.switch_mode(mode_password);
930 } else if (event.key === tui.keys.flatten
931 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
932 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
933 } else if (event.key === tui.keys.take_thing
934 && game.tasks.includes('PICK_UP')) {
935 server.send(["TASK:PICK_UP"]);
936 } else if (event.key === tui.keys.drop_thing
937 && game.tasks.includes('DROP')) {
938 server.send(["TASK:DROP"]);
939 } else if (event.key in tui.movement_keys
940 && game.tasks.includes('MOVE')) {
941 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
942 } else if (event.key === tui.keys.teleport) {
944 } else if (event.key === tui.keys.switch_to_portal) {
945 event.preventDefault();
946 tui.switch_mode(mode_portal);
947 } else if (event.key === tui.keys.switch_to_annotate) {
948 event.preventDefault();
949 tui.switch_mode(mode_annotate);
951 } else if (tui.mode == mode_study) {
952 if (event.key === tui.keys.switch_to_chat) {
953 event.preventDefault();
954 tui.switch_mode(mode_chat);
955 } else if (event.key == tui.keys.switch_to_play) {
956 tui.switch_mode(mode_play);
957 } else if (event.key in tui.movement_keys) {
958 explorer.move(tui.movement_keys[event.key]);
959 } else if (event.key == tui.keys.toggle_map_mode) {
960 if (tui.map_mode == 'terrain') {
961 tui.map_mode = 'control';
963 tui.map_mode = 'terrain';
970 rows_selector.addEventListener('input', function() {
971 if (rows_selector.value % 4 != 0) {
974 window.localStorage.setItem(rows_selector.id, rows_selector.value);
975 terminal.initialize();
978 cols_selector.addEventListener('input', function() {
979 if (cols_selector.value % 4 != 0) {
982 window.localStorage.setItem(cols_selector.id, cols_selector.value);
983 terminal.initialize();
984 tui.window_width = terminal.cols / 2,
987 for (let key_selector of key_selectors) {
988 key_selector.addEventListener('input', function() {
989 window.localStorage.setItem(key_selector.id, key_selector.value);
993 window.setInterval(function() {
994 if (server.connected) {
995 server.send(['PING']);
997 server.reconnect_to(server.url);
998 tui.log_msg('@ attempting reconnect …')
1001 document.getElementById("terminal").onclick = function() {
1002 tui.inputEl.focus();
1004 document.getElementById("help").onclick = function() {
1005 tui.show_help = true;
1008 document.getElementById("switch_to_play").onclick = function() {
1009 tui.switch_mode(mode_play);
1012 document.getElementById("switch_to_study").onclick = function() {
1013 tui.switch_mode(mode_study);
1016 document.getElementById("switch_to_chat").onclick = function() {
1017 tui.switch_mode(mode_chat);
1020 document.getElementById("switch_to_password").onclick = function() {
1021 tui.switch_mode(mode_password);
1024 document.getElementById("switch_to_edit").onclick = function() {
1025 tui.switch_mode(mode_edit);
1028 document.getElementById("switch_to_annotate").onclick = function() {
1029 tui.switch_mode(mode_annotate);
1032 document.getElementById("switch_to_portal").onclick = function() {
1033 tui.switch_mode(mode_portal);
1036 document.getElementById("toggle_map_mode").onclick = function() {
1037 if (tui.map_mode == 'terrain') {
1038 tui.map_mode = 'control';
1040 tui.map_mode = 'terrain';
1044 document.getElementById("take_thing").onclick = function() {
1045 server.send(['TASK:PICK_UP']);
1047 document.getElementById("drop_thing").onclick = function() {
1048 server.send(['TASK:DROP']);
1050 document.getElementById("flatten").onclick = function() {
1051 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1053 document.getElementById("teleport").onclick = function() {
1056 document.getElementById("move_upleft").onclick = function() {
1057 if (tui.mode == mode_play) {
1058 server.send(['TASK:MOVE', 'UPLEFT']);
1060 explorer.move('UPLEFT');
1063 document.getElementById("move_left").onclick = function() {
1064 if (tui.mode == mode_play) {
1065 server.send(['TASK:MOVE', 'LEFT']);
1067 explorer.move('LEFT');
1070 document.getElementById("move_downleft").onclick = function() {
1071 if (tui.mode == mode_play) {
1072 server.send(['TASK:MOVE', 'DOWNLEFT']);
1074 explorer.move('DOWNLEFT');
1077 document.getElementById("move_down").onclick = function() {
1078 if (tui.mode == mode_play) {
1079 server.send(['TASK:MOVE', 'DOWN']);
1081 explorer.move('DOWN');
1084 document.getElementById("move_up").onclick = function() {
1085 if (tui.mode == mode_play) {
1086 server.send(['TASK:MOVE', 'UP']);
1088 explorer.move('UP');
1091 document.getElementById("move_upright").onclick = function() {
1092 if (tui.mode == mode_play) {
1093 server.send(['TASK:MOVE', 'UPRIGHT']);
1095 explorer.move('UPRIGHT');
1098 document.getElementById("move_right").onclick = function() {
1099 if (tui.mode == mode_play) {
1100 server.send(['TASK:MOVE', 'RIGHT']);
1102 explorer.move('RIGHT');
1105 document.getElementById("move_downright").onclick = function() {
1106 if (tui.mode == mode_play) {
1107 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1109 explorer.move('DOWNRIGHT');