(function () { const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game'; const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game'; const STORAGE_KEY = 'gameCharacterId'; 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 = SERVER + '/img/default-shadow-'; const fileInputs = {}; const previews = {}; const layerFileInputs = {}; const layerPreviews = {}; dirs.forEach(d => { fileInputs[d] = document.getElementById('file-' + d); previews[d] = document.getElementById('preview-' + 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'); }); 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 = {}; for (const layer of layers) { const input = inputs[layer]; const files = input && input.files ? Array.from(input.files) : []; if (!files.length) continue; const file = files[Math.min(i, files.length - 1)]; try { layerObj[layer] = await readFileAsDataUrl(file); } catch (e) { // ข้ามเลเยอร์นี้เฟรมนี้ } } 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 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; if (layer === 'shadow') { imagePromises.push(file ? readImageFromFile(file) : loadImageFromUrl(DEFAULT_SHADOW_URL + dir + '.png').catch(() => null)); } else { if (!file) { imagePromises.push(Promise.resolve(null)); return; } imagePromises.push(readImageFromFile(file).catch(() => 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 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); }); }); }); function refreshList() { if (status) status.textContent = 'โหลดรายการตัวละคร...'; fetch(SERVER + '/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 = SERVER + '/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; // แสดง preview รูปปัจจุบันในส่วนอัปโหลด const ts = '?v=' + Date.now(); dirs.forEach(d => { const prev = previews[d]; if (!prev) return; const url = SERVER + '/img/characters/' + encodeURIComponent(char.id) + '_' + d + '.png' + ts; prev.onerror = function () { prev.src = 'data:image/svg+xml,' + encodeURIComponent('ไม่มี'); prev.onerror = null; }; prev.src = url; }); if (uploadStatus) uploadStatus.textContent = 'เลือกรูปใหม่เพื่ออัปเดต "' + (char.name || char.id) + '" แล้วกดอัปโหลด'; const firstSection = document.querySelector('.char-upload-simple, .char-upload'); if (firstSection) firstSection.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(SERVER + '/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(''); } 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 (uploadStatus) uploadStatus.textContent = 'กำลังอัปโหลด...'; return fetch(SERVER + '/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) { if (uploadStatus) uploadStatus.textContent = 'อัปโหลดสำเร็จ: ' + (data.characterId || ''); 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 = ''; }); } if (layerPreviews[d]) layerPreviews[d].src = ''; }); if (charName) charName.value = ''; refreshList(); } 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() }; let hasAny = false; const readers = []; dirs.forEach(d => { const input = fileInputs[d]; const files = input && input.files ? Array.from(input.files) : []; if (!files.length) return; hasAny = 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 (!hasAny) { if (uploadStatus) uploadStatus.textContent = 'กรุณาเลือกรูปอย่างน้อย 1 ทิศ'; return; } Promise.all(readers).then(() => { uploadCharacterPayload(payload); }); }); } if (btnUploadLayered) { btnUploadLayered.addEventListener('click', () => { const payload = { name: (charName && charName.value || '').trim() }; 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; }); 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 = 'กำลังรวมเลเยอร์และอัปโหลด...'; Promise.all([...tasks, ...layerCollectTasks]).then(() => { uploadCharacterPayload(payload); }); }); } })();