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:
CAtlas0784 committed 2026-11-01 14:49:56 +07:00
1 parent d9b7a4ce1b
commit 76986eed4a
9 files changed
+775 -187

No files matched your search

+68 -8
View File
@@ -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
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.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>
+375 -155
View File
File diff suppressed because it is too large. Load diff
+20 -3
View File
@@ -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;
}
+88 -4
View File
@@ -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,
};
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage } from './i18n.js?v=2.8';
import { getCurrentLanguage } from './i18n.js?v=2.16';
const strings = {
en: {