Files
Gacy-Srtool/js/command-assistant.mjs
T
CAtlas0784 76986eed4a 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.
2026-11-01 14:49:56 +07:00

154 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 });
export const COMMANDS = [
spec('/help', 'แสดงรายการคำสั่งและวิธีใช้'),
spec('/heal', 'ส่งคำสั่งฟื้นฟู HP, พลังงาน และแต้มสกิล'),
spec('/max', 'ปรับตัวละครทั้งหมดเป็นเลเวล 80 และ E6'),
spec('/unstuck', 'กู้หน้าจอที่หมุนค้าง โดยเก็บความคืบหน้า CW'),
spec('/clear lineup', 'ล้างสมาชิกทีมปัจจุบัน'),
spec('/avatar', 'ปรับเลเวลและ Eidolon ของตัวละคร', [arg('id', 'เลือก ID ตัวละครจากรายชื่อ', 'avatars'), arg('level', 'เลเวล 1–80 (ไม่ใส่เพื่อคงค่าเดิม)', ['1','20','40','60','70','80'], true), arg('rank', 'Eidolon 0–6', ['0','1','2','3','4','5','6'], true)]),
...['/give','/item'].map(p => spec(p, 'ส่งคำสั่งเพิ่มไอเท็ม', [arg('item_id', 'ID ไอเท็มจาก catalog เซิร์ฟเวอร์', 'items'), quantity()])),
...['/teleport','/scene'].map(p => spec(p, 'เปลี่ยนฉากปลายทาง', [arg('plane_id', 'เลือก Plane ID', 'planes'), arg('floor_id', 'เลือกชั้นที่อยู่ใน Plane นี้', 'floors', true)])),
spec('/faketime', 'ดู/ตั้งเวลาเกม โดยไม่เปลี่ยนเวลา Windows', [arg('time', 'status = ดูเวลา · off = เวลาจริง · หรือ Unix seconds / RFC3339 พร้อม timezone', 'time', true)]),
spec('/cw help', 'แสดงวิธีใช้คำสั่ง CW'), spec('/cw status', 'ดูเงิน HP เลเวล และสถานะรอบ CW'),
spec('/cw list', 'แสดงรายการ ID ของ CW', [arg('category', 'เลือกชนิดข้อมูล', ['role','equip','orb','consumable','forge'])]),
...['gold','hp','exp'].map(p => spec('/cw '+p, p === 'exp' ? 'เพิ่ม EXP ในรอบ CW' : 'ตั้งค่า '+p+' ในรอบ CW', [arg(p === 'exp' ? 'gain' : 'total', p === 'hp' ? 'HP 1 ถึงเพดานของรอบปัจจุบัน' : 'จำนวนเต็มไม่ติดลบ (เป็นยอดรวม ยกเว้น exp ที่เป็นจำนวนเพิ่ม)', ['1','10','100','1000'])])),
...['tbpower','tb','trailblaze'].map(p => spec('/cw '+p, 'ตั้งค่าพลังบุกเบิก (Trailblaze Power / เหรียญร้านรถไฟ) ในรอบ CW', [arg('total', 'จำนวนพลังบุกเบิก เช่น 10, 50, 100', ['10','50','100','500'])])),
spec('/cw role', 'เพิ่มตัวละคร CW (ใช้ได้ใน Preparation)', [arg('roleID', 'เลือก Role ID ของ CW', 'cw_roles'), arg('star', 'ดาวที่มีข้อมูลรองรับสำหรับ Role นี้', 'cw_stars'), quantity()]),
...['equip','orb','consumable','forge'].map(p => spec('/cw '+p, 'เพิ่ม '+p+' ในรอบ CW (Preparation)', [arg('itemID', 'เลือก ID จากข้อมูล CW ของ Client', 'cw_'+p), quantity()])),
];
export function usage(command) {
return [...command.path, ...command.args.map(a => a.optional ? '['+a.name+']' : '<'+a.name+'>')].join(' ');
}
export function inspectCommand(value = '', caret = value.length, catalogs = {}) {
caret = Math.max(0, Math.min(caret, value.length));
const tokens = Array.from(value.matchAll(/\S+/g), m => ({ value:m[0], start:m.index, end:m.index+m[0].length }));
let index = tokens.findIndex(t => caret >= t.start && caret <= t.end);
if (index < 0) index = tokens.filter(t => t.end < caret).length;
const token = tokens[index];
const start = token?.start ?? caret, end = token?.end ?? caret;
const query = value.slice(start, caret).toLowerCase();
const words = tokens.map(t => t.value);
const matching = COMMANDS.filter(c => c.path.slice(0, Math.min(index, c.path.length)).every((p,i) => p.toLowerCase() === words[i]?.toLowerCase()));
const exact = COMMANDS.find(c => c.path.every((p,i) => p.toLowerCase() === words[i]?.toLowerCase()));
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:txt(c.description) });
}
const argIndex = exact ? index-exact.path.length : -1;
const active = exact?.args[argIndex];
if (active) {
let choices = Array.isArray(active.source) ? active.source : catalogs[active.source] || [];
if (active.source === 'time') choices = ['status','off','reset','real',new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')];
if (active.source === 'floors') choices = (catalogs.floors || []).filter(c => String(c.plane) === words[exact.path.length]);
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: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() ? 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,
};
}
export function completeCommand(value, model, choice) {
const suffix = value.slice(model.end);
const separator = /^\s/.test(suffix) ? '' : ' ';
return { value:value.slice(0,model.start)+choice.value+separator+suffix, caret:model.start+choice.value.length+1 };
}