fix(showcase): responsive card, full localization, in-game stat icons and clean PNG export
Layout: card now fills its container (3/2/1 columns) with no horizontal scrollbar. i18n: every modal/showcase/console string follows the selected language, named fallbacks for th/en plus English fallback for other locales, localized character names and light-cone passive text. Showcase stats use the official in-game property icons (same set as the overview tab) instead of emoji, including the element DMG icon. Export: fixed 1200px clone with poster metrics, cover-cropped splash art (html2canvas ignores object-fit), transparent background so the rounded corners have no dark wedges, and a two-pass measurement inside the capture iframe so the PNG is never cropped.
This commit is contained in:
1 parent
d9b7a4ce1b
commit
76986eed4a
9 files changed
+775
-187
No files matched your search
+68
-8
@@ -15,6 +15,7 @@
|
||||
--accent-gold: #fbbf24;
|
||||
--accent-purple: #c084fc;
|
||||
--accent-red: #f87171;
|
||||
--accent-blue: #0284c7;
|
||||
|
||||
--glow-cyan: 0 0 20px rgba(56, 189, 248, 0.35);
|
||||
--glow-gold: 0 0 20px rgba(251, 191, 36, 0.35);
|
||||
@@ -1556,10 +1557,11 @@ input:checked + .slider:before {
|
||||
Modal Avatar Detail Widescreen Expansion
|
||||
========================================================================== */
|
||||
#modal-avatar-detail .modal-content {
|
||||
max-width: 1120px !important;
|
||||
width: 96% !important;
|
||||
max-width: min(1280px, 96vw) !important;
|
||||
width: 96vw !important;
|
||||
max-height: 92vh !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -1840,6 +1842,23 @@ input:checked + .slider:before {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.showcase-stat-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
min-width: 15px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
|
||||
}
|
||||
|
||||
.showcase-inline-stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.showcase-stat-val {
|
||||
font-weight: 800;
|
||||
font-family: monospace;
|
||||
@@ -3760,18 +3779,59 @@ input:checked + .slider:before {
|
||||
.console-suggestion-description { max-width:55%; }
|
||||
@media(max-width:640px) { .console-suggestion-option { flex-wrap:wrap; gap:.2rem .5rem; } .console-suggestion-description { display:block; width:100%; max-width:100%; margin-left:1.1rem; } }
|
||||
|
||||
/* การ์ดกว้างตามเนื้อหาจริง ไม่ล็อก 1050px และไม่ตัดขอบ */
|
||||
/* การ์ดยืดหยุ่นตามพื้นที่จริง ไม่ล็อกความกว้างและไม่เกิด scrollbar แนวนอน */
|
||||
.showcase-card-layout {
|
||||
width: max-content; /* เดิม: width/min-width 1050px */
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
min-width: 0;
|
||||
margin: 0 auto;
|
||||
overflow: visible; /* เดิม: hidden ทำให้ส่วนเกินโดนตัด */
|
||||
grid-template-columns: 360px 340px max-content;
|
||||
grid-template-columns: minmax(230px, 360px) minmax(210px, 340px) minmax(240px, 1fr);
|
||||
gap: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.showcase-splash-col { width: 360px; }
|
||||
.showcase-scroll-wrapper {
|
||||
overflow-x: visible;
|
||||
max-width: 100%;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* คอลัมน์รีลิกส์ขยายตามชื่อเซ็ตและ main stat ที่ยาวที่สุด */
|
||||
.showcase-relics-col { min-width: 420px; }
|
||||
.showcase-splash-col { width: 100%; min-width: 0; }
|
||||
.showcase-center-col { min-width: 0; }
|
||||
.showcase-relics-col { min-width: 0; }
|
||||
.showcase-relic-card:hover { transform: none; }
|
||||
|
||||
/* Export clone: always use the wide three-column poster layout so a PNG keeps
|
||||
the same composition regardless of the window size it was exported from. */
|
||||
.showcase-card-layout.showcase-export-mode {
|
||||
width: 1200px !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
/* Pin the poster metrics so the exported layout does not inherit the
|
||||
responsive (2-column / mobile) spacing of the current window size. */
|
||||
padding: 1.5rem !important;
|
||||
gap: 1.25rem !important;
|
||||
grid-template-columns: 360px 340px minmax(240px, 1fr) !important;
|
||||
}
|
||||
.showcase-card-layout.showcase-export-mode .showcase-splash-col {
|
||||
grid-column: auto !important;
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.showcase-card-layout {
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr);
|
||||
}
|
||||
.showcase-splash-col { grid-column: 1 / -1; max-width: 420px; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.showcase-card-layout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.showcase-splash-col { max-width: none; }
|
||||
}
|
||||
+12
-12
@@ -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.19">
|
||||
<link rel="stylesheet" href="css/styles.css?v=2.21">
|
||||
<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>
|
||||
@@ -273,7 +273,7 @@
|
||||
</form>
|
||||
<div class="console-command-preview-box" aria-live="polite" aria-atomic="true">
|
||||
<div class="console-preview-label">COMMAND PREVIEW</div>
|
||||
<code id="console-command-preview">พิมพ์ / เพื่อเลือกคำสั่ง</code>
|
||||
<code id="console-command-preview" data-i18n="consolePreviewHint">พิมพ์ / เพื่อเลือกคำสั่ง</code>
|
||||
<div id="console-command-description"></div>
|
||||
<div id="console-command-argument"></div>
|
||||
</div>
|
||||
@@ -285,7 +285,7 @@
|
||||
<div id="console-suggestion-options" role="listbox" aria-label="Command suggestions"></div>
|
||||
</div>
|
||||
<div style="font-size: 0.725rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>↑ ↓ เลื่อนรายการ · Tab เติมค่า · Shift+Tab ย้อนรายการ · Esc ปิด · Enter ส่งคำสั่ง</span>
|
||||
<span data-i18n="consoleKeyboardHint">↑ ↓ เลื่อนรายการ · Tab เติมค่า · Shift+Tab ย้อนรายการ · Esc ปิด · Enter ส่งคำสั่ง</span>
|
||||
<span id="lbl-console-conn-status" style="color: var(--accent-green);">● Server Online</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -680,12 +680,12 @@
|
||||
<div class="ex-switch-card">
|
||||
<div>
|
||||
<div style="font-weight: 700;">Freecam / FaceCam</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง</div>
|
||||
<label for="camera-mode">โหมดกล้อง</label>
|
||||
<select id="camera-mode"><option value="freecam">Freecam</option><option value="facecam">FaceCam (ทดลอง)</option></select>
|
||||
<label for="camera-distance">ระยะ FaceCam</label>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);" data-i18n="cameraHint">เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง</div>
|
||||
<label for="camera-mode" data-i18n="cameraMode">โหมดกล้อง</label>
|
||||
<select id="camera-mode"><option value="freecam">Freecam</option><option value="facecam" data-i18n="cameraFaceCamExperimental">FaceCam (ทดลอง)</option></select>
|
||||
<label for="camera-distance" data-i18n="cameraFaceCamDistance">ระยะ FaceCam</label>
|
||||
<input id="camera-distance" type="number" min="0.5" max="5" step="0.1" value="2">
|
||||
<button type="button" class="btn btn-success" id="btn-save-camera">บันทึกกล้อง</button>
|
||||
<button type="button" class="btn btn-success" id="btn-save-camera" data-i18n="cameraSave">บันทึกกล้อง</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ex-switch-card">
|
||||
@@ -1401,7 +1401,7 @@
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div style="font-weight: 800; font-size: 1rem; color: var(--text-highlight); display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span data-i18n="modalTabShowcase">🖼️ การ์ดแสดงผล</span>
|
||||
<span class="btn-sm btn-secondary" id="showcase-build-tag" data-i18n="overviewStatsTitle">Build Overview</span>
|
||||
<span class="btn-sm btn-secondary" id="showcase-build-tag" data-i18n="showcaseBuildTag">Build Overview</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button type="button" class="btn btn-sm btn-gold" id="btn-showcase-auto-relics" data-i18n="showcaseAutoRelics">
|
||||
@@ -1416,8 +1416,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Showcase Card UI Container with horizontal scroll wrapper -->
|
||||
<div class="showcase-scroll-wrapper" style="overflow-x: auto; padding-bottom: 0.5rem; max-width: 100%;">
|
||||
<!-- The Showcase Card UI Container -->
|
||||
<div class="showcase-scroll-wrapper" style="padding-bottom: 0.5rem; max-width: 100%;">
|
||||
<div id="showcase-card-container" class="showcase-card-layout">
|
||||
<!-- Rendered dynamically by JS -->
|
||||
</div>
|
||||
@@ -1823,6 +1823,6 @@
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<!-- Script Modules -->
|
||||
<script type="module" src="js/app.js?v=3.3"></script>
|
||||
<script type="module" src="js/app.js?v=3.14"></script>
|
||||
</body>
|
||||
</html>
|
||||
+20
-3
@@ -1,4 +1,4 @@
|
||||
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js";
|
||||
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.16";
|
||||
|
||||
export const AVATAR_METADATA = {
|
||||
"1001": {
|
||||
@@ -2099,6 +2099,23 @@ export const CW_ROLE_METADATA = {
|
||||
}
|
||||
};
|
||||
|
||||
// Localized defaults used when a Trailblazer entry stores the {NICKNAME} placeholder.
|
||||
const TRAILBLAZER_DEFAULT_NAMES = {
|
||||
th: 'ผู้บุกเบิก (Trailblazer)',
|
||||
en: 'Trailblazer',
|
||||
'zh-CN': '开拓者',
|
||||
'zh-TW': '開拓者',
|
||||
ja: '開拓者',
|
||||
ko: '개척자',
|
||||
ru: 'Первопроходец',
|
||||
vi: 'Nhà Khai Phá',
|
||||
id: 'Trailblazer',
|
||||
es: 'El Emisario',
|
||||
fr: 'Le Pionnier',
|
||||
de: 'Der Bahnbrecher',
|
||||
pt: 'O Desbravador'
|
||||
};
|
||||
|
||||
export function getAvatarInfo(avatarId, langOverride = null) {
|
||||
const lang = langOverride || getCurrentLanguage();
|
||||
const meta = AVATAR_METADATA[avatarId] || CW_ROLE_METADATA[avatarId];
|
||||
@@ -2113,9 +2130,9 @@ export function getAvatarInfo(avatarId, langOverride = null) {
|
||||
};
|
||||
}
|
||||
|
||||
let localizedName = meta.names?.[lang] || meta.names?.th || meta.names?.en || `Avatar ${avatarId}`;
|
||||
let localizedName = meta.names?.[lang] || meta.names?.en || meta.names?.th || `Avatar ${avatarId}`;
|
||||
if (localizedName.includes('{NICKNAME}')) {
|
||||
const tbName = lang === 'th' ? 'ผู้บุกเบิก (Trailblazer)' : 'Trailblazer';
|
||||
const tbName = TRAILBLAZER_DEFAULT_NAMES[lang] || TRAILBLAZER_DEFAULT_NAMES.en;
|
||||
localizedName = localizedName.replace(/\{NICKNAME\}/g, tbName).trim();
|
||||
if (!localizedName) localizedName = tbName;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,88 @@
|
||||
// Command syntax mirrors the SDK dispatcher. Runtime IDs come from its read-only
|
||||
// completion catalog; sample quantities are examples, not game resource IDs.
|
||||
|
||||
// The canonical command metadata below is authored in Thai. Non-Thai locales get
|
||||
// English text from this map so the console follows the selected language.
|
||||
let currentLanguage = 'th';
|
||||
|
||||
export function setCommandLanguage(lang) {
|
||||
currentLanguage = lang === 'th' ? 'th' : 'en';
|
||||
}
|
||||
|
||||
const EN_TEXT = {
|
||||
'แสดงรายการคำสั่งและวิธีใช้': 'Show the command list and usage',
|
||||
'ส่งคำสั่งฟื้นฟู HP, พลังงาน และแต้มสกิล': 'Restore HP, energy and skill points',
|
||||
'ปรับตัวละครทั้งหมดเป็นเลเวล 80 และ E6': 'Set all characters to level 80 and E6',
|
||||
'กู้หน้าจอที่หมุนค้าง โดยเก็บความคืบหน้า CW': 'Recover a spinning screen while keeping CW progress',
|
||||
'ล้างสมาชิกทีมปัจจุบัน': 'Clear the current team',
|
||||
'ปรับเลเวลและ Eidolon ของตัวละคร': 'Set a character level and Eidolon',
|
||||
'ส่งคำสั่งเพิ่มไอเท็ม': 'Add items',
|
||||
'เปลี่ยนฉากปลายทาง': 'Change the destination scene',
|
||||
'ดู/ตั้งเวลาเกม โดยไม่เปลี่ยนเวลา Windows': 'View or set game time without changing Windows time',
|
||||
'แสดงวิธีใช้คำสั่ง CW': 'Show CW command usage',
|
||||
'ดูเงิน HP เลเวล และสถานะรอบ CW': 'View money, HP, level and CW round status',
|
||||
'แสดงรายการ ID ของ CW': 'List CW IDs',
|
||||
'เพิ่ม EXP ในรอบ CW': 'Add EXP in the CW round',
|
||||
'ตั้งค่าพลังบุกเบิก (Trailblaze Power / เหรียญร้านรถไฟ) ในรอบ CW': 'Set Trailblaze Power (train shop currency) in the CW round',
|
||||
'เพิ่มตัวละคร CW (ใช้ได้ใน Preparation)': 'Add a CW character (Preparation only)',
|
||||
'จำนวนเต็มบวก เช่น 1, 10 หรือ 100': 'Positive integer, e.g. 1, 10 or 100',
|
||||
'เลือก ID ตัวละครจากรายชื่อ': 'Pick a character ID from the list',
|
||||
'เลเวล 1–80 (ไม่ใส่เพื่อคงค่าเดิม)': 'Level 1–80 (omit to keep the current value)',
|
||||
'Eidolon 0–6': 'Eidolon 0–6',
|
||||
'ID ไอเท็มจาก catalog เซิร์ฟเวอร์': 'Item ID from the server catalog',
|
||||
'เลือก Plane ID': 'Pick a Plane ID',
|
||||
'เลือกชั้นที่อยู่ใน Plane นี้': 'Pick a floor inside this Plane',
|
||||
'status = ดูเวลา · off = เวลาจริง · หรือ Unix seconds / RFC3339 พร้อม timezone': 'status = view time · off = real time · or Unix seconds / RFC3339 with timezone',
|
||||
'เลือกชนิดข้อมูล': 'Pick a data type',
|
||||
'HP 1 ถึงเพดานของรอบปัจจุบัน': 'HP from 1 up to the current round cap',
|
||||
'จำนวนเต็มไม่ติดลบ (เป็นยอดรวม ยกเว้น exp ที่เป็นจำนวนเพิ่ม)': 'Non-negative integer (a total, except exp which is added)',
|
||||
'จำนวนพลังบุกเบิก เช่น 10, 50, 100': 'Trailblaze Power amount, e.g. 10, 50, 100',
|
||||
'เลือก Role ID ของ CW': 'Pick a CW Role ID',
|
||||
'ดาวที่มีข้อมูลรองรับสำหรับ Role นี้': 'Supported stars for this Role',
|
||||
'เลือก ID จากข้อมูล CW ของ Client': 'Pick an ID from the Client CW data'
|
||||
};
|
||||
|
||||
function txt(value) {
|
||||
if (currentLanguage === 'th' || !value) return value;
|
||||
if (EN_TEXT[value]) return EN_TEXT[value];
|
||||
let match = value.match(/^ตั้งค่า (.+) ในรอบ CW$/);
|
||||
if (match) return `Set ${match[1]} in the CW round`;
|
||||
match = value.match(/^เพิ่ม (.+) ในรอบ CW \(Preparation\)$/);
|
||||
if (match) return `Add ${match[1]} in the CW round (Preparation)`;
|
||||
return value;
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
th: {
|
||||
optional: 'ช่องเสริม',
|
||||
required: 'ช่องที่ต้องใส่',
|
||||
missing: (list) => 'ยังขาด: ' + list,
|
||||
allSet: 'ใส่ครบแล้ว · Enter หรือ Run เพื่อส่งคำสั่ง',
|
||||
nextArg: 'ช่องถัดไป: เลือกคำสั่งย่อยจากรายการด้านล่าง',
|
||||
navHint: '↑ ↓ เลื่อน · Tab เติมค่าที่เลือก · Esc ปิดรายการ',
|
||||
notFound: 'ไม่พบคำสั่งนี้ในรายการ — ตรวจชื่อคำสั่งก่อนส่ง',
|
||||
typeSlash: 'พิมพ์ / เพื่อเลือกคำสั่ง'
|
||||
},
|
||||
en: {
|
||||
optional: 'Optional argument',
|
||||
required: 'Required argument',
|
||||
missing: (list) => 'Missing: ' + list,
|
||||
allSet: 'All set · press Enter or Run to send',
|
||||
nextArg: 'Next argument: pick a subcommand from the list below',
|
||||
navHint: '↑ ↓ navigate · Tab fill selected · Esc close',
|
||||
notFound: 'Command not found — check the name before sending',
|
||||
typeSlash: 'Type / to choose a command'
|
||||
}
|
||||
};
|
||||
|
||||
function labels() {
|
||||
return LABELS[currentLanguage] || LABELS.en;
|
||||
}
|
||||
|
||||
export function describeCommand(command) {
|
||||
return txt(command?.description || '');
|
||||
}
|
||||
|
||||
const arg = (name, help, source, optional = false) => ({ name, help, source, optional });
|
||||
const quantity = (name = 'count') => arg(name, 'จำนวนเต็มบวก เช่น 1, 10 หรือ 100', ['1', '10', '100'], true);
|
||||
const spec = (path, description, args = []) => ({ path: path.split(' '), description, args });
|
||||
@@ -39,7 +122,7 @@ export function inspectCommand(value = '', caret = value.length, catalogs = {})
|
||||
const command = value.trim() ? exact || matching.find(c => (c.path[index] || '').toLowerCase().startsWith(query)) || null : null;
|
||||
const suggestions = [];
|
||||
for (const c of matching) if (index < c.path.length && c.path[index].toLowerCase().startsWith(query)) {
|
||||
suggestions.push({ value:c.path[index], label:c.path[index], description:c.description });
|
||||
suggestions.push({ value:c.path[index], label:c.path[index], description:txt(c.description) });
|
||||
}
|
||||
const argIndex = exact ? index-exact.path.length : -1;
|
||||
const active = exact?.args[argIndex];
|
||||
@@ -50,15 +133,16 @@ export function inspectCommand(value = '', caret = value.length, catalogs = {})
|
||||
if (active.source === 'cw_stars') choices = (catalogs.cw_roles || []).filter(c => String(c.value) === words[exact.path.length]).map(c => String(c.star));
|
||||
for (const entry of choices) {
|
||||
const choice = typeof entry === 'object' ? entry : {value:String(entry),label:String(entry)};
|
||||
if ((choice.value+' '+(choice.label || '')).toLowerCase().includes(query)) suggestions.push({...choice, description:active.help});
|
||||
if ((choice.value+' '+(choice.label || '')).toLowerCase().includes(query)) suggestions.push({...choice, description:txt(active.help)});
|
||||
}
|
||||
}
|
||||
const seen = new Set();
|
||||
const unique = suggestions.filter(s => { if(seen.has(s.value))return false;seen.add(s.value);return true; });
|
||||
const missing = exact ? exact.args.filter((a,i) => !a.optional && !words[exact.path.length+i]).map(a => a.name) : [];
|
||||
const L = labels();
|
||||
return { command, active, argIndex, index, start, end, suggestions:unique, missing,
|
||||
preview:command ? [...command.path, ...command.args.map((a,i) => words[command.path.length+i] || (a.optional ? '['+a.name+']' : '<'+a.name+'>'))].join(' ') : value.trim() ? 'ไม่พบคำสั่งนี้ในรายการ — ตรวจชื่อคำสั่งก่อนส่ง' : 'พิมพ์ / เพื่อเลือกคำสั่ง',
|
||||
hint:active ? `${active.optional ? 'ช่องเสริม' : 'ช่องที่ต้องใส่'}: ${active.name} — ${active.help}` : exact ? (missing.length ? 'ยังขาด: '+missing.join(', ') : 'ใส่ครบแล้ว · Enter หรือ Run เพื่อส่งคำสั่ง') : command && index > 0 ? 'ช่องถัดไป: เลือกคำสั่งย่อยจากรายการด้านล่าง' : '↑ ↓ เลื่อน · Tab เติมค่าที่เลือก · Esc ปิดรายการ',
|
||||
preview:command ? [...command.path, ...command.args.map((a,i) => words[command.path.length+i] || (a.optional ? '['+a.name+']' : '<'+a.name+'>'))].join(' ') : value.trim() ? L.notFound : L.typeSlash,
|
||||
hint:active ? `${active.optional ? L.optional : L.required}: ${active.name} — ${txt(active.help)}` : exact ? (missing.length ? L.missing(missing.join(', ')) : L.allSet) : command && index > 0 ? L.nextArg : L.navHint,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCurrentLanguage, translatePath } from "./i18n.js";
|
||||
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.16";
|
||||
|
||||
export const LIGHTCONE_METADATA = {
|
||||
"20000": {
|
||||
@@ -4326,7 +4326,7 @@ export function getLightconeInfo(lcId, langOverride = null) {
|
||||
rawPath: "Unknown"
|
||||
};
|
||||
}
|
||||
const localizedName = meta.names?.[lang] || meta.names?.th || meta.names?.en || `Lightcone ${lcId}`;
|
||||
const localizedName = meta.names?.[lang] || meta.names?.en || meta.names?.th || `Lightcone ${lcId}`;
|
||||
return {
|
||||
name: localizedName,
|
||||
rarity: meta.rarity,
|
||||
@@ -4349,7 +4349,7 @@ export function getRelicSetInfo(setIdOrRelicId, langOverride = null) {
|
||||
name: `Relic Set ${setId}`
|
||||
};
|
||||
}
|
||||
const localizedName = meta.names?.[lang] || meta.names?.th || meta.names?.en || `Relic Set ${setId}`;
|
||||
const localizedName = meta.names?.[lang] || meta.names?.en || meta.names?.th || `Relic Set ${setId}`;
|
||||
return {
|
||||
id: setId,
|
||||
name: localizedName
|
||||
|
||||
+207
@@ -7044,6 +7044,213 @@ for (const [language, description] of Object.entries(ITEM_CATALOG_INTERACTION_DE
|
||||
TRANSLATIONS[language].itemCatalogDescription = description;
|
||||
}
|
||||
|
||||
// Strings that used to be hardcoded in the character modal, showcase card,
|
||||
// dashboard console and camera panel. Only th/en are defined here; every other
|
||||
// language falls back to en through t(), so no Thai leaks into other locales.
|
||||
const MODAL_AND_TOOL_TRANSLATIONS = {
|
||||
th: {
|
||||
relicSlotNotEquipped: 'ยังไม่ได้สวมใส่',
|
||||
relicSlotCreateHint: '+ คลิกเพื่อเลือกหรือสร้าง (แนะนำ: {stat})',
|
||||
relicSlotUnequip: 'ถอดชิ้นนี้',
|
||||
relicSubstatFallback: '4 ออปชั่นรอง (คลิกเพื่อปรับแต่ง)',
|
||||
relicSetNoBonus: 'ยังไม่มีเอฟเฟกต์เซ็ตทำงาน (ต้องใส่เซ็ตเดียวกันอย่างน้อย 2 หรือ 4 ชิ้น)',
|
||||
relicSet4Fallback: 'เปิดใช้งานเอฟเฟกต์ครบ 4 ชิ้น',
|
||||
relicSet2Fallback: 'เปิดใช้งานเอฟเฟกต์ 2 ชิ้น',
|
||||
overviewRelicCount: '{count}/6 ชิ้นสวมใส่ (ยังไม่มีโบนัสเซ็ต)',
|
||||
relicPickerEditTitle: '🔧 แก้ไขรีลิกส์: {set} ({slot}) - {name}',
|
||||
relicPickerEditSubtitle: 'ปรับแต่งเซ็ต สเตตัสหลัก และออฟรองของรีลิกส์ชิ้นนี้ (#{id})',
|
||||
relicPickerEditing: '✏️ กำลังแก้ไขรีลิกส์ #{id}',
|
||||
relicPickerSaveChanges: '💾 บันทึกการแก้ไขออฟรองชิ้นนี้',
|
||||
relicPickerEquipNow: '⚡ สวมใส่ชิ้นนี้ทันที (+15 5★ ออฟที่เลือก)',
|
||||
relicPickerInInventory: 'ชิ้นในคลัง',
|
||||
relicPickerEditThis: '✏️ แก้ไขออฟรองชิ้นนี้',
|
||||
relicPickerEditThisTitle: 'โหลดข้อมูลชิ้นนี้ลงมาแก้ไขออฟรอง',
|
||||
relicPickerRecommended: 'ออฟรองแนะนำ 4 แถวเทพ ปรับแต่งได้ตามต้องการด้านล่าง',
|
||||
substatMainConflict: ' (ตรงกับ Main Stat)',
|
||||
substatAlreadyChosen: ' (เลือกแล้ว)',
|
||||
substatSlotLabel: 'ออฟรอง {n}',
|
||||
substatRolls: '+{value} ({count} โรล)',
|
||||
substatDecreaseRoll: 'ลดโรล',
|
||||
substatIncreaseRoll: 'เพิ่มโรล',
|
||||
relicEmptyInventoryTitle: 'ไม่มีรีลิกส์สำหรับช่องนี้ในคลัง',
|
||||
relicEmptyInventoryHint: 'สามารถสลับไปที่แท็บ "⚡ สวมใส่ / สร้างชิ้นแนะนำ" เพื่อสร้างชิ้นใหม่ได้ทันที!',
|
||||
relicEquippedNow: 'กำลังสวมใส่อยู่',
|
||||
relicWornBy: 'สวมใส่โดย: {name}',
|
||||
relicFreeInInventory: 'ว่างในคลัง',
|
||||
relicInUse: 'ใช้งานอยู่',
|
||||
relicSelectThis: 'เลือกชิ้นนี้',
|
||||
lcNoneEquipped: 'ยังไม่ได้สวมใส่ Light Cone',
|
||||
lcSelectPrompt: 'กรุณาเลือก Light Cone ที่ต้องการสวมใส่',
|
||||
lcSamePathGroup: '⭐ Light Cones สาย {path}',
|
||||
lcOtherPathGroup: '🗡️ Light Cones สายอื่นๆ',
|
||||
lcNoResults: 'ไม่พบกรวยแสงตามเงื่อนไขที่ค้นหา',
|
||||
lcEquippedBadge: 'สวมใส่อยู่',
|
||||
lcSignatureBadge: 'ประจำตัว',
|
||||
lcSelectTitle: '🗡️ เลือกสวมใส่กรวยแสง: {name}',
|
||||
eqSkillFallbackTitle: 'สกิลพิเศษประจำกรวยแสง',
|
||||
eqSkillFallbackDesc: 'เพิ่มพลังต่อสู้และเสริมกลไกเฉพาะสายของตัวละครเมื่อติดตั้ง',
|
||||
eqEquippedByLabel: 'ผู้สวมใส่ (Equipped By):',
|
||||
eqSaveWearer: '💾 บันทึกสวมใส่',
|
||||
eqDelete: '🗑️ ลบ',
|
||||
eqDeleteTitle: 'ลบออกจากคลัง',
|
||||
eqSlotN: 'ชิ้นที่ {n}',
|
||||
eqMainStatLabel: 'สเตตัสหลัก (Main Stat)',
|
||||
eqSubstatsLabel: 'ออฟรอง (Substats):',
|
||||
eqNoSubstats: 'ไม่มีข้อมูลออฟรอง',
|
||||
eqEditSubstats: '🔧 ปรับแต่งออฟรอง / สเตตัส (Edit Substats)',
|
||||
eqSet2Fallback: 'เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด',
|
||||
eqSet4Fallback: 'ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง',
|
||||
showcaseActiveSets: 'โบนัสเซ็ตที่ทำงาน',
|
||||
showcaseEquippedRelics: 'รีลิกส์ที่สวมใส่ (+15)',
|
||||
showcaseBuildTag: 'ภาพรวมบิลด์',
|
||||
showcaseRankLevel: 'S{rank} • Lv. {level}/80',
|
||||
consolePreviewHint: 'พิมพ์ / เพื่อเลือกคำสั่ง',
|
||||
consoleDescriptionHint: 'เลือกคำสั่งเพื่อดูตัวอย่างและช่องที่ต้องใส่',
|
||||
consoleArgumentHint: '↑ ↓ เลื่อน · Tab เติมค่าที่เลือก · Esc ปิดรายการ',
|
||||
consoleKeyboardHint: '↑ ↓ เลื่อนรายการ · Tab เติมค่า · Shift+Tab ย้อนรายการ · Esc ปิด · Enter ส่งคำสั่ง',
|
||||
cameraHint: 'เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง',
|
||||
cameraMode: 'โหมดกล้อง',
|
||||
cameraFaceCamDistance: 'ระยะ FaceCam',
|
||||
cameraSave: 'บันทึกกล้อง',
|
||||
cameraFaceCamExperimental: 'FaceCam (ทดลอง)',
|
||||
cameraDistanceError: 'ระยะ FaceCam ต้องอยู่ระหว่าง 0.5 ถึง 5',
|
||||
cameraSaved: 'บันทึกกล้องแล้ว',
|
||||
lcPassiveFallback: 'เพิ่มค่าสถานะพื้นฐานตามสาย <strong>{path}</strong> และเพิ่มประสิทธิภาพความเสียหายและการสนับสนุน <strong>{value}%</strong> เมื่อเข้าสู่การต่อสู้',
|
||||
slotHeadShort: 'ส่วนหัว',
|
||||
slotHandsShort: 'ส่วนมือ',
|
||||
slotBodyShort: 'ลำตัว',
|
||||
slotFeetShort: 'รองเท้า',
|
||||
slotSphereShort: 'ลูกแก้ว',
|
||||
slotRopeShort: 'สร้อย',
|
||||
traceFallbackA2: 'ร่องรอย A2',
|
||||
traceFallbackA4: 'ร่องรอย A4',
|
||||
traceFallbackA6: 'ร่องรอย A6',
|
||||
printSavePdf: 'พิมพ์ / บันทึกเป็น PDF หรือรูปภาพ',
|
||||
printClose: 'ปิดหน้าต่าง',
|
||||
exRankNoData: 'ไม่มีข้อมูล Ex-Rank',
|
||||
exRankEnabled: 'เปิดใช้งานแล้ว',
|
||||
exRankDisabled: 'ปิดการใช้งาน',
|
||||
jsonStatusLoading: 'กำลังโหลด {file}...',
|
||||
jsonStatusMemory: '{file} (โหลดจากหน่วยความจำ/แคช)',
|
||||
jsonStatusServer: '{file} (โหลดจากเซิร์ฟเวอร์)',
|
||||
jsonStatusBrowserCache: '{file} (โหลดจากแคชของเบราว์เซอร์)',
|
||||
jsonStatusCache: '{file} (โหลดจากแคช)',
|
||||
jsonStatusDump: '{file} (โครงสร้าง Firefly dump)',
|
||||
jsonOfflineNoCache: 'Offline: ไม่มีแคช สามารถอัปโหลดหรือพิมพ์ JSON เองได้',
|
||||
jsonOfflinePaste: 'Offline: สามารถพิมพ์หรือวาง JSON เองได้',
|
||||
jsonCacheUnavailable: 'ไม่สามารถตรวจสอบได้',
|
||||
copyCardName: 'ชื่อ',
|
||||
copyCardPath: 'สาย',
|
||||
copyCardElement: 'ธาตุ',
|
||||
copyCardLevel: 'เลเวล',
|
||||
copyCardEidolon: 'Eidolon',
|
||||
copyCardLightCone: 'กรวยแสง',
|
||||
copyCardGenerated: 'สร้างโดย GaC-Srtool'
|
||||
},
|
||||
en: {
|
||||
relicSlotNotEquipped: 'Not equipped',
|
||||
relicSlotCreateHint: '+ Click to choose or create (recommended: {stat})',
|
||||
relicSlotUnequip: 'Unequip this piece',
|
||||
relicSubstatFallback: '4 substats (click to customize)',
|
||||
relicSetNoBonus: 'No set effect active (equip at least 2 or 4 pieces of the same set)',
|
||||
relicSet4Fallback: '4-piece set effect active',
|
||||
relicSet2Fallback: '2-piece set effect active',
|
||||
overviewRelicCount: '{count}/6 equipped (no set bonus yet)',
|
||||
relicPickerEditTitle: '🔧 Edit relic: {set} ({slot}) - {name}',
|
||||
relicPickerEditSubtitle: 'Customize the set, main stat and substats of this relic (#{id})',
|
||||
relicPickerEditing: '✏️ Editing relic #{id}',
|
||||
relicPickerSaveChanges: '💾 Save substat changes',
|
||||
relicPickerEquipNow: '⚡ Equip this piece now (+15 5★ with selected stats)',
|
||||
relicPickerInInventory: 'In inventory',
|
||||
relicPickerEditThis: '✏️ Edit this piece',
|
||||
relicPickerEditThisTitle: 'Load this piece to edit its substats',
|
||||
relicPickerRecommended: 'Recommended 4 substats - customize them below',
|
||||
substatMainConflict: ' (matches main stat)',
|
||||
substatAlreadyChosen: ' (already chosen)',
|
||||
substatSlotLabel: 'Substat {n}',
|
||||
substatRolls: '+{value} ({count} rolls)',
|
||||
substatDecreaseRoll: 'Decrease rolls',
|
||||
substatIncreaseRoll: 'Increase rolls',
|
||||
relicEmptyInventoryTitle: 'No relics for this slot in the inventory',
|
||||
relicEmptyInventoryHint: 'Switch to the "⚡ Equip / Create recommended" tab to create a new piece.',
|
||||
relicEquippedNow: 'Currently equipped',
|
||||
relicWornBy: 'Equipped by: {name}',
|
||||
relicFreeInInventory: 'Free in inventory',
|
||||
relicInUse: 'In use',
|
||||
relicSelectThis: 'Select this piece',
|
||||
lcNoneEquipped: 'No Light Cone equipped',
|
||||
lcSelectPrompt: 'Choose a Light Cone to equip',
|
||||
lcSamePathGroup: '⭐ {path} Light Cones',
|
||||
lcOtherPathGroup: '🗡️ Other paths',
|
||||
lcNoResults: 'No Light Cones match your search',
|
||||
lcEquippedBadge: 'Equipped',
|
||||
lcSignatureBadge: 'Signature',
|
||||
lcSelectTitle: '🗡️ Choose a Light Cone: {name}',
|
||||
eqSkillFallbackTitle: 'Light Cone passive',
|
||||
eqSkillFallbackDesc: 'Boosts combat power and reinforces the character path mechanics when equipped',
|
||||
eqEquippedByLabel: 'Equipped by:',
|
||||
eqSaveWearer: '💾 Save equipment',
|
||||
eqDelete: '🗑️ Delete',
|
||||
eqDeleteTitle: 'Remove from inventory',
|
||||
eqSlotN: 'Piece {n}',
|
||||
eqMainStatLabel: 'Main stat',
|
||||
eqSubstatsLabel: 'Substats:',
|
||||
eqNoSubstats: 'No substat data',
|
||||
eqEditSubstats: '🔧 Edit substats',
|
||||
eqSet2Fallback: 'Improves performance based on the set',
|
||||
eqSet4Fallback: 'Unlocks advanced combat effects',
|
||||
showcaseActiveSets: 'Active Set Bonuses',
|
||||
showcaseEquippedRelics: 'Equipped Relics (+15)',
|
||||
showcaseBuildTag: 'Build Overview',
|
||||
showcaseRankLevel: 'S{rank} • Lv. {level}/80',
|
||||
consolePreviewHint: 'Type / to choose a command',
|
||||
consoleDescriptionHint: 'Select a command to see an example and its arguments',
|
||||
consoleArgumentHint: '↑ ↓ navigate · Tab fill selected · Esc close',
|
||||
consoleKeyboardHint: '↑ ↓ navigate · Tab fill · Shift+Tab previous · Esc close · Enter send',
|
||||
cameraHint: 'Choose a mode, then press Tab in game to toggle the camera',
|
||||
cameraMode: 'Camera mode',
|
||||
cameraFaceCamDistance: 'FaceCam distance',
|
||||
cameraSave: 'Save camera',
|
||||
cameraFaceCamExperimental: 'FaceCam (experimental)',
|
||||
cameraDistanceError: 'FaceCam distance must be between 0.5 and 5',
|
||||
cameraSaved: 'Camera saved',
|
||||
lcPassiveFallback: 'Increases basic stats following the <strong>{path}</strong> path and boosts damage and support performance by <strong>{value}%</strong> in battle',
|
||||
slotHeadShort: 'Head',
|
||||
slotHandsShort: 'Hands',
|
||||
slotBodyShort: 'Body',
|
||||
slotFeetShort: 'Feet',
|
||||
slotSphereShort: 'Planar Sphere',
|
||||
slotRopeShort: 'Link Rope',
|
||||
traceFallbackA2: 'Trace A2',
|
||||
traceFallbackA4: 'Trace A4',
|
||||
traceFallbackA6: 'Trace A6',
|
||||
printSavePdf: 'Print / save as PDF or image',
|
||||
printClose: 'Close window',
|
||||
exRankNoData: 'No Ex-Rank data',
|
||||
exRankEnabled: 'Enabled',
|
||||
exRankDisabled: 'Disabled',
|
||||
jsonStatusLoading: 'Loading {file}...',
|
||||
jsonStatusMemory: '{file} (from memory/cache)',
|
||||
jsonStatusServer: '{file} (from server)',
|
||||
jsonStatusBrowserCache: '{file} (from browser cache)',
|
||||
jsonStatusCache: '{file} (from cache)',
|
||||
jsonStatusDump: '{file} (Firefly dump structure)',
|
||||
jsonOfflineNoCache: 'Offline: no cache. You can upload or paste JSON.',
|
||||
jsonOfflinePaste: 'Offline: you can paste or type JSON.',
|
||||
jsonCacheUnavailable: 'Unavailable',
|
||||
copyCardName: 'Name',
|
||||
copyCardPath: 'Path',
|
||||
copyCardElement: 'Element',
|
||||
copyCardLevel: 'Level',
|
||||
copyCardEidolon: 'Eidolon',
|
||||
copyCardLightCone: 'Light Cone',
|
||||
copyCardGenerated: 'Generated via GaC-Srtool'
|
||||
}
|
||||
};
|
||||
for (const [language, additions] of Object.entries(MODAL_AND_TOOL_TRANSLATIONS)) {
|
||||
if (TRANSLATIONS[language]) Object.assign(TRANSLATIONS[language], additions);
|
||||
}
|
||||
|
||||
let currentLang = 'th';
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
currentLang = localStorage.getItem('gac_srtool_lang') || 'th';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.8';
|
||||
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.16';
|
||||
|
||||
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.8';
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.16';
|
||||
|
||||
const strings = {
|
||||
en: {
|
||||
|
||||
Reference in new issue
Block a user