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:
1 parent
303332469a
commit
083848ee6e
9 files changed
+347
-41
No files matched your search
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.16';
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.17';
|
||||
|
||||
const strings = {
|
||||
en: {
|
||||
|
||||
Reference in new issue
Block a user