feat: add visual avatar picker for equipping new relics and light cones

- replace the ~100-option avatar <select>s in Add Relic / Add Light Cone with a searchable picker modal (keep-in-inventory card + avatar cards with element/path/level)
- warn when the chosen character already wears a relic in that slot or a light cone
- click+Confirm or double-click applies; Esc closes only the picker; search autofocused
- localize all new strings (th/en, other locales fall back to en) and refresh chips/picker on language change
- add AGENTS.md with localization and cache-busting rules
- bump app.js v3.20, styles.css v2.29, i18n.js v2.17 and align all i18n imports
This commit is contained in:
CAtlas0784 committed 2026-11-02 09:21:53 +07:00
1 parent 303332469a
commit 083848ee6e
9 files changed
+347 -41

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# Project Rules
## Localization (required)
Every user-facing string must go through `js/i18n.js`. Never hardcode Thai or English UI text in HTML or JS.
- Use `data-i18n`, `data-i18n-placeholder`, `data-i18n-title`, `data-i18n-alt` or `data-i18n-aria-label` in `index.html`.
- Use `t('key', { ...vars })` for strings rendered from JavaScript (including `innerHTML` templates).
- Add every new key to `MODAL_AND_TOOL_TRANSLATIONS` in `js/i18n.js` for both `th` and `en`. Other locales intentionally fall back to English through `t()`, so they do not need duplicate entries.
- When a language changes, dynamic content must refresh. Call the render function from `applyTranslations()` (for example `renderEquipTargetDisplay()` and `renderAvatarAssignGrid()`).
- Keep every import of `i18n.js` on the same `?v=` query string so the browser uses a single module instance. Bump it whenever `js/i18n.js` changes.
## Cache busting
When `css/styles.css` or `js/app.js` changes, bump its `?v=` query string in `index.html`.
## Scope
Do not modify `README.md` or the untracked `docs/` folder unless the task explicitly asks for it.
+34 -11
View File
@@ -1560,10 +1560,10 @@ input:checked + .slider:before {
width: 100%;
display: flex;
align-items: center;
gap: 0.65rem;
gap: 0.55rem;
text-align: left;
padding: 0.5rem 0.65rem;
min-height: 52px;
padding: 0.2rem 0.55rem;
min-height: 40px;
background: rgba(10, 13, 20, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
@@ -1589,8 +1589,8 @@ input:checked + .slider:before {
}
.equip-target-avatar {
width: 38px;
height: 38px;
width: 30px;
height: 30px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
@@ -1599,7 +1599,7 @@ input:checked + .slider:before {
justify-content: center;
background: var(--bg-secondary);
border: 2px solid var(--border-color);
font-size: 1.05rem;
font-size: 0.9rem;
line-height: 1;
}
@@ -1624,7 +1624,7 @@ input:checked + .slider:before {
}
.equip-target-name {
font-size: 0.875rem;
font-size: 0.83rem;
font-weight: 700;
color: var(--text-highlight);
white-space: nowrap;
@@ -1634,26 +1634,41 @@ input:checked + .slider:before {
}
.equip-target-sub {
font-size: 0.7rem;
font-size: 0.66rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.3rem;
gap: 0.25rem;
min-width: 0;
white-space: nowrap;
overflow: hidden;
}
.equip-target-sub .icon-badge {
width: 15px;
height: 15px;
padding: 1.5px;
flex-shrink: 0;
}
.equip-target-cta {
flex-shrink: 0;
font-size: 0.7rem;
font-size: 0.64rem;
font-weight: 800;
color: var(--accent-cyan);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 999px;
padding: 0.2rem 0.6rem;
padding: 0.15rem 0.45rem;
background: rgba(56, 189, 248, 0.08);
white-space: nowrap;
}
.equip-target-cta.has-warning {
color: var(--accent-gold);
border-color: rgba(251, 191, 36, 0.45);
background: rgba(251, 191, 36, 0.1);
}
.assign-picker-modal {
width: min(980px, 96vw) !important;
max-width: 980px !important;
@@ -1817,6 +1832,14 @@ input:checked + .slider:before {
overflow-wrap: anywhere;
}
.assign-picker-empty {
grid-column: 1 / -1;
padding: 1.5rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
.assign-picker-footer {
display: flex;
align-items: center;
+4 -4
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GaCy-Srtool</title>
<link rel="stylesheet" href="css/styles.css?v=2.24">
<link rel="stylesheet" href="css/styles.css?v=2.29">
<link rel="stylesheet" href="css/item-catalog.css?v=2">
<link rel="icon" type="image/png" href="assets/logo.png">
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
@@ -1531,7 +1531,7 @@
</div>
<!-- 4. Level, Promotion, Superimpose, and Equip Avatar -->
<div class="grid-4" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.6rem; margin-bottom: 0.75rem; align-items: end;">
<div class="grid-4" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.6rem; margin-bottom: 0.75rem; align-items: start;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="modalColLevel" style="font-size: 0.75rem;">เลเวล (1-80)</label>
<input type="number" id="add-lc-level" class="form-input" min="1" max="80" value="80" required>
@@ -1629,7 +1629,7 @@
<div class="modal-content assign-picker-modal" role="dialog" aria-modal="true" aria-labelledby="modal-assign-title">
<div class="modal-header assign-picker-header">
<div class="assign-picker-heading">
<h3 id="modal-assign-title" style="font-weight: 800; margin: 0; font-size: 1.15rem;">สวมใส่ให้ตัวละคร</h3>
<h3 id="modal-assign-title" data-i18n="assignPickerTitleRelic" style="font-weight: 800; margin: 0; font-size: 1.15rem;">สวมใส่ให้ตัวละคร</h3>
<div id="modal-assign-subtitle" class="assign-picker-subtitle"></div>
</div>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-assign-picker">✕</button>
@@ -1858,6 +1858,6 @@
<div class="toast-container" id="toast-container"></div>
<!-- Script Modules -->
<script type="module" src="js/app.js?v=3.16"></script>
<script type="module" src="js/app.js?v=3.20"></script>
</body>
</html>
+258 -22
View File
@@ -4,7 +4,7 @@ import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetIn
import { initializeItemCatalog } from './item-catalog.js?v=2';
import { initializeUnstuck } from './unstuck.js?v=1';
import { PRYDWEN_BUILD_GUIDES } from './build-guides-data.js';
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js?v=2.16';
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js?v=2.17';
import {
getAvatarIconUrl,
getLightconeArtUrl,
@@ -1063,6 +1063,14 @@ function applyTranslations() {
if (state.avatarsList.length > 0) {
renderAvatarsGrid();
}
// Keep the equip-target chips and the avatar picker in the active language.
renderEquipTargetDisplay('relic');
renderEquipTargetDisplay('lc');
if (el.modalAvatarAssign?.classList.contains('active')) {
updateAvatarAssignCurrent();
renderAvatarAssignGrid();
}
loadEquipment();
if (analystState.currentReport) {
renderDamageAnalyst();
@@ -2273,6 +2281,233 @@ function renderPickerGrid() {
});
}
/* ==========================================================================
Avatar Assignment Picker (Add Relic / Add Light Cone)
========================================================================== */
const avatarAssignState = { kind: 'relic', pending: 0 };
function avatarAssignSelectEl(kind) {
return kind === 'lc' ? el.addLcAvatar : el.addRelicAvatar;
}
function avatarAssignButtonEl(kind) {
return kind === 'lc' ? el.addLcAvatarBtn : el.addRelicAvatarBtn;
}
function getRelicSlotNumber() {
return parseInt(el.addRelicSlot?.value || '1', 10) || 1;
}
// What (if anything) the given avatar currently wears in this slot.
function findAvatarEquippedItem(kind, avatarId, relicSlot) {
if (kind === 'lc') {
const lc = (state.fullFreesrData?.lightcones || []).find(l => l.equip_avatar === avatarId);
return lc ? { name: getLightconeInfo(lc.item_id).name } : null;
}
const relic = (state.fullFreesrData?.relics || [])
.find(r => r.equip_avatar === avatarId && (r.relic_id % 10) === relicSlot);
if (!relic) return null;
const setId = Math.floor((relic.relic_id % 10000) / 10);
return { name: getRelicSetInfo(setId).name };
}
// Keep the hidden <select> as the source of truth for the submit handlers,
// but only ever hold the single selected option (instead of ~100 <option>s).
function setAvatarAssignValue(kind, avatarId) {
const select = avatarAssignSelectEl(kind);
if (!select) return;
const value = parseInt(avatarId, 10) || 0;
const option = document.createElement('option');
option.value = String(value);
option.textContent = value ? getAvatarInfo(value).name : t('unassigned');
select.innerHTML = '';
select.appendChild(option);
select.value = String(value);
}
function getAssignItemLabel(kind) {
if (kind === 'lc') {
const lcId = parseInt(el.addLcSelectedId?.value || '23001', 10);
return getLightconeInfo(lcId).name;
}
const setId = parseInt(el.addRelicSet?.value || '0', 10);
return `${getRelicSetInfo(setId).name} • ${getSlotInfo(getRelicSlotNumber()).short}`;
}
function renderEquipTargetDisplay(kind) {
const btn = avatarAssignButtonEl(kind);
const select = avatarAssignSelectEl(kind);
if (!btn) return;
const avatarId = parseInt(select?.value || '0', 10) || 0;
const meta = avatarId ? getAvatarInfo(avatarId) : null;
if (!meta) {
btn.classList.add('is-empty');
btn.title = '';
btn.innerHTML = `
<span class="equip-target-avatar">🎒</span>
<span class="equip-target-body">
<span class="equip-target-name">${t('assignKeepInventory')}</span>
<span class="equip-target-sub">${t('assignChooseHint')}</span>
</span>
<span class="equip-target-cta">${t('assignChoose')}</span>
`;
return;
}
btn.classList.remove('is-empty');
const avatar = (state.avatarsList || []).find(a => a.avatar_id === avatarId);
const levelText = avatar ? `Lv.${avatar.level} • E${avatar.rank}` : '';
const existing = findAvatarEquippedItem(kind, avatarId, getRelicSlotNumber());
const warnText = existing
? t(kind === 'lc' ? 'assignReplaceLc' : 'assignReplaceRelic', { name: existing.name })
: '';
btn.title = warnText;
btn.innerHTML = `
<span class="equip-target-avatar">
<img src="${getAvatarIconSrc(avatarId)}" onerror="handleAvatarImgError(this, ${avatarId})" alt="${meta.name}">
</span>
<span class="equip-target-body">
<span class="equip-target-name">${meta.name}</span>
<span class="equip-target-sub">
${getElementIconHtml(meta.element, 12)}
${getPathIconHtml(meta.path, 12)}
<span>${levelText}</span>
</span>
</span>
<span class="equip-target-cta${existing ? ' has-warning' : ''}">${existing ? '⚠ ' : ''}${t('assignChange')}</span>
`;
}
function openAvatarAssignPicker(kind) {
if (!el.modalAvatarAssign) return;
avatarAssignState.kind = kind;
avatarAssignState.pending = parseInt(avatarAssignSelectEl(kind)?.value || '0', 10) || 0;
if (el.modalAssignTitle) {
el.modalAssignTitle.textContent = t(kind === 'lc' ? 'assignPickerTitleLc' : 'assignPickerTitleRelic');
}
if (el.modalAssignSubtitle) {
el.modalAssignSubtitle.innerHTML = t('assignPickerSubtitle', {
item: `<strong>${getAssignItemLabel(kind)}</strong>`
});
}
if (el.assignPickerSearch) el.assignPickerSearch.value = '';
renderAvatarAssignGrid();
updateAvatarAssignCurrent();
el.modalAvatarAssign.classList.add('active');
setTimeout(() => el.assignPickerSearch?.focus(), 40);
}
function renderAvatarAssignGrid() {
if (!el.assignPickerGrid) return;
const kind = avatarAssignState.kind;
const search = (el.assignPickerSearch?.value || '').trim().toLowerCase();
const relicSlot = getRelicSlotNumber();
el.assignPickerGrid.innerHTML = '';
// "Keep in inventory" option
const inventoryCard = document.createElement('div');
inventoryCard.className = 'assign-inventory-card' + (avatarAssignState.pending === 0 ? ' is-selected' : '');
inventoryCard.setAttribute('role', 'option');
inventoryCard.setAttribute('aria-selected', avatarAssignState.pending === 0 ? 'true' : 'false');
inventoryCard.innerHTML = `
<span class="assign-inventory-icon">🎒</span>
<span class="assign-inventory-text">
<strong>${t('assignKeepInventory')}</strong>
<small>${t('assignKeepInventoryHint')}</small>
</span>
<span class="assign-check">✓</span>
`;
inventoryCard.addEventListener('click', () => selectAvatarAssign(0, inventoryCard));
el.assignPickerGrid.appendChild(inventoryCard);
const sortedAvatars = [...(state.avatarsList || [])].sort((a, b) => b.avatar_id - a.avatar_id);
let visible = 0;
sortedAvatars.forEach(item => {
const meta = getAvatarInfo(item.avatar_id);
if (search && !meta.name.toLowerCase().includes(search) && !String(item.avatar_id).includes(search)) {
return;
}
visible += 1;
const equipped = findAvatarEquippedItem(kind, item.avatar_id, relicSlot);
const isSelected = avatarAssignState.pending === item.avatar_id;
const card = document.createElement('div');
card.className = `picker-card-item assign-avatar-card card-rarity-${meta.rarity}${isSelected ? ' is-selected' : ''}`;
card.setAttribute('role', 'option');
card.setAttribute('aria-selected', isSelected ? 'true' : 'false');
card.innerHTML = `
<div class="picker-avatar-img-wrap">
<img class="picker-avatar-img"
src="${getAvatarIconSrc(item.avatar_id)}"
onerror="handleAvatarImgError(this, ${item.avatar_id})"
alt="${meta.name}">
</div>
<div class="picker-avatar-info">
<div class="picker-avatar-name" title="${meta.name}">${meta.name}</div>
<div class="picker-avatar-subinfo">
${getElementIconHtml(meta.element, 18)}
${getPathIconHtml(meta.path, 18)}
<span style="font-weight: 600; margin-left: 0.2rem;">Lv. ${item.level} • E${item.rank}</span>
</div>
${equipped ? `<div class="assign-equipped-badge">⚠ ${t(kind === 'lc' ? 'assignReplaceLc' : 'assignReplaceRelic', { name: equipped.name })}</div>` : ''}
</div>
`;
card.addEventListener('click', () => selectAvatarAssign(item.avatar_id, card));
card.addEventListener('dblclick', () => {
selectAvatarAssign(item.avatar_id, card);
confirmAvatarAssign();
});
el.assignPickerGrid.appendChild(card);
});
if (visible === 0) {
const empty = document.createElement('div');
empty.className = 'assign-picker-empty';
empty.textContent = t('assignNoResults');
el.assignPickerGrid.appendChild(empty);
}
}
function selectAvatarAssign(avatarId, cardEl) {
avatarAssignState.pending = parseInt(avatarId, 10) || 0;
el.assignPickerGrid?.querySelectorAll('.is-selected').forEach(node => node.classList.remove('is-selected'));
cardEl?.classList.add('is-selected');
el.assignPickerGrid?.querySelectorAll('[role="option"]').forEach(node => {
node.setAttribute('aria-selected', node === cardEl ? 'true' : 'false');
});
updateAvatarAssignCurrent();
}
function updateAvatarAssignCurrent() {
if (!el.assignPickerCurrentValue) return;
el.assignPickerCurrentValue.textContent = avatarAssignState.pending === 0
? t('assignKeepInventory')
: getAvatarInfo(avatarAssignState.pending).name;
}
function closeAvatarAssignPicker() {
el.modalAvatarAssign?.classList.remove('active');
}
function confirmAvatarAssign() {
const kind = avatarAssignState.kind;
setAvatarAssignValue(kind, avatarAssignState.pending);
renderEquipTargetDisplay(kind);
closeAvatarAssignPicker();
}
function resetAvatarAssignForForm(kind) {
setAvatarAssignValue(kind, 0);
renderEquipTargetDisplay(kind);
}
// Assign avatar to lineup slot
async function assignAvatarToLineup(slot, avatarId) {
try {
@@ -6776,18 +7011,8 @@ function openAddLcModal() {
const filterRarity = document.getElementById('add-lc-filter-rarity');
if (filterRarity) filterRarity.value = 'all';
// Avatars dropdown
if (el.addLcAvatar) {
el.addLcAvatar.innerHTML = `<option value="0">${t('unassigned') || '-- ยังไม่สวมใส่ (เก็บในคลัง) --'}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addLcAvatar.appendChild(opt);
});
}
// Equip target starts empty ("keep in inventory")
resetAvatarAssignForForm('lc');
if (el.addLcLevel) el.addLcLevel.value = 80;
if (el.addLcPromotion) el.addLcPromotion.value = 6;
@@ -6970,15 +7195,8 @@ async function openAddRelicModal() {
el.addRelicSet.appendChild(opt);
});
el.addRelicAvatar.innerHTML = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addRelicAvatar.appendChild(opt);
});
// Equip target starts empty ("keep in inventory")
resetAvatarAssignForForm('relic');
const firstAvailable = Array.from(el.addRelicSet.options).find(option => !option.disabled);
if (firstAvailable) el.addRelicSet.value = firstAvailable.value;
@@ -8712,6 +8930,24 @@ function setupEvents() {
el.btnClosePickerModal.addEventListener('click', () => el.modalLineupPicker.classList.remove('active'));
el.pickerSearch.addEventListener('input', renderPickerGrid);
// Avatar assignment picker (Add Relic / Add Light Cone)
el.addRelicAvatarBtn?.addEventListener('click', () => openAvatarAssignPicker('relic'));
el.addLcAvatarBtn?.addEventListener('click', () => openAvatarAssignPicker('lc'));
el.assignPickerSearch?.addEventListener('input', renderAvatarAssignGrid);
el.btnCloseAssignPicker?.addEventListener('click', closeAvatarAssignPicker);
el.btnCancelAssignPicker?.addEventListener('click', closeAvatarAssignPicker);
el.btnConfirmAssignPicker?.addEventListener('click', confirmAvatarAssign);
el.modalAvatarAssign?.addEventListener('click', (e) => {
if (e.target === el.modalAvatarAssign) closeAvatarAssignPicker();
});
// Esc closes only the picker, keeping the parent Add Relic / Add LC form open.
el.modalAvatarAssign?.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.stopPropagation();
closeAvatarAssignPicker();
}
});
// Creative Mode Toggle
if (el.toggleCreativeMode) {
el.toggleCreativeMode.checked = state.creativeMode;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.16";
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.17";
export const AVATAR_METADATA = {
"1001": {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.16";
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.17";
export const LIGHTCONE_METADATA = {
"20000": {
+28
View File
@@ -7049,6 +7049,20 @@ for (const [language, description] of Object.entries(ITEM_CATALOG_INTERACTION_DE
// language falls back to en through t(), so no Thai leaks into other locales.
const MODAL_AND_TOOL_TRANSLATIONS = {
th: {
assignPickerTitleRelic: 'สวมใส่รีลิกส์ให้ตัวละคร',
assignPickerTitleLc: 'สวมใส่กรวยแสงให้ตัวละคร',
assignPickerSubtitle: 'เลือกตัวละครที่จะสวมใส่ {item} — หรือเก็บไว้ในคลัง',
assignPickerSearch: '🔍 ค้นหาชื่อหรือ ID ตัวละคร...',
assignKeepInventory: 'เก็บในคลัง (ไม่สวมใส่)',
assignKeepInventoryHint: 'ไอเท็มจะถูกเพิ่มโดยไม่ผูกกับตัวละคร',
assignChoose: 'เลือก',
assignChange: 'เปลี่ยน',
assignChooseHint: 'ยังไม่ได้ผูกกับตัวละคร — คลิกเพื่อเลือก',
assignCurrent: 'เลือกอยู่:',
assignConfirm: 'ตกลง',
assignReplaceLc: 'มีกรวยแสงอยู่แล้ว: {name} (จะถูกแทนที่)',
assignReplaceRelic: 'สล็อตนี้มีอยู่แล้ว: {name} (จะถูกแทนที่)',
assignNoResults: 'ไม่พบตัวละครที่ตรงกับเงื่อนไข',
relicSlotNotEquipped: 'ยังไม่ได้สวมใส่',
relicSlotCreateHint: '+ คลิกเพื่อเลือกหรือสร้าง (แนะนำ: {stat})',
relicSlotUnequip: 'ถอดชิ้นนี้',
@@ -7148,6 +7162,20 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
analystDamageAvPlaceholder: "เฉลี่ยต่อรอบ: -", analystCyclesPlaceholder: "0 รอบ", analystTurnsPlaceholder: "0 เทิร์น", analystAutoTurnsPlaceholder: "เทิร์นอัตโนมัติ: 0", analystDamageAvPlaceholder: "Avg per cycle: -", analystCyclesPlaceholder: "0 Cycles", analystTurnsPlaceholder: "0 Turns", analystAutoTurnsPlaceholder: "Auto Turns: 0", analystDamageAvPlaceholder: "ラウンド平均: -", analystCyclesPlaceholder: "0 ラウンド", analystTurnsPlaceholder: "0 ターン", analystAutoTurnsPlaceholder: "オートターン: 0", analystDamageAvPlaceholder: "라운드당 평균: -", analystCyclesPlaceholder: "0 라운드", analystTurnsPlaceholder: "0 턴", analystAutoTurnsPlaceholder: "자동 턴: 0", analystDamageAvPlaceholder: "Trung bình mỗi vòng: -", analystCyclesPlaceholder: "0 vòng", analystTurnsPlaceholder: "0 lượt", analystAutoTurnsPlaceholder: "Lượt tự động: 0", analystDamageAvPlaceholder: "Rata-rata per siklus: -", analystCyclesPlaceholder: "0 Siklus", analystTurnsPlaceholder: "0 Giliran", analystAutoTurnsPlaceholder: "Giliran otomatis: 0", analystDamageAvPlaceholder: "Promedio por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", analystDamageAvPlaceholder: "Moyenne par cycle : -", analystCyclesPlaceholder: "0 cycle", analystTurnsPlaceholder: "0 tour", analystAutoTurnsPlaceholder: "Tours auto : 0", analystDamageAvPlaceholder: "Durchschnitt pro Zyklus: -", analystCyclesPlaceholder: "0 Zyklen", analystTurnsPlaceholder: "0 Züge", analystAutoTurnsPlaceholder: "Auto-Züge: 0", analystDamageAvPlaceholder: "В среднем за цикл: -", analystCyclesPlaceholder: "0 циклов", analystTurnsPlaceholder: "0 ходов", analystAutoTurnsPlaceholder: "Авто-ходы: 0", analystDamageAvPlaceholder: "Média por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", miniViewFullAnalystTooltip: "เปิดหน้า Damage Analyst ตัวเต็ม", miniViewFullAnalystTooltip: "Open full Damage Analyst", miniViewFullAnalystTooltip: "完全なダメージ分析を開く", miniViewFullAnalystTooltip: "전체 데미지 분석 열기", miniViewFullAnalystTooltip: "Mở Trình phân tích sát thương đầy đủ", miniViewFullAnalystTooltip: "Buka Analis Kerusakan Lengkap", miniViewFullAnalystTooltip: "Abrir analizador de daño completo", miniViewFullAnalystTooltip: "Ouvrir l'analyseur de dégâts complet", miniViewFullAnalystTooltip: "Vollständigen Schadensanalysten öffnen", miniViewFullAnalystTooltip: "Открыть полный анализ урона", miniViewFullAnalystTooltip: "Abrir analisador de dano completo", copyCardGenerated: 'สร้างโดย GaCy-Srtool'
},
en: {
assignPickerTitleRelic: 'Equip Relic to a Character',
assignPickerTitleLc: 'Equip Light Cone to a Character',
assignPickerSubtitle: 'Choose who wears {item} — or keep it in your inventory',
assignPickerSearch: '🔍 Search character name or ID...',
assignKeepInventory: 'Keep in inventory (unequipped)',
assignKeepInventoryHint: 'The item is added without being bound to a character',
assignChoose: 'Choose',
assignChange: 'Change',
assignChooseHint: 'Not bound to a character — click to choose',
assignCurrent: 'Selected:',
assignConfirm: 'Confirm',
assignReplaceLc: 'Already has a Light Cone: {name} (will be replaced)',
assignReplaceRelic: 'This slot is occupied: {name} (will be replaced)',
assignNoResults: 'No characters match your search',
relicSlotNotEquipped: 'Not equipped',
relicSlotCreateHint: '+ Click to choose or create (recommended: {stat})',
relicSlotUnequip: 'Unequip this piece',
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.16';
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.17';
const CATALOG_URL = new URL('../assets/item-catalog.json?v=2', import.meta.url);
const PAGE_SIZE = 48;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage } from './i18n.js?v=2.16';
import { getCurrentLanguage } from './i18n.js?v=2.17';
const strings = {
en: {