(function () { const BASE = '/Game'; const STORAGE_KEY = 'gameCharacterId'; /** Main-Lobby ใช้รูป composite idle ทิศ down แทน _down.png เมื่อมี */ const LOBBY_IDLE_DOWN_LS = 'jdCharLobbyIdleDown:'; function persistLobbyIdleDownPreview(characterId, dataUrl) { const id = String(characterId || '').trim(); if (!id) return; try { if (dataUrl && typeof dataUrl === 'string' && dataUrl.indexOf('data:image/') === 0) { localStorage.setItem(LOBBY_IDLE_DOWN_LS + id, dataUrl); } } catch (e) { console.warn('Lobby idle preview cache', e); } } function clearLobbyIdleDownPreview(characterId) { const id = String(characterId || '').trim(); if (!id) return; try { localStorage.removeItem(LOBBY_IDLE_DOWN_LS + id); } catch (e) { /* ignore */ } } function charIdForLobbyIdlePersist() { try { const sel = (localStorage.getItem(STORAGE_KEY) || '').trim(); if (sel) return sel; } catch (e) { /* ignore */ } const el = document.getElementById('char-name'); return el ? String(el.value || '').trim() : ''; } const grid = document.getElementById('character-grid'); const status = document.getElementById('char-status'); const hint = document.getElementById('char-hint'); const uploadStatus = document.getElementById('upload-status'); const dirs = ['up', 'down', 'left', 'right']; // ลำดับเลเยอร์จากหลังมาหน้า: shadow → body → head → hair → face (เงาอยู่ล่างสุด) const layers = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; const DEFAULT_SHADOW_URL = BASE + '/img/default-shadow-'; const fileInputs = {}; const previews = {}; const idleLayerFileInputs = {}; const idleLayerPreviews = {}; const layerFileInputs = {}; const layerPreviews = {}; dirs.forEach(d => { fileInputs[d] = document.getElementById('file-' + d); previews[d] = document.getElementById('preview-' + d); idleLayerFileInputs[d] = {}; layers.forEach(layer => { const el = document.getElementById('layer-idle-' + d + '-' + layer); if (el) idleLayerFileInputs[d][layer] = el; }); idleLayerPreviews[d] = document.getElementById('preview-idle-layer-' + d); layerFileInputs[d] = {}; layers.forEach(layer => { const inputId = 'layer-' + d + '-' + layer; const el = document.getElementById(inputId); if (el) layerFileInputs[d][layer] = el; }); layerPreviews[d] = document.getElementById('preview-' + d + '-layer'); }); /** จาก GET /api/characters/:id/layer-manifest — ใช้ตอนกดแก้ไขให้ preview / รวมเลเยอร์ก่อนอัปโหลดซ้ำ */ let editLayerManifest = null; function clearLayerThumbStrips() { document.querySelectorAll('.char-layer-seq-strip').forEach((el) => el.remove()); } function manifestFrameAt(dir, frameIndex) { const e = editLayerManifest && editLayerManifest.byDir && editLayerManifest.byDir[dir]; if (!e || !e.frames || !e.frames[frameIndex]) return null; return e.frames[frameIndex]; } function manifestIdleFrameAt(dir, frameIndex) { const e = editLayerManifest && editLayerManifest.byDirIdle && editLayerManifest.byDirIdle[dir]; if (!e || !e.frames || !e.frames[frameIndex]) return null; return e.frames[frameIndex]; } function characterLayerFileUrl(fname) { if (!fname || typeof fname !== 'string') return ''; return BASE + '/img/characters/' + encodeURIComponent(fname); } function fetchUrlAsDataUrl(fullUrl) { return fetch(fullUrl, { credentials: 'same-origin' }) .then((r) => { if (!r.ok) throw new Error('http'); return r.blob(); }) .then((blob) => new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = () => reject(new Error('read blob')); fr.readAsDataURL(blob); })); } function renderLayerThumbStripsFromManifest(charId) { clearLayerThumbStrips(); if (!editLayerManifest || (!editLayerManifest.byDir && !editLayerManifest.byDirIdle)) return; dirs.forEach((d) => { const entry = editLayerManifest.byDir && editLayerManifest.byDir[d]; if (!entry || !entry.frames || !entry.frames.length) return; layers.forEach((layer) => { const input = layerFileInputs[d] && layerFileInputs[d][layer]; if (!input || !input.parentElement) return; const strip = document.createElement('div'); strip.className = 'char-layer-seq-strip'; strip.setAttribute('data-strip-for', d + '-' + layer); const maxF = entry.frames.length; for (let fi = 0; fi < maxF; fi++) { const fr = entry.frames[fi]; const fname = fr && fr[layer]; if (!fname) continue; const im = document.createElement('img'); im.src = characterLayerFileUrl(fname) + '?v=' + Date.now(); im.alt = charId + ' ' + d + ' ' + layer + ' f' + fi; im.title = 'เฟรม ' + (fi + 1); im.onerror = function () { this.style.opacity = '0.25'; }; strip.appendChild(im); } if (strip.childNodes.length) input.insertAdjacentElement('afterend', strip); }); }); dirs.forEach((d) => { const entry = editLayerManifest.byDirIdle && editLayerManifest.byDirIdle[d]; if (!entry || !entry.frames || !entry.frames.length) return; layers.forEach((layer) => { const input = idleLayerFileInputs[d] && idleLayerFileInputs[d][layer]; if (!input || !input.parentElement) return; const strip = document.createElement('div'); strip.className = 'char-layer-seq-strip'; strip.setAttribute('data-strip-for', d + '-idle-' + layer); const maxF = entry.frames.length; for (let fi = 0; fi < maxF; fi++) { const fr = entry.frames[fi]; const fname = fr && fr[layer]; if (!fname) continue; const im = document.createElement('img'); im.src = characterLayerFileUrl(fname) + '?v=' + Date.now(); im.alt = charId + ' ' + d + ' idle ' + layer + ' f' + fi; im.title = 'idle เฟรม ' + (fi + 1); im.onerror = function () { this.style.opacity = '0.25'; }; strip.appendChild(im); } if (strip.childNodes.length) input.insertAdjacentElement('afterend', strip); }); }); } function getSelected() { try { return localStorage.getItem(STORAGE_KEY) || ''; } catch (e) { return ''; } } function setSelected(id) { try { if (id) localStorage.setItem(STORAGE_KEY, id); else localStorage.removeItem(STORAGE_KEY); } catch (e) {} } dirs.forEach(d => { const input = fileInputs[d]; const prev = previews[d]; if (input && prev) { input.addEventListener('change', () => { const files = input.files ? Array.from(input.files) : []; const f = files[0]; if (!f) { prev.src = ''; prev.title = ''; return; } const r = new FileReader(); r.onload = () => { prev.src = r.result; prev.title = files.length > 1 ? (files.length + ' เฟรม') : ''; }; r.readAsDataURL(f); }); } }); function readImageFromFile(file) { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('load image fail')); img.src = r.result; }; r.onerror = () => reject(new Error('read file fail')); r.readAsDataURL(file); }); } function readFileAsDataUrl(file) { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result); r.onerror = () => reject(new Error('read file fail')); r.readAsDataURL(file); }); } /** ส่งเลเยอร์ดิบต่อเฟรมให้เซิร์ฟเวอร์บันทึก — หน้า play จะย้อมสีแยก body / hair / head ได้ */ async function collectLayerFramesForDirection(dir, maxFrames) { const inputs = layerFileInputs[dir]; if (!inputs || maxFrames < 1) return null; const frames = []; for (let i = 0; i < maxFrames; i++) { const layerObj = {}; const mf = manifestFrameAt(dir, i); for (const layer of layers) { const input = inputs[layer]; const files = input && input.files ? Array.from(input.files) : []; if (files.length) { const file = files[Math.min(i, files.length - 1)]; try { layerObj[layer] = await readFileAsDataUrl(file); } catch (e) { /* skip */ } continue; } const fname = mf && mf[layer]; if (fname) { try { layerObj[layer] = await fetchUrlAsDataUrl(characterLayerFileUrl(fname)); } catch (e) { /* skip */ } } } frames.push(layerObj); } return frames; } function loadImageFromUrl(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = () => reject(new Error('load url fail')); img.src = url; }); } async function composeLayeredFrame(dir, frameIndex) { const inputs = layerFileInputs[dir] || {}; const mf = manifestFrameAt(dir, frameIndex); const imagePromises = []; layers.forEach(layer => { const input = inputs[layer]; const files = input && input.files ? Array.from(input.files) : []; const hasFile = files.length > 0; const file = hasFile ? files[Math.min(frameIndex, files.length - 1)] : null; const fname = mf && mf[layer] ? mf[layer] : null; const urlFromDisk = fname ? characterLayerFileUrl(fname) : ''; if (layer === 'shadow') { if (file) { imagePromises.push(readImageFromFile(file)); } else if (urlFromDisk) { imagePromises.push(loadImageFromUrl(urlFromDisk).catch(() => null)); } else { imagePromises.push(loadImageFromUrl(DEFAULT_SHADOW_URL + dir + '.png').catch(() => null)); } } else { if (file) { imagePromises.push(readImageFromFile(file).catch(() => null)); } else if (urlFromDisk) { imagePromises.push(loadImageFromUrl(urlFromDisk).catch(() => null)); } else { imagePromises.push(Promise.resolve(null)); } } }); const resolved = await Promise.all(imagePromises); const images = resolved.filter(Boolean); if (!images.length) return null; const base = images[0]; const w = base.width || 64; const h = base.height || 64; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); resolved.forEach(img => { if (img) ctx.drawImage(img, 0, 0, w, h); }); return canvas.toDataURL('image/png'); } async function collectIdleLayerFramesForDirection(dir, maxFrames) { const inputs = idleLayerFileInputs[dir]; if (!inputs || maxFrames < 1) return null; const frames = []; for (let i = 0; i < maxFrames; i++) { const layerObj = {}; const mf = manifestIdleFrameAt(dir, i); for (const layer of layers) { const input = inputs[layer]; const files = input && input.files ? Array.from(input.files) : []; if (files.length) { const file = files[Math.min(i, files.length - 1)]; try { layerObj[layer] = await readFileAsDataUrl(file); } catch (e) { /* skip */ } continue; } const fname = mf && mf[layer]; if (fname) { try { layerObj[layer] = await fetchUrlAsDataUrl(characterLayerFileUrl(fname)); } catch (e) { /* skip */ } } } frames.push(layerObj); } return frames; } async function composeLayeredIdleFrame(dir, frameIndex) { const inputs = idleLayerFileInputs[dir] || {}; const mf = manifestIdleFrameAt(dir, frameIndex); const imagePromises = []; layers.forEach(layer => { const input = inputs[layer]; const files = input && input.files ? Array.from(input.files) : []; const hasFile = files.length > 0; const file = hasFile ? files[Math.min(frameIndex, files.length - 1)] : null; const fname = mf && mf[layer] ? mf[layer] : null; const urlFromDisk = fname ? characterLayerFileUrl(fname) : ''; if (layer === 'shadow') { if (file) { imagePromises.push(readImageFromFile(file)); } else if (urlFromDisk) { imagePromises.push(loadImageFromUrl(urlFromDisk).catch(() => null)); } else { imagePromises.push(loadImageFromUrl(DEFAULT_SHADOW_URL + dir + '.png').catch(() => null)); } } else { if (file) { imagePromises.push(readImageFromFile(file).catch(() => null)); } else if (urlFromDisk) { imagePromises.push(loadImageFromUrl(urlFromDisk).catch(() => null)); } else { imagePromises.push(Promise.resolve(null)); } } }); const resolved = await Promise.all(imagePromises); const images = resolved.filter(Boolean); if (!images.length) return null; const base = images[0]; const w = base.width || 64; const h = base.height || 64; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); resolved.forEach(img => { if (img) ctx.drawImage(img, 0, 0, w, h); }); return canvas.toDataURL('image/png'); } function refreshIdleLayerPreviewForDir(dir) { const prev = idleLayerPreviews[dir]; if (!prev) return; composeLayeredIdleFrame(dir, 0).then(dataUrl => { if (dataUrl) { prev.src = dataUrl; prev.title = 'ตัวอย่าง idle เฟรมที่ 1'; if (dir === 'down') { persistLobbyIdleDownPreview(charIdForLobbyIdlePersist(), dataUrl); } } else { prev.src = ''; prev.title = ''; } }).catch(() => { prev.src = ''; prev.title = ''; }); } function refreshLayerPreviewForDir(dir) { const prev = layerPreviews[dir]; if (!prev) return; // ใช้เฟรมแรกเป็นตัวอย่าง composeLayeredFrame(dir, 0).then(dataUrl => { if (dataUrl) { prev.src = dataUrl; prev.title = 'ตัวอย่างเลเยอร์เฟรมที่ 1'; } else { prev.src = ''; prev.title = ''; } }).catch(() => { prev.src = ''; prev.title = ''; }); } // เมื่อมีการเปลี่ยนไฟล์เลเยอร์ใด ๆ ให้รีเฟรช preview ของทิศนั้น dirs.forEach(d => { const inputs = layerFileInputs[d]; if (!inputs) return; layers.forEach(layer => { const input = inputs[layer]; if (!input) return; input.addEventListener('change', () => { refreshLayerPreviewForDir(d); }); }); }); dirs.forEach(d => { const inputs = idleLayerFileInputs[d]; if (!inputs) return; layers.forEach(layer => { const input = inputs[layer]; if (!input) return; input.addEventListener('change', () => { refreshIdleLayerPreviewForDir(d); }); }); }); function refreshList() { if (status) status.textContent = 'โหลดรายการตัวละคร...'; fetch(BASE + '/api/characters') .then(r => r.json()) .then(list => { if (!Array.isArray(list)) throw new Error('ไม่ใช่ array'); if (status) status.textContent = ''; if (hint) hint.style.display = list.length === 0 ? 'block' : 'none'; const selected = getSelected(); if (!grid) return; grid.innerHTML = ''; list.forEach(char => { const div = document.createElement('div'); div.className = 'character-item' + (selected === char.id ? ' selected' : ''); div.setAttribute('data-id', char.id); const previewsDiv = document.createElement('div'); previewsDiv.className = 'previews'; dirs.forEach(d => { const img = document.createElement('img'); img.src = BASE + '/img/characters/' + encodeURIComponent(char.id) + '_' + d + '.png'; img.alt = d; img.onerror = () => { img.style.opacity = '0.4'; }; previewsDiv.appendChild(img); }); const name = document.createElement('div'); name.className = 'name'; name.textContent = char.name || char.id; const actions = document.createElement('div'); actions.className = 'character-actions'; const btnEdit = document.createElement('button'); btnEdit.type = 'button'; btnEdit.textContent = 'แก้ไข'; btnEdit.className = 'char-edit-btn'; btnEdit.addEventListener('click', (ev) => { ev.stopPropagation(); if (charName) charName.value = char.id; const ts = '?v=' + Date.now(); dirs.forEach(d => { const prev = previews[d]; if (!prev) return; const url = BASE + '/img/characters/' + encodeURIComponent(char.id) + '_' + d + '.png' + ts; prev.onerror = function () { prev.src = 'data:image/svg+xml,' + encodeURIComponent('ไม่มี'); prev.onerror = null; }; prev.src = url; refreshIdleLayerPreviewForDir(d); }); editLayerManifest = null; clearLayerThumbStrips(); const adv = document.getElementById('char-upload-advanced'); const scrollTarget = char.hasLayerFiles && adv ? adv : document.querySelector('.char-upload-simple'); if (char.hasLayerFiles) { fetch(BASE + '/api/characters/' + encodeURIComponent(char.id) + '/layer-manifest') .then((r) => r.json()) .then((man) => { if (man && man.ok && (man.byDir || man.byDirIdle)) { editLayerManifest = man; if (!editLayerManifest.byDir) editLayerManifest.byDir = {}; if (!editLayerManifest.byDirIdle) editLayerManifest.byDirIdle = {}; renderLayerThumbStripsFromManifest(char.id); dirs.forEach((d) => { refreshLayerPreviewForDir(d); refreshIdleLayerPreviewForDir(d); }); } else { dirs.forEach((d) => { const lp = layerPreviews[d]; if (lp) { lp.src = BASE + '/img/characters/' + encodeURIComponent(char.id) + '_' + d + '.png' + ts; lp.onerror = function () { lp.src = ''; lp.onerror = null; }; } }); } if (uploadStatus) { uploadStatus.textContent = 'แก้ไข "' + (char.name || char.id) + '" — ดูลำดับ PNG ต่อเลเยอร์ด้านล่าง · เลือกไฟล์ใหม่แล้วกดอัปโหลดเพื่ออัปเดต'; } if (scrollTarget) scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); }) .catch(() => { if (uploadStatus) uploadStatus.textContent = 'โหลดรายการเลเยอร์ไม่ได้ — แสดงเฉพาะรูปรวมด้านบน'; if (scrollTarget) scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } else { dirs.forEach((d) => { const lp = layerPreviews[d]; if (lp) { lp.src = BASE + '/img/characters/' + encodeURIComponent(char.id) + '_' + d + '.png' + ts; lp.onerror = function () { lp.src = ''; lp.onerror = null; }; } }); if (uploadStatus) uploadStatus.textContent = 'เลือกรูปใหม่เพื่ออัปเดต "' + (char.name || char.id) + '" แล้วกดอัปโหลด'; if (scrollTarget) scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); const btnDelete = document.createElement('button'); btnDelete.type = 'button'; btnDelete.textContent = 'ลบ'; btnDelete.className = 'char-delete-btn'; btnDelete.addEventListener('click', (ev) => { ev.stopPropagation(); const ok = window.confirm('ต้องการลบตัวละคร "' + (char.name || char.id) + '" จริงหรือไม่?'); if (!ok) return; if (uploadStatus) uploadStatus.textContent = 'กำลังลบตัวละคร...'; fetch(BASE + '/api/characters/' + encodeURIComponent(char.id), { method: 'DELETE' }) .then(async (r) => { const text = await r.text(); let data; try { data = JSON.parse(text); } catch (e) { data = { ok: false, error: text || r.status }; } if (!r.ok || !data.ok) { if (uploadStatus) uploadStatus.textContent = data.error || ('ลบไม่สำเร็จ (' + r.status + ')'); return; } if (uploadStatus) uploadStatus.textContent = 'ลบตัวละครแล้ว'; const current = getSelected(); if (current === char.id) { setSelected(''); } clearLobbyIdleDownPreview(char.id); refreshList(); }) .catch((err) => { console.error('Delete character failed', err); if (uploadStatus) uploadStatus.textContent = 'ลบไม่สำเร็จ: ' + (err.message || ''); }); }); actions.appendChild(btnEdit); actions.appendChild(btnDelete); div.appendChild(previewsDiv); div.appendChild(name); div.appendChild(actions); div.addEventListener('click', () => { setSelected(char.id); grid.querySelectorAll('.character-item').forEach(el => el.classList.remove('selected')); div.classList.add('selected'); }); grid.appendChild(div); }); }) .catch(() => { if (status) status.textContent = 'โหลดรายการตัวละครไม่ได้'; if (hint) hint.style.display = 'block'; }); } refreshList(); const btnUpload = document.getElementById('btn-upload'); const charName = document.getElementById('char-name'); const btnUploadLayered = document.getElementById('btn-upload-layered'); function uploadCharacterPayload(payload) { // ถ้าทิศไหนมีแค่ 1 เฟรม ให้ส่งเป็น string เพื่อให้เข้ากับ server แบบเก่า dirs.forEach(d => { if (Array.isArray(payload[d]) && payload[d].length === 1) { payload[d] = payload[d][0]; } if (Array.isArray(payload[d]) && payload[d].length === 0) { delete payload[d]; } }); if (payload.idle && typeof payload.idle === 'object') { dirs.forEach((d) => { const v = payload.idle[d]; if (Array.isArray(v) && v.length === 1) payload.idle[d] = v[0]; if (Array.isArray(v) && v.length === 0) delete payload.idle[d]; }); if (!Object.keys(payload.idle).length) delete payload.idle; } if (uploadStatus) uploadStatus.textContent = 'กำลังอัปโหลด...'; return fetch(BASE + '/api/characters/upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(async (r) => { const text = await r.text(); let data; try { data = JSON.parse(text); } catch (e) { data = { ok: false, error: text || r.status }; } if (!r.ok) { console.error('Upload error', r.status, data.error || text); if (uploadStatus) uploadStatus.textContent = data.error || 'ข้อผิดพลาด ' + r.status; return; } if (data.ok) { const savedId = data.characterId || ''; const idleDownImg = idleLayerPreviews.down; const idleDownDataUrl = idleDownImg && idleDownImg.src && String(idleDownImg.src).indexOf('data:image/') === 0 ? idleDownImg.src : ''; if (savedId && idleDownDataUrl) { persistLobbyIdleDownPreview(savedId, idleDownDataUrl); } if (uploadStatus) uploadStatus.textContent = 'อัปโหลดสำเร็จ: ' + savedId; editLayerManifest = null; clearLayerThumbStrips(); dirs.forEach(d => { if (fileInputs[d]) fileInputs[d].value = ''; if (previews[d]) previews[d].src = ''; const inputs = layerFileInputs[d]; if (inputs) { layers.forEach(layer => { if (inputs[layer]) inputs[layer].value = ''; }); } const idleIn = idleLayerFileInputs[d]; if (idleIn) { layers.forEach(layer => { if (idleIn[layer]) idleIn[layer].value = ''; }); } if (idleLayerPreviews[d]) { idleLayerPreviews[d].src = ''; idleLayerPreviews[d].title = ''; } if (layerPreviews[d]) layerPreviews[d].src = ''; }); if (charName) charName.value = savedId; refreshList(); if (savedId) { fetch(BASE + '/api/characters/' + encodeURIComponent(savedId) + '/layer-manifest') .then((r) => r.json()) .then((man) => { if (man && man.ok && (man.byDir || man.byDirIdle)) { editLayerManifest = man; if (!editLayerManifest.byDir) editLayerManifest.byDir = {}; if (!editLayerManifest.byDirIdle) editLayerManifest.byDirIdle = {}; renderLayerThumbStripsFromManifest(savedId); dirs.forEach((d) => { refreshLayerPreviewForDir(d); refreshIdleLayerPreviewForDir(d); }); } }) .catch(() => {}); } } else { if (uploadStatus) uploadStatus.textContent = data.error || 'อัปโหลดไม่สำเร็จ'; } }) .catch((err) => { console.error('Upload failed', err); if (uploadStatus) uploadStatus.textContent = 'อัปโหลดไม่สำเร็จ: ' + (err.message || ''); }); } if (btnUpload) { btnUpload.addEventListener('click', () => { const payload = { name: (charName && charName.value || '').trim() || getSelected() || '' }; let hasWalk = false; const readers = []; dirs.forEach(d => { const input = fileInputs[d]; const files = input && input.files ? Array.from(input.files) : []; if (!files.length) return; hasWalk = true; payload[d] = []; files.forEach((f, idx) => { readers.push(new Promise(resolve => { const r = new FileReader(); r.onload = () => { payload[d][idx] = r.result; resolve(); }; r.readAsDataURL(f); })); }); }); if (!hasWalk) { if (uploadStatus) uploadStatus.textContent = 'กรุณาเลือกรูปเดินอย่างน้อย 1 ทิศ (idle เป็นทางเลือก)'; return; } Promise.all(readers).then(() => { uploadCharacterPayload(payload); }); }); } if (btnUploadLayered) { btnUploadLayered.addEventListener('click', () => { const payload = { name: (charName && charName.value || '').trim() || getSelected() || '' }; let hasAny = false; const tasks = []; const layerCollectTasks = []; dirs.forEach(d => { const inputs = layerFileInputs[d]; if (!inputs) return; let maxFrames = 0; layers.forEach(layer => { const input = inputs[layer]; const len = input && input.files ? input.files.length : 0; if (len > maxFrames) maxFrames = len; }); const manEnt = editLayerManifest && editLayerManifest.byDir && editLayerManifest.byDir[d]; if (manEnt && Array.isArray(manEnt.frames) && manEnt.frames.length > maxFrames) { maxFrames = manEnt.frames.length; } if (maxFrames === 0) return; hasAny = true; payload[d] = []; layerCollectTasks.push( collectLayerFramesForDirection(d, maxFrames).then((lf) => { if (lf && lf.some(fr => fr && Object.keys(fr).length > 0)) { payload.layerFrames = payload.layerFrames || {}; payload.layerFrames[d] = lf; } }) ); for (let i = 0; i < maxFrames; i++) { tasks.push( composeLayeredFrame(d, i).then(dataUrl => { if (dataUrl) { payload[d][i] = dataUrl; } }) ); } }); if (!hasAny) { if (uploadStatus) uploadStatus.textContent = 'กรุณาเลือกรูปเลเยอร์อย่างน้อย 1 ทิศ'; return; } if (uploadStatus) uploadStatus.textContent = 'กำลังรวมเลเยอร์และอัปโหลด...'; const idleLayerCollectTasks = []; const idleTasks = []; dirs.forEach(d => { const inputs = idleLayerFileInputs[d]; if (!inputs) return; let maxIdle = 0; layers.forEach(layer => { const input = inputs[layer]; const len = input && input.files ? input.files.length : 0; if (len > maxIdle) maxIdle = len; }); const manEntIdle = editLayerManifest && editLayerManifest.byDirIdle && editLayerManifest.byDirIdle[d]; if (manEntIdle && Array.isArray(manEntIdle.frames) && manEntIdle.frames.length > maxIdle) { maxIdle = manEntIdle.frames.length; } if (maxIdle === 0) return; payload.idle = payload.idle || {}; payload.idle[d] = []; idleLayerCollectTasks.push( collectIdleLayerFramesForDirection(d, maxIdle).then((lf) => { if (lf && lf.some(fr => fr && Object.keys(fr).length > 0)) { payload.layerFramesIdle = payload.layerFramesIdle || {}; payload.layerFramesIdle[d] = lf; } }) ); for (let i = 0; i < maxIdle; i++) { idleTasks.push( composeLayeredIdleFrame(d, i).then(dataUrl => { if (dataUrl) payload.idle[d][i] = dataUrl; }) ); } }); Promise.all([...tasks, ...layerCollectTasks, ...idleTasks, ...idleLayerCollectTasks]).then(() => { uploadCharacterPayload(payload); }); }); } (function wireAdvancedLayerModeTabs() { const adv = document.getElementById('char-upload-advanced'); const bar = document.getElementById('char-layer-global-tabs'); if (!adv || !bar) return; function setMode(mode) { const m = mode === 'idle' ? 'idle' : 'move'; adv.setAttribute('data-adv-layer-mode', m); bar.querySelectorAll('.char-mode-tab').forEach((btn) => { const on = btn.getAttribute('data-adv-mode') === m; btn.classList.toggle('is-active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); }); } bar.addEventListener('click', (ev) => { const t = ev.target.closest('[data-adv-mode]'); if (!t) return; const mode = t.getAttribute('data-adv-mode'); if (mode === 'move' || mode === 'idle') setMode(mode); }); setMode(adv.getAttribute('data-adv-layer-mode') || 'move'); })(); })();