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

+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,
};
}