feat: add 76 avatar PNG images and integrate into UI cards, lineup, and Ex-Rank

This commit is contained in:
CAtlas0784 committed 2026-10-16 22:23:16 +07:00
1 parent 9d90c2b845
commit 5cce9bdd04
79 files changed
+133 -6

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+27 -4
View File
@@ -368,16 +368,39 @@ body {
.avatar-header-info { .avatar-header-info {
display: flex; display: flex;
gap: 0.75rem; gap: 0.85rem;
align-items: center; align-items: center;
} }
.avatar-icon-placeholder { .avatar-image-container {
width: 48px; position: relative;
height: 48px; width: 52px;
height: 52px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden;
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.25s ease;
}
.card:hover .avatar-img {
transform: scale(1.08);
}
.avatar-icon-placeholder {
width: 100%;
height: 100%;
background: var(--bg-secondary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
+28 -2
View File
@@ -245,9 +245,20 @@ function renderLineup(lineups) {
} else { } else {
const info = getAvatarInfo(avatarId); const info = getAvatarInfo(avatarId);
card.innerHTML = ` card.innerHTML = `
<div style="display: flex; gap: 0.75rem; align-items: center;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<img class="avatar-img"
src="./assets/avatars/${avatarId}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
alt="${info.name}">
<div class="avatar-icon-placeholder" style="display: none;">${info.name.charAt(0)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div> <div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div>
<div style="font-weight: 700; margin-top: 0.25rem;">${info.name}</div> <div style="font-weight: 700; margin-top: 0.15rem; font-size: 0.95rem;">${info.name}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div> <div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
</div>
</div>
`; `;
} }
el.lineupContainer.appendChild(card); el.lineupContainer.appendChild(card);
@@ -298,7 +309,13 @@ function renderAvatarsGrid() {
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span> <span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
<div> <div>
<div class="avatar-header-info"> <div class="avatar-header-info">
<div class="avatar-icon-placeholder">${meta.name.charAt(0)}</div> <div class="avatar-image-container">
<img class="avatar-img"
src="./assets/avatars/${item.avatar_id}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
alt="${meta.name}">
<div class="avatar-icon-placeholder" style="display: none;">${meta.name.charAt(0)}</div>
</div>
<div> <div>
<div class="avatar-name">${meta.name}</div> <div class="avatar-name">${meta.name}</div>
<div class="avatar-subinfo"> <div class="avatar-subinfo">
@@ -378,12 +395,21 @@ function renderExRanks(exList) {
const localizedMeta = getAvatarInfo(item.avatar_id); const localizedMeta = getAvatarInfo(item.avatar_id);
card.innerHTML = ` card.innerHTML = `
<div style="display: flex; gap: 0.85rem; align-items: center;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<img class="avatar-img"
src="./assets/avatars/${item.avatar_id}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
alt="${localizedMeta.name}">
<div class="avatar-icon-placeholder" style="display: none;">${localizedMeta.name.charAt(0)}</div>
</div>
<div> <div>
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div> <div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
<div style="font-size: 0.8rem; color: var(--text-muted);"> <div style="font-size: 0.8rem; color: var(--text-muted);">
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id} Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
</div> </div>
</div> </div>
</div>
<label class="switch"> <label class="switch">
<input type="checkbox" ${item.unlocked ? 'checked' : ''} data-avatar="${item.avatar_id}"> <input type="checkbox" ${item.unlocked ? 'checked' : ''} data-avatar="${item.avatar_id}">
<span class="slider"></span> <span class="slider"></span>
+78
View File
@@ -0,0 +1,78 @@
const fs = require('fs');
const https = require('https');
const path = require('path');
const ids = [
8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008,
1001, 1224, 1002, 1003, 1004, 1008, 1009, 1013,
1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112,
1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1217, 1218, 1220, 1221, 1222, 1223, 1225,
1005, 1006, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1312, 1313, 1314, 1315, 1317,
1401, 1402, 1403, 1404, 1405, 1407, 1408, 1409, 1410
];
const targetDir = path.join(__dirname, '..', 'assets', 'avatars');
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
function fetchUrl(url, dest) {
return new Promise((resolve) => {
const req = https.get(url, (res) => {
if (res.statusCode === 200) {
const file = fs.createWriteStream(dest);
res.pipe(file);
file.on('finish', () => {
file.close();
resolve(true);
});
} else {
res.resume();
resolve(false);
}
});
req.on('error', () => resolve(false));
req.setTimeout(6000, () => {
req.destroy();
resolve(false);
});
});
}
async function processId(id) {
const dest = path.join(targetDir, `${id}.png`);
if (fs.existsSync(dest) && fs.statSync(dest).size > 1000) {
return { id, status: 'exists' };
}
// 1. Try StarRailRes CDN
const cdnUrl = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${id}.png`;
let ok = await fetchUrl(cdnUrl, dest);
if (ok) return { id, status: 'ok (cdn)' };
// 2. Try raw github
const rawUrl = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/avatar/${id}.png`;
ok = await fetchUrl(rawUrl, dest);
if (ok) return { id, status: 'ok (raw)' };
// Fallbacks for trailblazer female variants if not directly mapped
if ([8004, 8006, 8008].includes(id) && fs.existsSync(path.join(targetDir, '8002.png'))) {
fs.copyFileSync(path.join(targetDir, '8002.png'), dest);
return { id, status: 'fallback 8002' };
}
if ([8003, 8005, 8007].includes(id) && fs.existsSync(path.join(targetDir, '8001.png'))) {
fs.copyFileSync(path.join(targetDir, '8001.png'), dest);
return { id, status: 'fallback 8001' };
}
return { id, status: 'not found' };
}
(async () => {
console.log(`Starting parallel download for ${ids.length} avatars...`);
const chunkSize = 8;
for (let i = 0; i < ids.length; i += chunkSize) {
const chunk = ids.slice(i, i + chunkSize);
const results = await Promise.all(chunk.map(processId));
results.forEach(r => console.log(`Avatar ${r.id}: ${r.status}`));
}
console.log('Finished downloading avatars.');
})();