UNPKG

@magicwenli/league-fan-assets

Version:

Typed League of Legends asset loaders (icons, emotes, wards, loot, champions, skins) for league-fan

668 lines (651 loc) 19.6 kB
// src/sources/cdragon.ts var CDRAGON = "https://raw.communitydragon.org"; var TENCENT_PROFILE_ICON_CDN = "https://dlied1.qq.com/lolapp/lol/summoner/profileicon/"; var CATEGORY_CDRAGON_FILE = { "summoner-icons": "summoner-icons.json", "summoner-icon-sets": "summoner-icon-sets.json", "summoner-emotes": "summoner-emotes.json", "ward-skins": "ward-skins.json", "ward-skin-sets": "ward-skin-sets.json", loot: "loot.json", champions: "champion-summary.json", skinlines: "skinlines.json", universes: "universes.json", skins: "skins.json" }; function dataRoot(opts = {}) { const patch = opts.patch ?? "latest"; const lang = opts.lang ?? "default"; return `${CDRAGON}/${patch}/plugins/rcp-be-lol-game-data/global/${lang}`; } function cdragonDataUrl(opts) { const file = CATEGORY_CDRAGON_FILE[opts.category]; if (!file) { throw new Error( `Category "${opts.category}" is not loaded from CDragon v1 JSON` ); } return `${dataRoot({ patch: opts.patch, lang: opts.lang })}/v1/${file}`; } function defaultPatchForCategory(category) { switch (category) { case "champions": case "skinlines": case "universes": case "skins": case "added": return "pbe"; default: return "latest"; } } // src/sources/release.ts var DEFAULT_RELEASE_BASE = "https://github.com/league-fan/league-fan-assets/releases/latest/download"; function releaseBaseUrl(opts) { if (opts?.baseUrl) { return opts.baseUrl.replace(/\/$/, ""); } if (opts?.version) { const ver = opts.version.startsWith("data-v") ? opts.version : `data-v${opts.version}`; return `https://github.com/league-fan/league-fan-assets/releases/download/${ver}`; } return DEFAULT_RELEASE_BASE; } function releaseAssetName(relPath) { return relPath.replace(/\//g, "__"); } function releaseDataUrl(opts) { const base = releaseBaseUrl({ baseUrl: opts.baseUrl, version: opts.version }); if (opts.category === "version") { return `${base}/${releaseAssetName("version.json")}`; } if (opts.category === "added") { return `${base}/${releaseAssetName("default/added.json")}`; } const lang = opts.lang ?? "default"; return `${base}/${releaseAssetName(`${lang}/${opts.category}.json`)}`; } function releaseManifestUrl(opts) { return `${releaseBaseUrl(opts)}/${releaseAssetName("manifest.json")}`; } // src/loaders/http.ts var FetchError = class extends Error { status; url; constructor(url, status, statusText) { super(`HTTP ${status} ${statusText} for ${url}`); this.name = "FetchError"; this.status = status; this.url = url; } }; async function fetchJson(url, opts) { const fetchImpl = opts?.fetch ?? globalThis.fetch; if (!fetchImpl) { throw new Error("No fetch implementation available"); } const res = await fetchImpl(url); if (!res.ok) { throw new FetchError(url, res.status, res.statusText); } return await res.json(); } // src/sources/ddragon.ts var DDRAGON_REALM_TENCENT = "https://ddragon.leagueoflegends.com/realms/tencent.json"; var DDRAGON_REALM_NA = "https://ddragon.leagueoflegends.com/realms/na.json"; async function getGameVersion(opts) { const info = await getVersionInfo(opts); return info.v; } async function getVersionInfo(opts) { const url = opts?.realmUrl ?? DDRAGON_REALM_TENCENT; return fetchJson(url, { fetch: opts?.fetch }); } // src/transforms/paths.ts var GAME_DATA_PREFIX = "/lol-game-data/assets"; function assetUrl(path, opts = {}) { if (path == null || path === "") return ""; if (/^https?:\/\//i.test(path)) return path; const patch = opts.patch ?? "latest"; if (opts.category === "summoner-icons" && (opts.lang === "zh_cn" || opts.lang === "zh_CN")) { const m = path.match( /\/lol-game-data\/assets\/v1\/profile-icons\/([0-9]+\.(?:jpg|png))/i ); if (m?.[1]) { return `${TENCENT_PROFILE_ICON_CDN}${m[1].toLowerCase()}`; } } if (path.startsWith(GAME_DATA_PREFIX)) { const rest = path.slice(GAME_DATA_PREFIX.length).replace(/^\/+/, ""); return `${dataRoot({ patch, lang: "default" })}/${rest.toLowerCase()}`; } if (path.startsWith("/")) { return `${dataRoot({ patch, lang: "default" })}${path.toLowerCase()}`; } return path; } function rewritePathsDeep(value, opts = {}) { if (value == null) return value; if (typeof value === "string") { if (value.includes(GAME_DATA_PREFIX) || value.startsWith("/lol-game-data")) { return assetUrl(value, opts); } return value; } if (Array.isArray(value)) { return value.map((v) => rewritePathsDeep(v, opts)); } if (typeof value === "object") { const out = {}; for (const [k, v] of Object.entries(value)) { out[k] = rewritePathsDeep(v, opts); } return out; } return value; } // src/transforms/loot.ts var CHEST_ID = /CHEST_[0-9]{1,4}$/; function filterLootChests(raw, opts = {}) { const patch = opts.patch ?? "latest"; let items = []; if (Array.isArray(raw)) { items = raw; } else if (raw && typeof raw === "object" && "LootItems" in raw) { items = raw.LootItems ?? []; } const filtered = items.filter((el) => { const id = String(el.id ?? ""); return CHEST_ID.test(id); }); const mapped = filtered.map((el) => { const image = assetUrl(String(el.image ?? el.imagePath ?? ""), { patch, lang: opts.lang, category: "loot" }); return { ...el, id: String(el.id), name: String(el.name ?? ""), description: String(el.description ?? ""), image }; }); return mapped.sort( (a, b) => (a.mappedStoreId ?? 0) - (b.mappedStoreId ?? 0) ); } // src/transforms/cosmetics.ts function flattenDescription(item) { if (item.descriptions?.[0]?.description) { return item.descriptions[0].description; } return item.description ?? ""; } function transformSummonerIcons(raw, opts = {}) { const patch = opts.patch ?? "latest"; const lang = opts.lang ?? "default"; const list = raw.map((item) => { const imagePath = assetUrl(item.imagePath ?? "", { patch, lang, category: "summoner-icons" }); return { ...item, id: Number(item.id), title: item.title ?? "", imagePath, description: flattenDescription(item) }; }); return list.filter((i) => i.imagePath).sort((a, b) => b.id - a.id); } function transformSummonerIconSets(raw, opts = {}) { const data = Array.isArray(raw) ? raw : []; return rewritePathsDeep(data, { patch: opts.patch ?? "latest", lang: opts.lang, category: "summoner-icon-sets" }); } function transformSummonerEmotes(raw, opts = {}) { const patch = opts.patch ?? "latest"; return raw.map((item) => ({ ...item, id: Number(item.id), name: item.name ?? "", description: item.description ?? "", inventoryIcon: assetUrl(item.inventoryIcon ?? "", { patch, lang: opts.lang, category: "summoner-emotes" }) })); } function transformWardSkins(raw, opts = {}) { const patch = opts.patch ?? "latest"; return raw.map((item) => ({ ...item, id: Number(item.id), name: item.name ?? "", description: item.description ?? "", wardImagePath: assetUrl(item.wardImagePath ?? "", { patch, lang: opts.lang, category: "ward-skins" }), wardShadowImagePath: assetUrl(item.wardShadowImagePath ?? "", { patch, lang: opts.lang, category: "ward-skins" }) })); } function transformWardSkinSets(raw, opts = {}) { const data = Array.isArray(raw) ? raw : []; return rewritePathsDeep(data, { patch: opts.patch ?? "latest", lang: opts.lang, category: "ward-skin-sets" }); } function transformLoot(raw, opts = {}) { return filterLootChests(raw, opts); } // src/helpers/substitute.ts var ALIAS_SUBSTITUTIONS = { monkeyking: "wukong" }; function substitute(value, sets = ALIAS_SUBSTITUTIONS) { return sets[value] ?? value; } // src/transforms/champions.ts function transformChampions(raw, opts = {}) { const patch = opts.patch ?? "pbe"; return raw.filter((d) => d.id !== -1 && d.id != null).map((a) => { const alias = a.alias ?? ""; return { id: Number(a.id), name: a.name ?? "", alias, squarePortraitPath: assetUrl(a.squarePortraitPath ?? "", { patch, lang: opts.lang, category: "champions" }), roles: a.roles ?? [], key: substitute(alias.toLowerCase()) }; }).sort((a, b) => a.name > b.name ? 1 : -1); } // src/transforms/skins.ts var PATH_KEYS = [ "splashPath", "uncenteredSplashPath", "tilePath", "loadScreenPath", "loadScreenVintagePath", "chromaPath", "splashVideoPath", "collectionSplashVideoPath", "collectionCardHoverVideoPath", "rarityGemPath" ]; function absolutizeSkinPaths(skin, opts) { const next = { ...skin }; for (const key of PATH_KEYS) { const val = next[key]; if (typeof val === "string" && val) { next[key] = assetUrl(val, { patch: opts.patch, lang: opts.lang, category: "skins" }); } } if (next.chromas?.length) { next.chromas = next.chromas.map((c) => ({ ...c, chromaPath: assetUrl(c.chromaPath, { patch: opts.patch, lang: opts.lang, category: "skins" }) })); } if (next.emblems?.length) { next.emblems = next.emblems.map((e) => ({ ...e, emblemPath: { large: assetUrl(e.emblemPath?.large, { patch: opts.patch, lang: opts.lang, category: "skins" }), small: assetUrl(e.emblemPath?.small, { patch: opts.patch, lang: opts.lang, category: "skins" }) } })); } if (next.skinAugments) { next.skinAugments = rewritePathsDeep(next.skinAugments, { patch: opts.patch, lang: opts.lang, category: "skins" }); } return next; } function transformSkins(raw, opts = {}) { const patch = opts.patch ?? "pbe"; const originalPrefix = opts.originalPrefix ?? true; const data = { ...raw }; for (const id of Object.keys(data)) { const skin = data[id]; if (!skin) continue; if (skin.isBase && originalPrefix && !skin.name.startsWith("Original ")) { skin.name = "Original " + skin.name; } if (skin.questSkinInfo?.tiers?.length) { const base = { ...skin }; delete base.questSkinInfo; for (const tier of skin.questSkinInfo.tiers) { const merged = { ...base, ...tier }; data[String(merged.id)] = absolutizeSkinPaths(merged, { patch, lang: opts.lang }); } } data[id] = absolutizeSkinPaths(skin, { patch, lang: opts.lang }); } return data; } function transformSkinlines(raw, _opts = {}) { return raw.filter((d) => d.id !== 0).map((d) => ({ id: d.id, name: d.name ?? "", description: d.description ?? "" })).sort((a, b) => a.name > b.name ? 1 : -1); } function transformUniverses(raw, opts = {}) { const patch = opts.patch ?? "pbe"; return raw.filter((d) => d.id !== 0 && d.id != null).map((d) => ({ id: Number(d.id), name: d.name ?? "", description: d.description ?? "", imagePath: assetUrl(d.imagePath ?? "", { patch, lang: opts.lang, category: "universes" }), skinSets: d.skinSets ?? [] })).sort((a, b) => a.name > b.name ? 1 : -1); } // src/transforms/added.ts function computeAdded(opts) { const oldSkinIds = new Set(Object.keys(opts.previous.skins)); const oldChampionIds = new Set(opts.previous.champions.map((c) => c.id)); const oldSkinlineIds = new Set(opts.previous.skinlines.map((l) => l.id)); const oldUniverseIds = new Set(opts.previous.universes.map((u) => u.id)); return { skins: Object.keys(opts.current.skins).filter((i) => !oldSkinIds.has(i)), champions: opts.current.champions.map((c) => c.id).filter((i) => !oldChampionIds.has(i)), skinlines: opts.current.skinlines.map((l) => l.id).filter((i) => !oldSkinlineIds.has(i)), universes: opts.current.universes.map((u) => u.id).filter((i) => !oldUniverseIds.has(i)) }; } // src/loaders/category.ts function resolvePatch(category, source, explicit) { if (explicit) return explicit; if (source.kind === "cdragon") { return source.patch ?? defaultPatchForCategory(category); } return defaultPatchForCategory(category); } async function loadCategory(category, opts = {}) { const lang = opts.lang ?? "default"; const source = opts.source ?? { kind: "cdragon" }; const patch = resolvePatch(category, source, opts.patch); const fetchOpt = { fetch: opts.fetch }; if (category === "version") { if (source.kind === "release") { const url2 = releaseDataUrl({ category: "version", baseUrl: source.baseUrl, version: source.version }); return await fetchJson(url2, fetchOpt); } return await getVersionInfo(fetchOpt); } if (category === "added") { if (source.kind === "release") { const url2 = releaseDataUrl({ category: "added", baseUrl: source.baseUrl, version: source.version }); return await fetchJson(url2, fetchOpt); } throw new Error( 'Category "added" requires source: { kind: "release" } or precomputed data from build-data' ); } let raw; if (source.kind === "release") { const url2 = releaseDataUrl({ category, lang, baseUrl: source.baseUrl, version: source.version }); raw = await fetchJson(url2, fetchOpt); return raw; } const url = cdragonDataUrl({ category, lang, patch }); raw = await fetchJson(url, fetchOpt); const tOpts = { lang, patch }; switch (category) { case "summoner-icons": return transformSummonerIcons(raw, tOpts); case "summoner-icon-sets": return transformSummonerIconSets(raw, tOpts); case "summoner-emotes": return transformSummonerEmotes(raw, tOpts); case "ward-skins": return transformWardSkins(raw, tOpts); case "ward-skin-sets": return transformWardSkinSets(raw, tOpts); case "loot": return transformLoot(raw, tOpts); case "champions": return transformChampions(raw, tOpts); case "skinlines": return transformSkinlines(raw, tOpts); case "universes": return transformUniverses(raw, tOpts); case "skins": return transformSkins(raw, tOpts); default: throw new Error(`Unsupported category: ${category}`); } } // src/client.ts function createClient(options = {}) { const lang = options.lang ?? "default"; const source = options.source ?? { kind: "cdragon" }; const base = { lang, source, fetch: options.fetch, patch: options.patch }; const load = (category) => loadCategory(category, base); return { lang, source, load, summonerIcons: () => load("summoner-icons"), summonerIconSets: () => load("summoner-icon-sets"), summonerEmotes: () => load("summoner-emotes"), wardSkins: () => load("ward-skins"), wardSkinSets: () => load("ward-skin-sets"), loot: () => load("loot"), champions: () => load("champions"), skinlines: () => load("skinlines"), universes: () => load("universes"), skins: () => load("skins"), added: () => load("added"), version: () => load("version") }; } // src/catalog.ts var ASSET_CATEGORIES = [ "summoner-icons", "summoner-icon-sets", "summoner-emotes", "ward-skins", "ward-skin-sets", "loot", "champions", "skinlines", "universes", "skins", "added", "version" ]; var PUBLISHED_DATA_CATEGORIES = [ "summoner-icons", "summoner-icon-sets", "summoner-emotes", "ward-skins", "ward-skin-sets", "loot", "champions", "skinlines", "universes", "skins", "added" ]; var DEFAULT_DATA_LANGUAGES = ["default", "zh_cn"]; var CATEGORY_META = { "summoner-icons": { id: "summoner-icons", domain: "cosmetics", description: "Summoner profile icons" }, "summoner-icon-sets": { id: "summoner-icon-sets", domain: "cosmetics", description: "Summoner icon sets" }, "summoner-emotes": { id: "summoner-emotes", domain: "cosmetics", description: "Summoner emotes" }, "ward-skins": { id: "ward-skins", domain: "cosmetics", description: "Ward skins" }, "ward-skin-sets": { id: "ward-skin-sets", domain: "cosmetics", description: "Ward skin sets" }, loot: { id: "loot", domain: "cosmetics", description: "Loot chests (filtered)" }, champions: { id: "champions", domain: "champions", description: "Champion summary list" }, skinlines: { id: "skinlines", domain: "skins", description: "Skin lines / thematics" }, universes: { id: "universes", domain: "skins", description: "Universes / IP sets" }, skins: { id: "skins", domain: "skins", description: "Full skins map with chromas / quest tiers" }, added: { id: "added", domain: "meta", description: "IDs added vs previous patch snapshot" }, version: { id: "version", domain: "meta", description: "Data dragon realm / game version" } }; // src/helpers/split-skin-id.ts function splitSkinId(id) { return { champId: Math.floor(id / 1e3), skinIndex: id % 1e3 }; } function splitId(id) { const { champId, skinIndex } = splitSkinId(id); return [champId, skinIndex]; } // src/types/skins.ts var RarityEnum = /* @__PURE__ */ ((RarityEnum2) => { RarityEnum2["KEpic"] = "kEpic"; RarityEnum2["KLegendary"] = "kLegendary"; RarityEnum2["KMythic"] = "kMythic"; RarityEnum2["KNoRarity"] = "kNoRarity"; RarityEnum2["KRare"] = "kRare"; RarityEnum2["KTranscendent"] = "kTranscendent"; RarityEnum2["KUltimate"] = "kUltimate"; return RarityEnum2; })(RarityEnum || {}); var SkinType = /* @__PURE__ */ ((SkinType2) => { SkinType2["Empty"] = ""; SkinType2["Ultimate"] = "Ultimate"; return SkinType2; })(SkinType || {}); // src/helpers/rarity.ts var raritiesMap = { ["kUltimate" /* KUltimate */]: ["ultimate.png", "Ultimate"], ["kMythic" /* KMythic */]: ["mythic.png", "Mythic"], ["kLegendary" /* KLegendary */]: ["legendary.png", "Legendary"], ["kEpic" /* KEpic */]: ["epic.png", "Epic"], ["kTranscendent" /* KTranscendent */]: ["transcendent.png", "Transcendent"], ["kRare" /* KRare */]: ["rare.png", "Rare"] }; function getRarityUrl(rarity, opts) { const key = rarity; const info = raritiesMap[key]; if (!info) return null; const [imgName, name] = info; const imgUrl = `${dataRoot({ patch: opts?.patch ?? "pbe", lang: "default" })}/v1/rarity-gem-icons/${imgName}`; return { imgUrl, name }; } function modelviewerUrl(skinId) { return `https://www.modelviewer.lol/en-US/model-viewer?id=${skinId}`; } export { ALIAS_SUBSTITUTIONS, ASSET_CATEGORIES, CATEGORY_CDRAGON_FILE, CATEGORY_META, CDRAGON, DDRAGON_REALM_NA, DDRAGON_REALM_TENCENT, DEFAULT_DATA_LANGUAGES, DEFAULT_RELEASE_BASE, FetchError, PUBLISHED_DATA_CATEGORIES, RarityEnum, SkinType, TENCENT_PROFILE_ICON_CDN, assetUrl, cdragonDataUrl, computeAdded, createClient, dataRoot, defaultPatchForCategory, fetchJson, filterLootChests, getGameVersion, getRarityUrl, getVersionInfo, loadCategory, modelviewerUrl, raritiesMap, releaseAssetName, releaseBaseUrl, releaseDataUrl, releaseManifestUrl, rewritePathsDeep, splitId, splitSkinId, substitute, transformChampions, transformLoot, transformSkinlines, transformSkins, transformSummonerEmotes, transformSummonerIconSets, transformSummonerIcons, transformUniverses, transformWardSkinSets, transformWardSkins }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map