const fs = require('node:fs'); const path = require('node:path'); const genshin = require('genshin-db'); const opts = { matchCategories: true, verboseCategories: true, resultLanguage: 'English', queryLanguages: ['English'] }; function asArray(value) { return Array.isArray(value) ? value : []; } const characters = asArray(genshin.characters('names', opts)) .map((entry) => ({ id: entry.id, name: entry.name, rarity: entry.rarity, element: entry.elementText, weapon: entry.weaponText, })) .filter((entry) => entry.name) .sort((a, b) => a.name.localeCompare(b.name)); const artifacts = asArray(genshin.artifacts('names', opts)) .map((entry) => ({ id: entry.id, name: entry.name, rarityList: entry.rarityList ?? [], pieces: [entry.flower, entry.plume, entry.sands, entry.goblet, entry.circlet] .filter(Boolean) .map((piece) => ({ name: piece.name, relicType: piece.relicType })), })) .filter((entry) => entry.name) .sort((a, b) => a.name.localeCompare(b.name)); const artifactPieces = artifacts .flatMap((set) => set.pieces.map((piece) => ({ name: piece.name, setName: set.name, slot: slotFromRelicType(piece.relicType), relicType: piece.relicType, })), ) .sort((a, b) => a.name.localeCompare(b.name)); const slotByPiece = Object.fromEntries(artifactPieces.map((piece) => [piece.name, piece.slot])); const setByPiece = Object.fromEntries(artifactPieces.map((piece) => [piece.name, piece.setName])); const setToPieces = Object.fromEntries( artifacts.map((set) => [set.name, artifactPieces.filter((piece) => piece.setName === set.name).map((piece) => piece.name)]), ); const mainStats = [ 'Elemental Mastery', 'Energy Recharge', 'CRIT Rate', 'CRIT DMG', 'Healing Bonus', 'ATK%', 'HP%', 'DEF%', 'ATK', 'HP', 'DEF', 'Hydro DMG Bonus', 'Pyro DMG Bonus', 'Electro DMG Bonus', 'Cryo DMG Bonus', 'Dendro DMG Bonus', 'Anemo DMG Bonus', 'Geo DMG Bonus', 'Physical DMG Bonus', ]; const substats = ['CRIT DMG', 'CRIT Rate', 'Energy Recharge', 'Elemental Mastery', 'ATK', 'ATK%', 'HP', 'HP%', 'DEF', 'DEF%']; const mainStatsBySlot = { 'Flower of Life': ['HP'], 'Plume of Death': ['ATK'], 'Sands of Eon': ['HP%', 'ATK%', 'DEF%', 'Energy Recharge', 'Elemental Mastery'], 'Goblet of Eonothem': [ 'HP%', 'ATK%', 'DEF%', 'Elemental Mastery', 'Hydro DMG Bonus', 'Pyro DMG Bonus', 'Electro DMG Bonus', 'Cryo DMG Bonus', 'Dendro DMG Bonus', 'Anemo DMG Bonus', 'Geo DMG Bonus', 'Physical DMG Bonus', ], 'Circlet of Logos': ['HP%', 'ATK%', 'DEF%', 'Elemental Mastery', 'CRIT Rate', 'CRIT DMG', 'Healing Bonus'], }; const mainStatValueReferences = { 'Flower of Life': [{ stat: 'HP', base: 717, max: 4780 }], 'Plume of Death': [{ stat: 'ATK', base: 47, max: 311 }], 'Sands of Eon': [ { stat: 'HP%', base: 7.0, max: 46.6 }, { stat: 'ATK%', base: 7.0, max: 46.6 }, { stat: 'DEF%', base: 8.7, max: 58.3 }, { stat: 'Energy Recharge', base: 7.8, max: 51.8 }, { stat: 'Elemental Mastery', base: 28, max: 187 }, ], 'Goblet of Eonothem': [ { stat: 'HP%', base: 7.0, max: 46.6 }, { stat: 'ATK%', base: 7.0, max: 46.6 }, { stat: 'DEF%', base: 8.7, max: 58.3 }, { stat: 'Elemental Mastery', base: 28, max: 187 }, { stat: 'Hydro DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Pyro DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Electro DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Cryo DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Dendro DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Anemo DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Geo DMG Bonus', base: 7.0, max: 46.6 }, { stat: 'Physical DMG Bonus', base: 8.7, max: 58.3 }, ], 'Circlet of Logos': [ { stat: 'HP%', base: 7.0, max: 46.6 }, { stat: 'ATK%', base: 7.0, max: 46.6 }, { stat: 'DEF%', base: 8.7, max: 58.3 }, { stat: 'Elemental Mastery', base: 28, max: 187 }, { stat: 'CRIT Rate', base: 4.7, max: 31.1 }, { stat: 'CRIT DMG', base: 9.3, max: 62.2 }, { stat: 'Healing Bonus', base: 5.4, max: 35.9 }, ], }; const data = { schemaVersion: 2, generatedAt: new Date().toISOString(), source: { package: 'genshin-db', version: require('genshin-db/package.json').version, resultLanguage: opts.resultLanguage, }, sourceVersions: { genshinDb: require('genshin-db/package.json').version, resultLanguage: opts.resultLanguage, }, sourceVersion: `genshin-db@${require('genshin-db/package.json').version}`, characters, artifactSets: artifacts, artifactPieces, slotByPiece, setByPiece, slots: ['Flower of Life', 'Plume of Death', 'Sands of Eon', 'Goblet of Eonothem', 'Circlet of Logos'], mainStats, mainStatsBySlot, mainStatValueReferences, substats, stats: { main: mainStats, mainBySlot: mainStatsBySlot, sub: substats, }, aliases: { stats: { 'Crit Damage': 'CRIT DMG', 'Critical Damage': 'CRIT DMG', 'Crit Rate': 'CRIT Rate', 'Critical Rate': 'CRIT Rate', 'Energy Recharge %': 'Energy Recharge', 'Elemental Master': 'Elemental Mastery', 'Elemental Masterie': 'Elemental Mastery', 'Heal Bonus': 'Healing Bonus', }, textReplacements: { 'CIT DMG': 'CRIT DMG', 'CRIT DMG+I': 'CRIT DMG+1', 'CRIT Rate+Z': 'CRIT Rate+2', 'Energv Recharge': 'Energy Recharge', 'Elemental Masterv': 'Elemental Mastery', 'Equipped;': 'Equipped:', }, slotAliases: { 'Sands of Eon Vi': 'Sands of Eon', 'Sands of Eon V': 'Sands of Eon', 'Sands of Eon 2': 'Sands of Eon', 'Flower of Life 2': 'Flower of Life', 'Flower of Lif': 'Flower of Life', 'Goblet of Eonotherm': 'Goblet of Eonothem', 'Goblet of Eonothemn': 'Goblet of Eonothem', 'Circlet of Logas': 'Circlet of Logos', 'Circlet of Loges': 'Circlet of Logos', }, setAliases: { 'Viridescent Venere': 'Viridescent Venerer', 'Maiden Beloved:': 'Maiden Beloved', 'Gladiators Finale': "Gladiator's Finale", }, pieceAliases: { 'A Note in Springs Leich': "A Note in Spring's Leich", 'Viridescent Vencrers Vessel': "Viridescent Venerer's Vessel", 'Holy Crown of the Believer ': 'Holy Crown of the Believer', }, characterAliases: { 'Citlall': 'Citlali', 'Sandrone ': 'Sandrone', 'Qiqi ': 'Qiqi', }, }, lookup: { normalizedKeys: { sets: normalizedMap(artifacts.map((set) => set.name)), pieces: normalizedMap(artifactPieces.map((piece) => piece.name)), slots: normalizedMap(['Flower of Life', 'Plume of Death', 'Sands of Eon', 'Goblet of Eonothem', 'Circlet of Logos']), stats: normalizedMap([...mainStats, ...substats]), characters: normalizedMap(characters.map((character) => character.name)), }, goodKeys: { sets: Object.fromEntries(artifacts.map((set) => [set.name, goodKey(set.name)])), pieces: Object.fromEntries(artifactPieces.map((piece) => [piece.name, goodKey(piece.name)])), stats: Object.fromEntries([...mainStats, ...substats].map((stat) => [stat, goodStatKey(stat)])), characters: Object.fromEntries(characters.map((character) => [character.name, goodKey(character.name)])), }, setToPieces, validation: validateLookupPackage({ artifacts, artifactPieces, characters, mainStats, substats, setToPieces }), }, uiProfiles: { artifactDetailEn: { language: 'English', supportedResolutions: ['1920x1080', '2560x1440', '3840x2160'], detailPanel: { x: 0.5, y: 0.05, width: 0.45, height: 0.9 }, note: 'Relative profile used as scanner contract; runtime crops may tune offsets from review samples.', }, }, }; const outPath = path.join(process.cwd(), 'src', 'data', 'genshinGameData.json'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, JSON.stringify(data, null, 2) + '\n'); console.log(`generated ${outPath}`); console.log(`${characters.length} characters, ${artifacts.length} artifact sets`); console.log(`${artifactPieces.length} artifact pieces, ${mainStats.length} main stats, ${substats.length} substats`); function slotFromRelicType(relicType) { switch (relicType) { case 'EQUIP_BRACER': return 'Flower of Life'; case 'EQUIP_NECKLACE': return 'Plume of Death'; case 'EQUIP_SHOES': return 'Sands of Eon'; case 'EQUIP_RING': return 'Goblet of Eonothem'; case 'EQUIP_DRESS': return 'Circlet of Logos'; default: return ''; } } function normalizeLookupKey(value) { return String(value) .toLowerCase() .normalize('NFKD') .replace(/['’]/g, '') .replace(/[^a-z0-9]+/g, ''); } function normalizedMap(values) { return Object.fromEntries(values.filter(Boolean).map((value) => [normalizeLookupKey(value), value])); } function goodKey(value) { return String(value) .replace(/['’]/g, '') .replace(/[^A-Za-z0-9]+(.)/g, (_match, next) => String(next).toUpperCase()) .replace(/^[a-z]/, (first) => first.toUpperCase()) .replace(/[^A-Za-z0-9]/g, ''); } function goodStatKey(stat) { switch (stat) { case 'HP': return 'hp'; case 'HP%': return 'hp_'; case 'ATK': return 'atk'; case 'ATK%': return 'atk_'; case 'DEF': return 'def'; case 'DEF%': return 'def_'; case 'Elemental Mastery': return 'eleMas'; case 'Energy Recharge': return 'enerRech_'; case 'CRIT Rate': return 'critRate_'; case 'CRIT DMG': return 'critDMG_'; case 'Healing Bonus': return 'heal_'; case 'Physical DMG Bonus': return 'physical_dmg_'; default: return stat.toLowerCase().replace(' dmg bonus', '_dmg_').replace(/\s+/g, ''); } } function validateLookupPackage({ artifacts, artifactPieces, characters, mainStats, substats, setToPieces }) { const errors = []; const warnings = []; const setNames = new Set(artifacts.map((set) => set.name)); const slotNames = new Set(['Flower of Life', 'Plume of Death', 'Sands of Eon', 'Goblet of Eonothem', 'Circlet of Logos']); const goodSetKeys = new Set(); for (const set of artifacts) { const key = goodKey(set.name); if (goodSetKeys.has(key)) errors.push(`Duplicate GOOD set key: ${key}`); goodSetKeys.add(key); if ((setToPieces[set.name] ?? []).length === 0) warnings.push(`Set has no pieces: ${set.name}`); } for (const piece of artifactPieces) { if (!setNames.has(piece.setName)) errors.push(`Piece ${piece.name} references missing set ${piece.setName}`); if (!slotNames.has(piece.slot)) errors.push(`Piece ${piece.name} references missing slot ${piece.slot}`); } if (!characters.length) warnings.push('No characters generated.'); if (!mainStats.length || !substats.length) errors.push('Stats were not generated.'); return { valid: errors.length === 0, errors, warnings, summary: { artifactSets: artifacts.length, artifactPieces: artifactPieces.length, characters: characters.length, stats: mainStats.length + substats.length, }, }; }