Skip to content

Instantly share code, notes, and snippets.

@atouu
Forked from KodingDev/README.md
Last active March 28, 2025 16:45
Show Gist options
  • Save atouu/23c03510db72c505430cfeb6e56e087f to your computer and use it in GitHub Desktop.
Save atouu/23c03510db72c505430cfeb6e56e087f to your computer and use it in GitHub Desktop.
Teyvat Map Comments - UserScript
// ==UserScript==
// @name Teyvat Comments
// @namespace https://koding.dev/
// @version 0.6
// @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/atouu/23c03510db72c505430cfeb6e56e087f/raw/script.user.js
// @updateURL https://gist.githubusercontent.com/atouu/23c03510db72c505430cfeb6e56e087f/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 = {
2: [ // TEYVAT
[-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
],
7: [ // Enkanomiya
[-662.86, -1235.21], // North - Teleport Waypoint
[-1379.35, 382.64], // The Serpent's Bowels - Teleport Waypoint
[242, 1063.5], // Kanado's Locus - Teleport Waypoint
[889.86, -803.78], // Northeast - Teleport Waypoint
],
9: [ // The Chasm
[-356.55, -404.77], // Ad-Hoc Main Tunnel - Teleport Waypoint
[390.30, -244.05], // Main Mining Area - Time Trial
[150.78, 212.5], // Stony Halls - Teleport Waypoint
[-263.93, 269.092], // The Glowing Narrows - Teleport Waypoint
],
34: [ // Sea of Bygone Eras
[-522.37, -93.25], // Initium Iani - Teleport Waypoint
[22, 153.5], // Collegium Phonascorum - Radiant Spincrystal
[22.75, -142.75], // Alta Semita - Teleport Waypoint
[443.75, -80.25], // Clivus Capitolinus - Radiant Spincrystal
],
36: [ // Ancient Sacred Mountain
[96.5, 536], // Flame-Melding Ritual Grounds - Teleport Waypoint
[-538, 196.5], // Sea of Shifting Sentience - Teleport Waypoint
[-194.25, -514], // Summoning Hall - Teleport Waypoint
[392.5, 236.5], // East - Teleport Waypoint
]
};
const APPSAMPLE_CONTROLS = {
2: [ // TEYVAT
[-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
],
7: [ // Enkanomiya
[-0.29593, 0.51515], // North - Teleport Waypoint
[-0.54133, -0.03897], // The Serpent's Bowels - Teleport Waypoint
[0.01398, -0.27216], // Kanado's Locus - Teleport Waypoint
[0.23588, 0.36738], // Northeast - Teleport Waypoint
],
9: [ // The Chasm
[-0.1053, 0.26974], // Ad-Hoc Main Tunnel - Teleport Waypoint
[0.4063, 0.15964], // Main Mining Area - Time Trial
[0.24223, -0.1531], // Stony Halls - Teleport Waypoint
[-0.04185, -0.19186], // The Glowing Narrows - Teleport Waypoint
],
34: [ // Sea of Bygone Eras
[-0.14675, 0.00112], // Initium Iani - Teleport Waypoint
[0.04017, -0.08361], // Collegium Phonascorum - Radiant Spincrystal
[0.04043, 0.01811], // Alta Semita - Teleport Waypoint
[0.18499, -0.00335], // Clivus Capitolinus - Radiant Spincrystal
],
36: [ // Ancient Sacred Mountain
[0.0497, -0.27604], // Flame-Melding Ritual Grounds - Teleport Waypoint
[-0.27707, -0.1012], // Sea of Shifting Sentience - Teleport Waypoint
[-0.10004, 0.26471], // Summoning Hall - Teleport Waypoint
[0.20214, -0.1218], // East - Teleport Waypoint
]
};
const OFFICIAL_TO_UNOFFICIAL_SCALE = {}
for (let key in APPSAMPLE_CONTROLS) {
// On the unofficial map, +Y is north, while on the official map it's south. Flip Y on all unofficial coords.
APPSAMPLE_CONTROLS[key].forEach((x, i) => (APPSAMPLE_CONTROLS[key][i] = vecFlipY(x)));
OFFICIAL_TO_UNOFFICIAL_SCALE[key] = (() => {
let scale = 0;
for (let i = 1; i < OFFICIAL_CONTROLS[key].length; i++) {
const relOfficial = vecSub(OFFICIAL_CONTROLS[key][i], OFFICIAL_CONTROLS[key][0]);
const relUnofficial = vecSub(APPSAMPLE_CONTROLS[key][i], APPSAMPLE_CONTROLS[key][0]);
scale += vecLength(relUnofficial) / vecLength(relOfficial);
}
return scale / (OFFICIAL_CONTROLS[key].length - 1);
})();
}
// Calculate average scale based on all coordinates
/**
* 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, id) => {
const relOfficial = vecSub(x, OFFICIAL_CONTROLS[id][0]);
const relUnofficial = vecScale(relOfficial, OFFICIAL_TO_UNOFFICIAL_SCALE[id]);
const absUnofficial = vecAdd(relUnofficial, APPSAMPLE_CONTROLS[id][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.v4.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-map-container").length > 0);
const mapContainer = document.getElementsByClassName("mhy-map-container")[0]
mapContainer.__vue__.$watch("loading", (loading) => {
if (loading) return
const leafletMap = mapContainer.__vue__.$refs.mhyMap.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], popup.mapId);
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 img = document.createElement("img");
img.setAttribute("preview", "preview")
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";
div.appendChild(img);
}
// Add the desc
const desc = document.createElement("pre");
desc.classList.add("map-popup__desc");
desc.innerHTML = commentContent.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>')
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);
popup.$previewRefresh()
}
});
})
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment