Skip to content

Instantly share code, notes, and snippets.

@escalonn
Last active November 19, 2025 16:17
Show Gist options
  • Select an option

  • Save escalonn/07805044602fe18983a6c32bbe3a2b10 to your computer and use it in GitHub Desktop.

Select an option

Save escalonn/07805044602fe18983a6c32bbe3a2b10 to your computer and use it in GitHub Desktop.
function getOwnedCharacters() {
// in the bookmarklet:
// const chars = o.data.avatars.map(a => [a.name, a.actived_constellation_num]);
// const url = 'https://genshin.wife4.dev/teams#' + new URLSearchParams(chars);
// fallback to test data for node testing
const paramsString =
typeof window !== 'undefined'
? window.location.hash.substring(1)
: 'Jean=1&Kaedehara+Kazuha=0&Raiden+Shogun=0&Yelan=0&Tighnari=0&Nahida=0&Furina=0&Kirara=1&Manekin=0&Manekina=0&Neuvillette=0&Arlecchino=0&Bennett=4&Sucrose=0&Kuki+Shinobu=1&Chevreuse=6&Skirk=0&Lauma=0&Xianyun=0&Lisa=1&Xiangling=6&Xingqiu=6&Fischl=4&Traveler=6&Zhongli=0&Hu+Tao=1&Navia=0&Citlali=0&Rosaria=4&Gaming=5&Escoffier=0&Ororon=6&Iansan=0&Diluc=3&Mona=0&Clorinde=0&Beidou=2&Layla=6&Yaoyao=6&Lan+Yan=5&Keqing=1&Diona=5&Kujou+Sara=0&Yun+Jin=3&Collei=4&Lynette=0&Sethos=3&Kachina=2&Aino=0&Ifa=2&Noelle=6&Barbara=6&Shikanoin+Heizou=5&Faruzan=3&Kaeya=0&Amber=1&Ningguang=0&Dehya=0&Razor=2&Chongyun=0&Xinyan=1&Yanfei=3&Thoma=5&Sayu=0&Gorou=2&Dori=0&Candace=2&Mika=1&Kaveh=0&Yumemizuki+Mizuki=0&Freminet=0&Dahlia=1';
const chars = [];
for (const [name, constellation] of new URLSearchParams(paramsString)) {
chars[name] = parseInt(constellation);
}
return chars;
}
const ENDPOINTS = {
abyss:
'https://api.yshelper.com/ys/getAbyssRank.php?star=all&role=all&lang=en',
stygian:
'https://api.yshelper.com/ys/getAbyssRank2.php?star=all&role=all&lang=en',
dire: 'https://api.yshelper.com/ys/getAbyssRank2.php?star=only_nandu6&role=all&lang=en'
};
async function fetchData(endpoint, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(endpoint, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
function buildCharacterMap(data) {
const misspellings = { Ambor: 'Amber', Ayaka: 'Kamisato Ayaka' };
// There is no entry for Traveler's avatar, so it will be hardcoded
const extraAvatars = {
'https://upload-bbs.mihoyo.com/game_record/genshin/character_icon/UI_AvatarIcon_PlayerGirl.png':
'Traveler'
};
const chars = {};
data.result[0].forEach(group => {
group.list.forEach(char => {
// The api gives non-cumulative constellation ownership percentages like:
// c0_rate: 78.2, c1_rate: 6.6, c2_rate: 9.5, etc.
// Now calculate, for each con, what proportion of owners of the character
// have a con lesser or equal. like:
// [0.782, 0.848, ..., 1]
// This is used to compute the adjusted score for teams.
const conRates = [char.c0_rate];
for (let i = 1; i <= 5; i++) {
conRates.push(conRates[conRates.length - 1] + char[`c${i}_rate`]);
}
const normalizedRates = [...conRates.map(x => x / 100), 1];
// In .result[3], team members are only identified by their avatar images,
// so character data will be indexed by the avatar.
chars[char.avatar] = {
name: misspellings[char.name] || char.name,
conRates: normalizedRates
};
});
});
Object.entries(extraAvatars).forEach(([avatar, name]) => {
chars[avatar] = Object.values(chars).find(c => c.name === name);
});
return chars;
}
function validateOwnedCharacters(ownedChars, charMap) {
const expectedMissingChars = new Set(['Manekin', 'Manekina']);
const charNames = new Set(Object.values(charMap).map(c => c.name));
const missingChars = Object.keys(ownedChars).filter(
name => !charNames.has(name) && !expectedMissingChars.has(name)
);
// This is for future-proofing; if YSHelper can have inconsistent names for
// Amber and Ayaka, it could do it with another character in the future,
// and without this output, they would be silently considered unowned.
if (missingChars.length > 0) {
console.log(
`YSHelper has no match for these owned characters: ${missingChars.join(
', '
)}`
);
}
}
function processTeams(data, charMap, ownedChars, stageCount) {
const teams = [];
data.result[3].forEach(team => {
const avatars = team.role.map(c => c.avatar);
const members = avatars.map(a => charMap[a].name);
// Filter out teams with unowned characters
if (!members.every(c => ownedChars.hasOwnProperty(c))) {
return;
}
// Calculate constellation adjustment factor
const conFactor = members.reduce((factor, char, i) => {
return factor * charMap[avatars[i]].conRates[ownedChars[char]];
}, 1);
const teamData = {
members,
conFactor,
time: team.time // Include time data for stygian endpoints
};
// Add scores for each stage
for (let i = 0; i < stageCount; i++) {
let scoreKey;
if (stageCount === 2) {
scoreKey = ['up_use_num', 'down_use_num'][i]; // Abyss
} else {
scoreKey = ['up_use_num', 'mid_use_num', 'down_use_num'][i]; // Stygian
}
const score = team[scoreKey] / team.has;
// The "Score" is simply the proportion of people who used this team for
// this specific Abyss half or Stygian enemy out of all the people who
// own the characters in the team.
// That is, what % of people who COULD use it DID use it.
teamData[`stage${i}Score`] = score;
// The "Adj Score" (adjusted score) penalizes teams based on whether
// the current user has the constellations that the sampled users did.
// If the user has all C6 for this team, Adj Score = Score.
// If the user has all C0 for this team, and 40% of sampled users have
// C1 or higher, for each of 4 characters, then Adj Score will be
// 60%*60%*60%*60% = 12.96% of Score.
// Since constellation relevance varies too much, the results will
// be sorted by Score, not Adj Score.
teamData[`stage${i}AdjScore`] = score * conFactor;
}
teams.push(teamData);
});
return teams;
}
function formatTable(teams, stageName, stageIndex, hasTimeData = false) {
const scoreKey = `stage${stageIndex}Score`;
const adjScoreKey = `stage${stageIndex}AdjScore`;
console.log(`\nTop ${stageName} teams:`);
const tableData = teams.map(team => {
const row = {
Score: (team[scoreKey] * 100).toFixed(1) + '%',
'Adj Score': (team[adjScoreKey] * 100).toFixed(3) + '%'
};
// Add Time column only if time data is available
if (hasTimeData) {
row['Avg Clear'] = `${team.time} s`;
}
row['Team'] = team.members.join(' + ');
return row;
});
console.table(tableData);
}
async function analyzeEndpoint(endpointName, endpointUrl, stageConfig) {
try {
console.log(`\n=== ${endpointName.toUpperCase()} ANALYSIS ===`);
const ownedChars = getOwnedCharacters();
const data = await fetchData(endpointUrl);
const charMap = buildCharacterMap(data);
validateOwnedCharacters(ownedChars, charMap);
const teams = processTeams(data, charMap, ownedChars, stageConfig.length);
console.log(`\nSamples: ${data.top_own}`);
// The API response says things like "Version 6.1" but it's always out of
// date. This .update property is the most reliable, and reflects the 2-3
// day period at the beginning of the cycle when data is collected.
// This could be dynamically cross-referenced with wiki or predicted
// hardcoded version/cycle data.
console.log(data.update);
// Check if this is a stygian endpoint (has time data)
const hasTimeData = endpointName.includes('Stygian');
// Generate top 10 teams for each stage
stageConfig.forEach((stageName, index) => {
const scoreKey = `stage${index}Score`;
const topTeams = teams
.sort((a, b) => b[scoreKey] - a[scoreKey])
.slice(0, 10);
formatTable(topTeams, stageName, index, hasTimeData);
});
} catch (error) {
console.error(`${endpointName} Error:`, error);
}
}
async function main() {
const ownedChars = getOwnedCharacters();
console.log(`Processing ${Object.keys(ownedChars).length} owned characters`);
const abyssStages = ['first-half', 'second-half'];
const stygianStages = ['room-1', 'room-2', 'room-3'];
await analyzeEndpoint('Abyss', ENDPOINTS.abyss, abyssStages);
await analyzeEndpoint(
'Stygian (Fearless & Dire)',
ENDPOINTS.stygian,
stygianStages
);
await analyzeEndpoint('Stygian (Dire)', ENDPOINTS.dire, stygianStages);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment