Files
justice/www/html/Game/public/character.js
T

771 lines
31 KiB
JavaScript

(function () {
const BASE = '/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 = 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';
} 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('<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56"><rect fill="%2324283b" width="56" height="56"/><text x="28" y="30" fill="%23a9b1d6" font-size="10" text-anchor="middle">ไม่มี</text></svg>'); 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('');
}
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 || '';
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');
})();
})();