A UserScript to display comments from the Unofficial Genshin Impact Map on the Official Map.
Click Here :3
A UserScript to display comments from the Unofficial Genshin Impact Map on the Official Map.
Click Here :3
| // ==UserScript== | |
| // @name Teyvat Comments | |
| // @namespace https://koding.dev/ | |
| // @version 0.3 | |
| // @description Teyvat Map comments from AppSample | |
| // @author KodingDev, atouu | |
| // @match https://act.hoyolab.com/ys/app/interactive-map/index.html* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=hoyolab.com | |
| // @grant none | |
| // | |
| // @downloadURL https://gist.githubusercontent.com/KodingDev/1f63e0bd22869292d1f7fc35be005bab/raw/script.user.js | |
| // @updateURL https://gist.githubusercontent.com/KodingDev/1f63e0bd22869292d1f7fc35be005bab/raw/script.user.js | |
| // ==/UserScript== | |
| // --- TRANSFORMATION CALCULATIONS --- | |
| /** | |
| * Adds two vectors together | |
| * | |
| * @param {[number, number]} a | |
| * @param {[number, number]} b | |
| * @returns {[number, number]} The sum of the two vectors | |
| */ | |
| const vecAdd = (a, b) => [a[0] + b[0], a[1] + b[1]]; | |
| /** | |
| * Subtracts two vectors | |
| * | |
| * @param {[number, number]} a | |
| * @param {[number, number]} b | |
| * @returns {[number, number]} The difference of the two vectors | |
| */ | |
| const vecSub = (a, b) => [a[0] - b[0], a[1] - b[1]]; | |
| /** | |
| * Scales a vector by a scalar | |
| * | |
| * @param {[number, number]} a | |
| * @param {number} s | |
| * @returns {[number, number]} The scaled vector | |
| */ | |
| const vecScale = (a, s) => [a[0] * s, a[1] * s]; | |
| /** | |
| * Calculates the length of a vector | |
| * | |
| * @param {[number, number]} x | |
| * @returns {number} The length of the vector | |
| */ | |
| const vecLength = (x) => Math.sqrt(Math.pow(x[0], 2) + Math.pow(x[1], 2)); | |
| /** | |
| * Flips the Y coordinate of a vector | |
| * | |
| * @param {[number, number]} x | |
| * @returns {[number, number]} The flipped vector | |
| */ | |
| const vecFlipY = (x) => [x[0], -x[1]]; | |
| // Define the control points for both spaces | |
| const OFFICIAL_CONTROLS = [ | |
| [-5140.75, 154.5], // Emperor of Fire and Iron | |
| [3288.5, 6761], // Serai Island - Statue of the Seven | |
| [2525.5, 8455], // Statue of the Seven - Tsurumi Island | |
| [889, -483.5], // Teleport Waypoint - Stormbearer Point | |
| ]; | |
| const APPSAMPLE_CONTROLS = [ | |
| [-0.10802, 0.18615], // Emperor of Fire and Iron | |
| [0.61536, -0.38097], // Serai Island - Statue of the Seven | |
| [0.54988, -0.52639], // Statue of the Seven - Tsurumi Island | |
| [0.40944, 0.24092], // Teleport Waypoint - Stormbearer Point | |
| ]; | |
| // On the unofficial map, +Y is north, while on the official map it's south. Flip Y on all unofficial coords. | |
| APPSAMPLE_CONTROLS.forEach((x, i) => (APPSAMPLE_CONTROLS[i] = vecFlipY(x))); | |
| // Calculate average scale based on all coordinates | |
| const OFFICIAL_TO_UNOFFICIAL_SCALE = (() => { | |
| let scale = 0; | |
| for (let i = 1; i < OFFICIAL_CONTROLS.length; i++) { | |
| const relOfficial = vecSub(OFFICIAL_CONTROLS[i], OFFICIAL_CONTROLS[0]); | |
| const relUnofficial = vecSub(APPSAMPLE_CONTROLS[i], APPSAMPLE_CONTROLS[0]); | |
| scale += vecLength(relUnofficial) / vecLength(relOfficial); | |
| } | |
| return scale / (OFFICIAL_CONTROLS.length - 1); | |
| })(); | |
| /** | |
| * Converts coordinates from the Official coordinate system to the AppSample coordinate system | |
| * | |
| * @param {[number, number]} x The coordinates in the Official coordinate system | |
| * @returns {[number, number]} The coordinates in the AppSample coordinate system | |
| */ | |
| const toUnofficial = (x) => { | |
| const relOfficial = vecSub(x, OFFICIAL_CONTROLS[0]); | |
| const relUnofficial = vecScale(relOfficial, OFFICIAL_TO_UNOFFICIAL_SCALE); | |
| const absUnofficial = vecAdd(relUnofficial, APPSAMPLE_CONTROLS[0]); | |
| return vecFlipY(absUnofficial); | |
| }; | |
| // Regex | |
| const IMGUR_REGEX = | |
| /(^(http|https):\/\/)?(i\.)?imgur.com\/((?<gallery>gallery\/)(?<galleryid>\w+)|(?<album>a\/)(?<albumid>\w+)#?)?(?<imgid>\w*)/g; | |
| // Globals | |
| const MARKER_DATA = []; | |
| /** | |
| * Calculates the distance between two points | |
| * | |
| * @param {number} x1 The X coordinate of the first point | |
| * @param {number} y1 The Y coordinate of the first point | |
| * @param {number} x2 The X coordinate of the second point | |
| * @param {number} y2 The Y coordinate of the second point | |
| * @returns {number} The distance between the two points | |
| */ | |
| function distance(x1, y1, x2, y2) { | |
| return Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2); | |
| } | |
| /** | |
| * Finds the nearest marker to the given coordinates | |
| * | |
| * @param {number} x The X coordinate in the AppSample coordinate system | |
| * @param {number} y The Y coordinate in the AppSample coordinate system | |
| * @returns {object} The nearest marker | |
| */ | |
| function findNearestMarker(x, y) { | |
| return MARKER_DATA.map((marker) => ({ | |
| ...marker, | |
| dist: distance(x, y, marker.lng, marker.lat), | |
| })).sort((a, b) => a.dist - b.dist)[0]; | |
| } | |
| /** | |
| * Gets the marker info from the AppSample API | |
| * | |
| * @param {object} marker The marker to get info for | |
| * @returns {object} The marker info | |
| */ | |
| function getMarkerInfo(marker) { | |
| const params = new URLSearchParams(); | |
| params.append("action", "get"); | |
| params.append("app", "gim"); | |
| params.append("ttl", "7000"); | |
| params.append("categoryId", marker.type); | |
| params.append("docId", marker.id); | |
| params.append("sort", ""); | |
| params.append("page", "1"); | |
| params.append("pageSize", "50"); | |
| return fetch( | |
| `https://cache-proxy.lemonapi.com/skywise/comment/v1?${params.toString()}` | |
| ).then((res) => res.json()); | |
| } | |
| /** | |
| * Waits for a condition to be true | |
| * | |
| * @param {function} condition The condition to wait for | |
| * @returns {Promise} A promise that resolves when the condition is true | |
| */ | |
| function wait(condition) { | |
| return new Promise((resolve) => { | |
| const interval = setInterval(() => { | |
| if (condition()) { | |
| clearInterval(interval); | |
| resolve(); | |
| } | |
| }, 100); | |
| }); | |
| } | |
| (async function () { | |
| "use strict"; | |
| // --- INIT --- | |
| // Fetch the AppSample marker data | |
| const markerRes = await fetch( | |
| "https://game-data.lemonapi.com/gim/markers_all.v3.json" | |
| ).then((res) => res.json()); | |
| // Update the marker data to include header information | |
| MARKER_DATA.push( | |
| ...markerRes.data.map((marker) => | |
| marker.reduce((acc, cur, i) => { | |
| acc[markerRes.headers[i]] = cur; | |
| return acc; | |
| }, {}) | |
| ) | |
| ); | |
| // --- LISTENER --- | |
| // Wait for the map to load | |
| await wait(() => document.getElementsByClassName("mhy-game-gis").length > 0); | |
| const gameGis = document.getElementsByClassName("mhy-game-gis")[0] | |
| const unwatch = gameGis.__vue__.$watch('mapListReady', (t) => { | |
| if (!t) return | |
| const leafletMap = gameGis.__vue__.map; | |
| leafletMap.on("popupopen", async (d) => { | |
| // Wait for the popup content to populate | |
| await wait(() => d.popup._content.vueInstance); | |
| const popup = d.popup._content.vueInstance; | |
| const { x_pos: x, y_pos: y } = popup.marker; | |
| // Perform a lookup of the marker | |
| const [convX, convY] = toUnofficial([x, y]); | |
| const marker = findNearestMarker(convX, convY); | |
| // Get the marker info | |
| const markerInfo = await getMarkerInfo(marker); | |
| // Sort the comments by upvotes | |
| const comments = markerInfo.data.comments.sort((a, b) => b.vote - a.vote); | |
| if (!comments.length) return; | |
| // Update the popup content | |
| const content = popup.$el.getElementsByClassName("map-popup__content")[0]; | |
| // Add the header | |
| { | |
| const h4 = document.createElement("h4"); | |
| h4.style.marginBottom = ".1rem"; | |
| h4.style.marginTop = ".1rem"; | |
| const span = document.createElement("span"); | |
| span.classList.add("map-popup__name-link"); | |
| span.href = ""; | |
| span.target = "_blank"; | |
| const titleSpan = document.createElement("span"); | |
| titleSpan.classList.add("label"); | |
| titleSpan.innerText = "Unofficial Comments"; | |
| span.appendChild(titleSpan); | |
| h4.appendChild(span); | |
| content.appendChild(h4); | |
| } | |
| for (const comment of comments) { | |
| // Decode the content | |
| const commentContent = new DOMParser().parseFromString( | |
| comment.content, | |
| "text/html" | |
| ).body.innerText; | |
| // Add the DIV | |
| const div = document.createElement("div"); | |
| div.style.borderTop = ".01rem solid rgba(74, 83, 102, .5)"; | |
| // Detect the image | |
| let image = comment.image | |
| ? `https://game-cdn.appsample.com${comment.image}` | |
| : ""; | |
| // Match against Imgur links in the comment | |
| const imgurMatch = IMGUR_REGEX.exec(comment.content); | |
| if (imgurMatch) { | |
| const { imgid } = imgurMatch.groups; | |
| if (imgid && imgid.length) image = `https://i.imgur.com/${imgid}.png`; | |
| } | |
| // Add the image if it exists | |
| if (image && image.length) { | |
| const a = document.createElement("a"); | |
| a.href = image; | |
| a.target = "_blank"; | |
| a.rel = "noopener noreferrer"; | |
| const img = document.createElement("img"); | |
| img.classList.add("map-popup__img"); | |
| img.src = image; | |
| // Fix layout shift | |
| img.style.maxWidth = "100%"; | |
| img.style.aspectRatio = "16 / 9"; | |
| img.style.objectFit = "cover"; | |
| img.style.objectPosition = "center"; | |
| a.appendChild(img); | |
| div.appendChild(a); | |
| } | |
| // Add the desc | |
| const desc = document.createElement("pre"); | |
| desc.classList.add("map-popup__desc"); | |
| desc.innerText = commentContent; | |
| desc.style.userSelect = "text"; | |
| div.appendChild(desc); | |
| const details = document.createElement("div"); | |
| details.classList.add("map-popup__contribute"); | |
| details.style.display = "flex"; | |
| details.style.justifyContent = "space-between"; | |
| // Add the contributor & votes | |
| const data = { | |
| Contributor: comment.aname, | |
| Votes: comment.vote, | |
| }; | |
| for (const [key, value] of Object.entries(data)) { | |
| const contributeList = document.createElement("div"); | |
| contributeList.classList.add("map-popup__contribute-list"); | |
| contributeList.tabIndex = 0; | |
| const contributorLabel = document.createElement("span"); | |
| contributorLabel.classList.add("contributor-label"); | |
| contributorLabel.innerText = key; | |
| contributeList.appendChild(contributorLabel); | |
| const ul = document.createElement("ul"); | |
| contributeList.appendChild(ul); | |
| const li = document.createElement("li"); | |
| li.classList.add("first"); | |
| li.style.marginLeft = "3px"; | |
| ul.appendChild(li); | |
| const span = document.createElement("span"); | |
| span.innerText = value; | |
| li.appendChild(span); | |
| details.appendChild(contributeList); | |
| } | |
| div.appendChild(details); | |
| // Add the time | |
| const time = document.createElement("div"); | |
| time.classList.add("map-popup__time"); | |
| time.innerText = `Update Time: ${new Date( | |
| comment.time | |
| ).toLocaleString()}`; | |
| div.appendChild(time); | |
| content.appendChild(div); | |
| } | |
| }); | |
| unwatch() | |
| }) | |
| })(); | 
// ==UserScript==
// @name         Teyvat Comments
// @namespace    https://koding.dev/
// @version      0.2
// @description  Teyvat Map comments from AppSample
// @author       KodingDev
// @match        https://act.hoyolab.com/ys/app/interactive-map/index.html*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=hoyolab.com
// @grant        none
//
// @downloadURL  https://gist.githubusercontent.com/KodingDev/1f63e0bd22869292d1f7fc35be005bab/raw/script.user.js
// @updateURL    https://gist.githubusercontent.com/KodingDev/1f63e0bd22869292d1f7fc35be005bab/raw/script.user.js
// ==/UserScript==
// --- TRANSFORMATION CALCULATIONS ---
/**
 * Adds two vectors together
 *
 * @param {[number, number]} a
 * @param {[number, number]} b
 * @returns {[number, number]} The sum of the two vectors
 */
const vecAdd = (a, b) => [a[0] + b[0], a[1] + b[1]];
/**
 * Subtracts two vectors
 *
 * @param {[number, number]} a
 * @param {[number, number]} b
 * @returns {[number, number]} The difference of the two vectors
 */
const vecSub = (a, b) => [a[0] - b[0], a[1] - b[1]];
/**
 * Scales a vector by a scalar
 *
 * @param {[number, number]} a
 * @param {number} s
 * @returns {[number, number]} The scaled vector
 */
const vecScale = (a, s) => [a[0] * s, a[1] * s];
/**
 * Calculates the length of a vector
 *
 * @param {[number, number]} x
 * @returns {number} The length of the vector
 */
const vecLength = (x) => Math.sqrt(Math.pow(x[0], 2) + Math.pow(x[1], 2));
/**
 * Flips the Y coordinate of a vector
 *
 * @param {[number, number]} x
 * @returns {[number, number]} The flipped vector
 */
const vecFlipY = (x) => [x[0], -x[1]];
// Define the control points for both spaces
const OFFICIAL_CONTROLS = [
  [-5140.75, 154.5], // Emperor of Fire and Iron
  [3288.5, 6761], // Serai Island - Statue of the Seven
  [2525.5, 8455], // Statue of the Seven - Tsurumi Island
  [889, -483.5], // Teleport Waypoint - Stormbearer Point
];
const APPSAMPLE_CONTROLS = [
  [-0.10802, 0.18615], // Emperor of Fire and Iron
  [0.61536, -0.38097], // Serai Island - Statue of the Seven
  [0.54988, -0.52639], // Statue of the Seven - Tsurumi Island
  [0.40944, 0.24092], // Teleport Waypoint - Stormbearer Point
];
// On the unofficial map, +Y is north, while on the official map it's south. Flip Y on all unofficial coords.
APPSAMPLE_CONTROLS.forEach((x, i) => (APPSAMPLE_CONTROLS[i] = vecFlipY(x)));
// Calculate average scale based on all coordinates
const OFFICIAL_TO_UNOFFICIAL_SCALE = (() => {
  let scale = 0;
  for (let i = 1; i < OFFICIAL_CONTROLS.length; i++) {
    const relOfficial = vecSub(OFFICIAL_CONTROLS[i], OFFICIAL_CONTROLS[0]);
    const relUnofficial = vecSub(APPSAMPLE_CONTROLS[i], APPSAMPLE_CONTROLS[0]);
    scale += vecLength(relUnofficial) / vecLength(relOfficial);
  }
  return scale / (OFFICIAL_CONTROLS.length - 1);
})();
/**
 * Converts coordinates from the Official coordinate system to the AppSample coordinate system
 *
 * @param {[number, number]} x The coordinates in the Official coordinate system
 * @returns {[number, number]} The coordinates in the AppSample coordinate system
 */
const toUnofficial = (x) => {
  const relOfficial = vecSub(x, OFFICIAL_CONTROLS[0]);
  const relUnofficial = vecScale(relOfficial, OFFICIAL_TO_UNOFFICIAL_SCALE);
  const absUnofficial = vecAdd(relUnofficial, APPSAMPLE_CONTROLS[0]);
  return vecFlipY(absUnofficial);
};
// Regex
const IMGUR_REGEX =
  /(^(http|https):\/\/)?(i\.)?imgur.com\/((?<gallery>gallery\/)(?<galleryid>\w+)|(?<album>a\/)(?<albumid>\w+)#?)?(?<imgid>\w*)/g;
// Globals
const MARKER_DATA = [];
/**
 * Calculates the distance between two points
 *
 * @param {number} x1 The X coordinate of the first point
 * @param {number} y1 The Y coordinate of the first point
 * @param {number} x2 The X coordinate of the second point
 * @param {number} y2 The Y coordinate of the second point
 * @returns {number} The distance between the two points
 */
function distance(x1, y1, x2, y2) {
  return Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2);
}
/**
 * Finds the nearest marker to the given coordinates
 *
 * @param {number} x The X coordinate in the AppSample coordinate system
 * @param {number} y The Y coordinate in the AppSample coordinate system
 * @returns {object} The nearest marker
 */
function findNearestMarker(x, y) {
  return MARKER_DATA.map((marker) => ({
    ...marker,
    dist: distance(x, y, marker.lng, marker.lat),
  })).sort((a, b) => a.dist - b.dist)[0];
}
/**
 * Gets the marker info from the AppSample API
 *
 * @param {object} marker The marker to get info for
 * @returns {object} The marker info
 */
function getMarkerInfo(marker) {
  const params = new URLSearchParams();
  params.append("action", "get");
  params.append("app", "gim");
  params.append("ttl", "7000");
  params.append("categoryId", marker.type);
  params.append("docId", marker.id);
  params.append("sort", "");
  params.append("page", "1");
  params.append("pageSize", "50");
  return fetch(
    `https://cache-proxy.lemonapi.com/skywise/comment/v1?${params.toString()}`
  ).then((res) => res.json());
}
/**
 * Waits for a condition to be true
 *
 * @param {function} condition The condition to wait for
 * @returns {Promise} A promise that resolves when the condition is true
 */
function wait(condition) {
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if (condition()) {
        clearInterval(interval);
        resolve();
      }
    }, 100);
  });
}
(async function () {
  "use strict";
  // --- INIT ---
  // Fetch the AppSample marker data
  const markerRes = await fetch(
    "https://game-data.lemonapi.com/gim/markers_all.v3.json"
  ).then((res) => res.json());
  // Update the marker data to include header information
  MARKER_DATA.push(
    ...markerRes.data.map((marker) =>
      marker.reduce((acc, cur, i) => {
        acc[markerRes.headers[i]] = cur;
        return acc;
      }, {})
    )
  );
  // --- LISTENER ---
  // Wait for the map to load
  await wait(() => document.getElementsByClassName("mhy-game-gis").length > 0);
  const gameGis = document.getElementsByClassName("mhy-game-gis")[0]
  const unwatch = gameGis.__vue__.$watch('mapListReady', (t) => {
    if (!t) return
    const leafletMap = gameGis.__vue__.map;
    leafletMap.on("popupopen", async (d) => {
      // Wait for the popup content to populate
      await wait(() => d.popup._content.vueInstance);
      const popup = d.popup._content.vueInstance;
      const { x_pos: x, y_pos: y } = popup.marker;
      // Perform a lookup of the marker
      const [convX, convY] = toUnofficial([x, y]);
      const marker = findNearestMarker(convX, convY);
      // Get the marker info
      const markerInfo = await getMarkerInfo(marker);
      // Sort the comments by upvotes
      const comments = markerInfo.data.comments.sort((a, b) => b.vote - a.vote);
      if (!comments.length) return;
      // Update the popup content
      const content = popup.$el.getElementsByClassName("map-popup__content")[0];
      // Add the header
      {
        const h4 = document.createElement("h4");
        h4.style.marginBottom = ".1rem";
        h4.style.marginTop = ".1rem";
        const span = document.createElement("span");
        span.classList.add("map-popup__name-link");
        span.href = "";
        span.target = "_blank";
        const titleSpan = document.createElement("span");
        titleSpan.classList.add("label");
        titleSpan.innerText = "Unofficial Comments";
        span.appendChild(titleSpan);
        h4.appendChild(span);
        content.appendChild(h4);
      }
      for (const comment of comments) {
        // Decode the content
        const commentContent = new DOMParser().parseFromString(
          comment.content,
          "text/html"
        ).body.innerText;
        // Add the DIV
        const div = document.createElement("div");
        div.style.borderTop = ".01rem solid rgba(74, 83, 102, .5)";
        // Detect the image
        let image = comment.image
          ? `https://game-cdn.appsample.com${comment.image}`
          : "";
        // Match against Imgur links in the comment
        const imgurMatch = IMGUR_REGEX.exec(comment.content);
        if (imgurMatch) {
          const { imgid } = imgurMatch.groups;
          if (imgid && imgid.length) image = `https://i.imgur.com/${imgid}.png`;
        }
        // Add the image if it exists
        if (image && image.length) {
          const a = document.createElement("a");
          a.href = image;
          a.target = "_blank";
          a.rel = "noopener noreferrer";
          const img = document.createElement("img");
          img.classList.add("map-popup__img");
          img.src = image;
          // Fix layout shift
          img.style.maxWidth = "100%";
          img.style.aspectRatio = "16 / 9";
          img.style.objectFit = "cover";
          img.style.objectPosition = "center";
          a.appendChild(img);
          div.appendChild(a);
        }
        // Add the desc
        const desc = document.createElement("pre");
        desc.classList.add("map-popup__desc");
        desc.innerText = commentContent;
        desc.style.userSelect = "text";
        div.appendChild(desc);
        const details = document.createElement("div");
        details.classList.add("map-popup__contribute");
        details.style.display = "flex";
        details.style.justifyContent = "space-between";
        // Add the contributor & votes
        const data = {
          Contributor: comment.aname,
          Votes: comment.vote,
        };
        for (const [key, value] of Object.entries(data)) {
          const contributeList = document.createElement("div");
          contributeList.classList.add("map-popup__contribute-list");
          contributeList.tabIndex = 0;
          const contributorLabel = document.createElement("span");
          contributorLabel.classList.add("contributor-label");
          contributorLabel.innerText = key;
          contributeList.appendChild(contributorLabel);
          const ul = document.createElement("ul");
          contributeList.appendChild(ul);
          const li = document.createElement("li");
          li.classList.add("first");
          li.style.marginLeft = "3px";
          ul.appendChild(li);
          const span = document.createElement("span");
          span.innerText = value;
          li.appendChild(span);
          details.appendChild(contributeList);
        }
        div.appendChild(details);
        // Add the time
        const time = document.createElement("div");
        time.classList.add("map-popup__time");
        time.innerText = `Update Time: ${new Date(
          comment.time
        ).toLocaleString()}`;
        div.appendChild(time);
        content.appendChild(div);
      }
    });
    unwatch()
  })
})();oh that's really cool :o mind if I update the original w your ver
oh that's really cool :o mind if I update the original w your ver
Sure!
It is useful to show the ID (specially for seelies), incase you want to add this functionality.