Files
justice/www/html/Game/public/js/customize-popup.js
T
2026-06-20 18:13:43 +00:00

1318 lines
54 KiB
JavaScript

(function () {
'use strict';
var script = document.currentScript;
var assetBase = (script && script.getAttribute('data-customize-asset-base')) || 'img/03-5-Customize';
var triggerSelector = (script && script.getAttribute('data-customize-triggers')) || '#btn-customize,#btn-cloth';
var triggerSelectors = triggerSelector.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
var storageKey = 'customizePopupStateV1';
var appliedStorageKey = 'customizePopupAppliedV1';
function isRoomLobbyPage() {
return /room-lobby\.html$/i.test(String(window.location.pathname || ''));
}
var overlayId = isRoomLobbyPage() ? 'room-cz-overlay' : 'customize-popup-overlay';
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
var PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' };
/** ธีมสี 8 ช่อง — default ช่อง 8 = #fec1fe */
var THEME_SWATCH_HEX = [
'#d72520', '#ef8508', '#efe237', '#5bb443',
'#2585cb', '#3f4ead', '#b53fd6', '#fec1fe'
];
/** สีผิว 3 ช่อง — default ช่อง 1 = #eaa78a */
var SKIN_SWATCH_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1'];
var PLAY_TINT_HEAD = SKIN_SWATCH_HEX.slice();
var PLAY_TINT_HAIR = THEME_SWATCH_HEX.slice();
var PLAY_TINT_BODY = THEME_SWATCH_HEX.slice();
var DEFAULT_ACTIVE_COLOR = 8;
var DEFAULT_ACTIVE_SKIN = 1;
var previewImagePromiseCache = Object.create(null);
var previewCompositeCache = Object.create(null);
var previewLayerManifestPromiseCache = Object.create(null);
var previewRenderToken = 0;
function resolveAssetBase(raw) {
var base = String(raw || 'img/03-5-Customize').replace(/\/$/, '');
if (/^\/(Game|img)\//i.test(base)) {
return typeof window.appPath === 'function' ? window.appPath(base) : base;
}
try {
return new URL(base + '/', window.location.href).pathname.replace(/\/$/, '');
} catch (e) {
return base;
}
}
var resolvedAssetBase = resolveAssetBase(assetBase);
function asset(name) {
return resolvedAssetBase + '/' + name;
}
function qs(sel) { return document.querySelector(sel); }
function readInitialState() {
var state = {
activeTab: 1,
activeColor: DEFAULT_ACTIVE_COLOR,
activeSkin: DEFAULT_ACTIVE_SKIN,
activeItem: 1,
coins: '150'
};
try {
var raw = localStorage.getItem(storageKey);
if (raw) {
var parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
if (parsed.activeTab >= 1 && parsed.activeTab <= 3) state.activeTab = parsed.activeTab;
if (parsed.activeColor >= 1 && parsed.activeColor <= 8) state.activeColor = parsed.activeColor;
if (parsed.activeSkin >= 1 && parsed.activeSkin <= 3) state.activeSkin = parsed.activeSkin;
if (parsed.activeItem >= 1 && parsed.activeItem <= 8) state.activeItem = parsed.activeItem;
}
}
var appliedRaw = localStorage.getItem(appliedStorageKey);
if (appliedRaw) {
var applied = JSON.parse(appliedRaw);
if (applied && typeof applied === 'object') {
if (applied.activeTab >= 1 && applied.activeTab <= 3) state.activeTab = applied.activeTab;
if (applied.activeColor >= 1 && applied.activeColor <= 8) state.activeColor = applied.activeColor;
if (applied.activeSkin >= 1 && applied.activeSkin <= 3) state.activeSkin = applied.activeSkin;
if (applied.activeItem >= 1 && applied.activeItem <= 8) state.activeItem = applied.activeItem;
}
}
var c = localStorage.getItem('jdCoins');
if (c != null && c !== '') state.coins = String(Math.max(0, parseInt(c, 10) || 0));
} catch (e) { /* ignore */ }
return state;
}
var state = readInitialState();
var refs = {
overlay: null,
dialog: null,
panelShell: null,
closeBtn: null,
confirmBtn: null,
previewFace: null,
nameEl: null,
coinVal: null,
colorRow: null,
skinRow: null,
tabRow: null,
itemsEl: null,
scrollEl: null,
scrollTrack: null,
scrollThumb: null
};
var scrollbarDrag = {
active: false,
pointerOffsetY: 0
};
function resetStateToConfirmed() {
var confirmed = readInitialState();
state.activeTab = confirmed.activeTab;
state.activeColor = confirmed.activeColor;
state.activeSkin = confirmed.activeSkin;
state.activeItem = confirmed.activeItem;
state.coins = confirmed.coins;
}
function persistConfirmedState() {
try {
localStorage.setItem(storageKey, JSON.stringify({
activeTab: clampIndex(state.activeTab, 1, 3),
activeColor: clampIndex(state.activeColor, 1, 8),
activeSkin: clampIndex(state.activeSkin, 1, 3),
activeItem: clampIndex(state.activeItem, 1, 8)
}));
} catch (e) { /* ignore */ }
}
function clampIndex(n, min, max) {
var v = parseInt(n, 10);
if (isNaN(v)) v = min;
return Math.max(min, Math.min(max, v));
}
function getStoredCharacterId() {
try {
return (localStorage.getItem('gameCharacterId') || '').trim();
} catch (e) {
return '';
}
}
/**
* หา characterId สำหรับ apply — ถ้า localStorage ว่าง ให้ดึงจาก API เหมือน lobby.js
* (กันเคสเครื่องที่ไม่เคยเซฟ gameCharacterId → กดยืนยันแล้วตัวใหญ่ไม่อัปเดต)
*/
var resolvedDefaultCharacterId = '';
function resolveApplyCharacterId(cb) {
var id = getStoredCharacterId();
if (id) { cb(id); return; }
if (resolvedDefaultCharacterId) { cb(resolvedDefaultCharacterId); return; }
try {
fetch(projectPath('/Game/api/characters'), { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) { cb(''); return; }
// เลือกตัวที่มี layer files ก่อน (composite สีได้จริง) ไม่งั้นใช้ตัวล่าสุด
var pick = null;
for (var i = 0; i < list.length; i += 1) {
if (list[i] && list[i].hasLayerFiles) { pick = list[i]; break; }
}
if (!pick) pick = list[list.length - 1];
resolvedDefaultCharacterId = pick && pick.id ? String(pick.id).trim() : '';
cb(resolvedDefaultCharacterId);
})
.catch(function () { cb(''); });
} catch (e) { cb(''); }
}
/** key เดียวกับ lobby.js (ensurePlayerKey) — ใช้ผูกสีกับบัญชี guest ฝั่ง server */
function getPlayerKey() {
try {
return (localStorage.getItem('jdPlayerKey') || '').trim();
} catch (e) {
return '';
}
}
/** บันทึกสีที่เลือกไปเซิร์ฟเวอร์ (cross-device) — fire-and-forget, ไม่บล็อก UI */
function postLobbyStyleToServer(themeIndex, skinIndex) {
var key = getPlayerKey();
if (!key) return;
try {
fetch(projectPath('/Admin/api/player-lobby-style.php'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'omit',
keepalive: true,
body: JSON.stringify({
playerKey: key,
colorThemeIndex: clampIndex(themeIndex, 1, 8),
skinToneIndex: clampIndex(skinIndex, 1, 3)
})
}).catch(function () { /* ออฟไลน์ — local ยังเก็บไว้ */ });
} catch (e) { /* ignore */ }
}
function projectPath(path) {
if (typeof window.appPath === 'function') return window.appPath(path);
return path;
}
function characterAssetsBasePath() {
var p = projectPath('/Game/img/characters/');
return p.replace(/\/?$/, '/');
}
function defaultShadowPath(dir) {
return projectPath('/Game/img/default-shadow-' + dir + '.png');
}
function characterLayerManifestUrl(characterId) {
return projectPath('/Game/api/characters/' + encodeURIComponent(characterId) + '/layer-manifest');
}
function fetchCharacterLayerManifest(characterId) {
if (!characterId) return Promise.resolve(null);
if (previewLayerManifestPromiseCache[characterId]) return previewLayerManifestPromiseCache[characterId];
previewLayerManifestPromiseCache[characterId] = fetch(characterLayerManifestUrl(characterId), { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) return null;
return res.json();
})
.then(function (json) {
if (!json || json.ok !== true) return null;
return json;
})
.catch(function () { return null; });
return previewLayerManifestPromiseCache[characterId];
}
function firstManifestFrame(manifest, dir) {
if (!manifest) return null;
var idleDir = manifest.byDirIdle && manifest.byDirIdle[dir];
if (idleDir && Array.isArray(idleDir.frames) && idleDir.frames.length) return idleDir.frames[0] || null;
var walkDir = manifest.byDir && manifest.byDir[dir];
if (walkDir && Array.isArray(walkDir.frames) && walkDir.frames.length) return walkDir.frames[0] || null;
return null;
}
function characterLayerFileUrl(fileName) {
if (!fileName) return '';
return characterAssetsBasePath() + encodeURIComponent(fileName);
}
function hexToRgb01(hex) {
var h = String(hex || '').replace('#', '').trim();
if (h.length !== 6) return [1, 1, 1];
return [
parseInt(h.slice(0, 2), 16) / 255,
parseInt(h.slice(2, 4), 16) / 255,
parseInt(h.slice(4, 6), 16) / 255
];
}
function tintImageData(imageData, tintHex) {
var rgb = hexToRgb01(tintHex);
var tr = rgb[0], tg = rgb[1], tb = rgb[2];
var d = imageData.data;
for (var i = 0; i < d.length; i += 4) {
if (d[i + 3] < 12) continue;
// Force exact tint color for color layers (bodyColor/headColor/hairColor).
d[i] = Math.min(255, Math.round(tr * 255));
d[i + 1] = Math.min(255, Math.round(tg * 255));
d[i + 2] = Math.min(255, Math.round(tb * 255));
}
}
function drawTintedLayer(ctx, w, h, img, tintHex) {
if (!tintHex) {
ctx.drawImage(img, 0, 0, w, h);
return;
}
var c = document.createElement('canvas');
c.width = w;
c.height = h;
var x = c.getContext('2d');
x.drawImage(img, 0, 0, w, h);
try {
var idata = x.getImageData(0, 0, w, h);
tintImageData(idata, tintHex);
x.putImageData(idata, 0, 0);
} catch (e) {
x.clearRect(0, 0, w, h);
x.drawImage(img, 0, 0, w, h);
}
ctx.drawImage(c, 0, 0, w, h);
}
function rgbToHsl01(r, g, b) {
var rn = r / 255;
var gn = g / 255;
var bn = b / 255;
var max = Math.max(rn, gn, bn);
var min = Math.min(rn, gn, bn);
var h = 0;
var s = 0;
var l = (max + min) / 2;
if (max !== min) {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case rn: h = (gn - bn) / d + (gn < bn ? 6 : 0); break;
case gn: h = (bn - rn) / d + 2; break;
default: h = (rn - gn) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function tintRgbByLuma(r, g, b, tintHex) {
var rgb = hexToRgb01(tintHex);
return [
Math.min(255, Math.round(rgb[0] * 255)),
Math.min(255, Math.round(rgb[1] * 255)),
Math.min(255, Math.round(rgb[2] * 255))
];
}
function composeHeuristicTintFromSource(sourceSrc, tint) {
if (!sourceSrc || !tint) return Promise.resolve(null);
var cacheKey = ['heuristic', sourceSrc, tint.head, tint.hair, tint.body].join('|');
if (Object.prototype.hasOwnProperty.call(previewCompositeCache, cacheKey)) {
return Promise.resolve(previewCompositeCache[cacheKey] || null);
}
return loadImageCached(sourceSrc).then(function (img) {
if (!img || !img.naturalWidth || !img.naturalHeight) return null;
var w = img.naturalWidth;
var h = img.naturalHeight;
var c = document.createElement('canvas');
c.width = w;
c.height = h;
var ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
try {
var idata = ctx.getImageData(0, 0, w, h);
var d = idata.data;
function alphaAt(x, y) {
if (x < 0 || y < 0 || x >= w || y >= h) return 0;
return d[((y * w + x) * 4) + 3];
}
function lumaAt(x, y) {
if (x < 0 || y < 0 || x >= w || y >= h) return 0;
var bi = (y * w + x) * 4;
var aa = d[bi + 3];
if (aa < 12) return 0;
var rr = d[bi];
var gg = d[bi + 1];
var bb = d[bi + 2];
return (0.299 * rr + 0.587 * gg + 0.114 * bb) / 255;
}
function isOutlinePixel(x, y, a) {
if (a < 12) return false;
// Keep outer contour/stroke untouched:
// pixels touching transparency are treated as outline.
if (alphaAt(x - 1, y) < 12) return true;
if (alphaAt(x + 1, y) < 12) return true;
if (alphaAt(x, y - 1) < 12) return true;
if (alphaAt(x, y + 1) < 12) return true;
return false;
}
function isNearOuterContour(x, y) {
for (var oy = -2; oy <= 2; oy += 1) {
for (var ox = -2; ox <= 2; ox += 1) {
if (ox === 0 && oy === 0) continue;
if (alphaAt(x + ox, y + oy) < 12) return true;
}
}
return false;
}
function isStrokeLikeByContrast(x, y, lig, sat) {
if (sat < 0.18 && lig < 0.68) return true;
var c = lumaAt(x, y);
var d1 = Math.abs(c - lumaAt(x - 1, y));
var d2 = Math.abs(c - lumaAt(x + 1, y));
var d3 = Math.abs(c - lumaAt(x, y - 1));
var d4 = Math.abs(c - lumaAt(x, y + 1));
var maxDiff = Math.max(d1, d2, d3, d4);
if (maxDiff >= 0.24 && lig < 0.78) return true;
return false;
}
for (var i = 0; i < d.length; i += 4) {
var a = d[i + 3];
if (a < 12) continue;
var px = (i / 4) % w;
var py = Math.floor((i / 4) / w);
if (isOutlinePixel(px, py, a)) continue;
if (isNearOuterContour(px, py)) continue;
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
var hsl = rgbToHsl01(r, g, b);
var hueDeg = hsl[0] * 360;
var sat = hsl[1];
var lig = hsl[2];
// Preserve dark lines/stroke and tiny details.
if (lig < 0.23) continue;
if (isStrokeLikeByContrast(px, py, lig, sat)) continue;
// Skin region: warm hue + upper-center body area
var inHeadArea = py <= h * 0.62 && px >= w * 0.18 && px <= w * 0.82;
var isSkinLike = inHeadArea && hueDeg >= 8 && hueDeg <= 45 && sat >= 0.14 && sat <= 0.78 && lig >= 0.24 && lig <= 0.95;
if (isSkinLike) {
var skinRgb = tintRgbByLuma(r, g, b, tint.head);
d[i] = skinRgb[0];
d[i + 1] = skinRgb[1];
d[i + 2] = skinRgb[2];
continue;
}
// รักษาเส้น tech สีฟ้า/cyan accent — อย่าย้อมให้เป็นสีธีม (เคสที่ user รายงาน)
if (hueDeg >= 150 && hueDeg <= 265 && sat >= 0.22) continue;
// Theme region: colored non-gray pixels (exclude very dark/very bright)
var isThemeCandidate = sat >= 0.2 && lig >= 0.3 && lig <= 0.95;
if (!isThemeCandidate) continue;
var themeHex = py <= h * 0.52 ? tint.hair : tint.body;
var themeRgb = tintRgbByLuma(r, g, b, themeHex);
d[i] = themeRgb[0];
d[i + 1] = themeRgb[1];
d[i + 2] = themeRgb[2];
}
ctx.putImageData(idata, 0, 0);
} catch (e) {
return null;
}
var out = c.toDataURL('image/png');
previewCompositeCache[cacheKey] = out;
return out;
});
}
function loadImageOnce(url, crossOrigin) {
return new Promise(function (resolve) {
var img = new Image();
img.decoding = 'async';
if (crossOrigin) { try { img.crossOrigin = 'anonymous'; } catch (e) { /* ignore */ } }
img.onload = function () { resolve(img.naturalWidth > 0 ? img : null); };
img.onerror = function () { resolve(null); };
// cache-bust เฉพาะตอน retry กันเบราว์เซอร์คืน error response ที่ cache ไว้
img.src = url;
if (img.complete && img.naturalWidth > 0) resolve(img);
});
}
function loadImageCached(url) {
if (!url) return Promise.resolve(null);
if (previewImagePromiseCache[url]) return previewImagePromiseCache[url];
// same-origin: อย่าตั้ง crossOrigin (บาง nginx ทำรูปโหลดล้ม) — ตั้งเฉพาะข้ามโดเมนเพื่อกัน canvas taint
var crossOrigin = false;
try {
var u = new URL(url, window.location.href);
crossOrigin = (u.origin !== window.location.origin);
} catch (e) { /* ignore */ }
var p = loadImageOnce(url, crossOrigin).then(function (img) {
if (img) return img;
// retry 1 ครั้ง (กันรูป layer หลุดชั่วคราวตอน burst โหลดหน้าแรก) แนบ cache-bust กัน error response ที่ cache
var bust = url + (url.indexOf('?') === -1 ? '?' : '&') + 'r=' + Date.now();
return loadImageOnce(bust, crossOrigin);
}).then(function (img) {
// อย่า cache ผลที่ล้ม → รอบหน้า (เปลี่ยนสี/เปิดใหม่) จะลองโหลดอีกครั้ง ไม่ค้าง null ทั้ง session
if (!img && previewImagePromiseCache[url] === p) delete previewImagePromiseCache[url];
return img;
});
previewImagePromiseCache[url] = p;
return p;
}
function resolveFirstImage(urls, idx) {
var index = idx || 0;
if (!urls || index >= urls.length) return Promise.resolve(null);
return loadImageCached(urls[index]).then(function (img) {
if (img) return img;
return resolveFirstImage(urls, index + 1);
});
}
function layerUrlCandidates(id, dir, layerName, frameIndex) {
var enc = encodeURIComponent(id);
var base = characterAssetsBasePath() + enc + '_' + dir;
var out = [];
function add(u) {
if (out.indexOf(u) === -1) out.push(u);
}
add(base + '_' + frameIndex + '_layer_' + layerName + '.png');
if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png');
add(base + '_layer_' + layerName + '.png');
return out;
}
function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) {
var enc = encodeURIComponent(id);
var base = characterAssetsBasePath() + enc + '_' + dir + '_idle';
var out = [];
function add(u) {
if (out.indexOf(u) === -1) out.push(u);
}
add(base + '_' + frameIndex + '_layer_' + layerName + '.png');
if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png');
add(base + '_layer_' + layerName + '.png');
return out;
}
function getSelectedTint() {
var skinIdx = clampIndex(state.activeSkin, 1, 3) - 1;
var colorIdx = clampIndex(state.activeColor, 1, 8) - 1;
return {
head: PLAY_TINT_HEAD[skinIdx],
hair: PLAY_TINT_HAIR[colorIdx],
body: PLAY_TINT_BODY[colorIdx]
};
}
function composeTintedCharacterPreview(characterId, tint) {
if (!characterId || !tint) return Promise.resolve(null);
var dir = 'down';
var frameIndex = 0;
var cacheKey = [characterId, dir, frameIndex, tint.head, tint.hair, tint.body, 'customize'].join('|');
if (Object.prototype.hasOwnProperty.call(previewCompositeCache, cacheKey)) {
return Promise.resolve(previewCompositeCache[cacheKey] || null);
}
function composeFromLayers(baseW, baseH, layers) {
var c = document.createElement('canvas');
c.width = baseW;
c.height = baseH;
var ctx = c.getContext('2d');
var anyTintColorLayer = false;
for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) {
var layerName = PLAY_LAYER_ORDER[li];
var img = layers[li];
if (!img) continue;
var tintKey = PLAY_LAYER_TINT_KEY[layerName];
var tintHex = tintKey ? tint[tintKey] : null;
if (tintKey) anyTintColorLayer = true;
drawTintedLayer(ctx, c.width, c.height, img, tintHex);
}
// ต้องมีอย่างน้อย 1 layer สี (bodyColor/headColor/hairColor) ที่โหลดได้ ไม่งั้นถือว่าไม่สำเร็จ
if (!anyTintColorLayer) return null;
var out = c.toDataURL('image/png');
previewCompositeCache[cacheKey] = out;
return out;
}
function tryManifest() {
return fetchCharacterLayerManifest(characterId).then(function (manifest) {
var manifestFrame = firstManifestFrame(manifest, dir);
if (!manifestFrame) return null;
var manifestLayerPromises = PLAY_LAYER_ORDER.map(function (layerName) {
var urls = [];
var fileName = manifestFrame[layerName];
if (fileName) urls.push(characterLayerFileUrl(fileName));
if (layerName === 'shadow') urls.push(defaultShadowPath(dir));
return resolveFirstImage(urls);
});
return Promise.all(manifestLayerPromises).then(function (layers) {
var baseImg = null;
for (var bi = 0; bi < layers.length; bi += 1) {
if (layers[bi] && layers[bi].naturalWidth > 0 && layers[bi].naturalHeight > 0) {
baseImg = layers[bi];
break;
}
}
if (!baseImg) return null;
return composeFromLayers(baseImg.naturalWidth, baseImg.naturalHeight, layers);
});
});
}
function tryRawLayers() {
var rawCandidates = [
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_idle.png',
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '.png',
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_0.png'
];
return resolveFirstImage(rawCandidates).then(function (rawImg) {
if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null;
var layerPromises = PLAY_LAYER_ORDER.map(function (layerName) {
var urls = layerUrlCandidatesIdle(characterId, dir, layerName, frameIndex)
.concat(layerUrlCandidates(characterId, dir, layerName, frameIndex));
if (layerName === 'shadow') urls.push(defaultShadowPath(dir));
return resolveFirstImage(urls);
});
return Promise.all(layerPromises).then(function (layers) {
return composeFromLayers(rawImg.naturalWidth, rawImg.naturalHeight, layers);
});
});
}
// exact: ลอง manifest ก่อน → ถ้าไม่สำเร็จ (layer สีโหลดไม่ครบ) ลอง raw layer แบบ convention ต่อ
// กันเคส "บาง browser/PC" ที่ตกไปใช้ heuristic แล้วย้อมทั้งรูป (เส้นฟ้า/ถุงมือกลายเป็นสีธีม)
return tryManifest().then(function (out) {
return out || tryRawLayers();
}).catch(function () { return null; });
}
/** สร้าง tint จาก index (ใช้ palette hardcoded — deterministic ทุก browser) */
function tintFromIndices(themeIndex, skinIndex) {
var skinIdx = clampIndex(skinIndex, 1, 3) - 1;
var colorIdx = clampIndex(themeIndex, 1, 8) - 1;
return {
head: PLAY_TINT_HEAD[skinIdx],
hair: PLAY_TINT_HAIR[colorIdx],
body: PLAY_TINT_BODY[colorIdx]
};
}
/**
* Public API — ให้ lobby.js เรียก recompute สีตัวละครจาก index ได้ทุกเครื่อง
* ใช้เส้น exact (composite layer) ก่อน, ถ้าไม่มีเลเยอร์ค่อย fallback heuristic จากรูปดิบฝั่ง server
* @returns {Promise<string|null>} data URL ของ idle ทิศ down หรือ null
*/
function composeTintedCharacterByIndex(characterId, themeIndex, skinIndex) {
if (!characterId) return Promise.resolve(null);
var tint = tintFromIndices(themeIndex, skinIndex);
return composeTintedCharacterPreview(characterId, tint).then(function (out) {
if (out) return out;
var enc = encodeURIComponent(characterId);
var rawCandidates = [
characterAssetsBasePath() + enc + '_down_idle.png',
characterAssetsBasePath() + enc + '_down.png',
characterAssetsBasePath() + enc + '_down_0.png'
];
return resolveFirstImage(rawCandidates).then(function (img) {
if (!img || !img.src) return null;
return composeHeuristicTintFromSource(img.src, tint);
});
}).catch(function () { return null; });
}
function resolveDomAvatarSource() {
var avatarSrc = '';
var avatarAlt = 'ตัวละครของผู้เล่น';
var candidates = [
'#lobby-character-img',
'#room-lobby-profile-avatar',
'#room-lobby-profile-overlay-avatar',
'#lobby-profile-avatar',
'#lobby-avatar-wrap img'
];
for (var i = 0; i < candidates.length; i += 1) {
var el = document.querySelector(candidates[i]);
if (!el) continue;
var src = (el.getAttribute('src') || '').trim();
if (!src) continue;
avatarSrc = src;
avatarAlt = (el.getAttribute('alt') || '').trim() || avatarAlt;
break;
}
return { src: avatarSrc, alt: avatarAlt };
}
function syncLiveAvatarElements(src, alt) {
if (!src) return;
var selectors = [
'#lobby-profile-avatar',
'#room-lobby-profile-avatar',
'#room-lobby-profile-overlay-avatar',
'#lobby-character-img'
];
selectors.forEach(function (sel) {
var el = document.querySelector(sel);
if (!el || el.tagName !== 'IMG') return;
try {
el.setAttribute('src', src);
if (alt) el.setAttribute('alt', alt);
} catch (e) { /* ignore */ }
});
}
function applyCustomizeSelection() {
return new Promise(function (resolve) {
resolveApplyCharacterId(function (characterId) {
function runApply() {
// ไม่มีตัวละคร (เครื่องที่ยังไม่ได้เลือกตัวละคร) — ยังต้องซิงก์สีไปเซิร์ฟเวอร์
// ให้คนอื่นเห็น แต่ข้ามการ composite รูป preview เพราะไม่มี characterId
if (!characterId) {
try {
persistConfirmedState();
syncLobbyTintStorageKeys();
postLobbyStyleToServer(state.activeColor, state.activeSkin);
} catch (e0) { /* ignore */ }
try {
window.dispatchEvent(new CustomEvent('customize-popup:applied', {
detail: {
characterId: '',
activeColor: clampIndex(state.activeColor, 1, 8),
activeSkin: clampIndex(state.activeSkin, 1, 3),
avatarSrc: ''
}
}));
} catch (e1) { /* ignore */ }
resolve(null);
return;
}
var tint = getSelectedTint();
var domAvatar = resolveDomAvatarSource();
var fallbackSrc = domAvatar.src || '';
composeTintedCharacterPreview(characterId, tint)
.then(function (outSrc) {
if (outSrc) return outSrc;
return composeHeuristicTintFromSource(fallbackSrc, tint);
})
.then(function (finalSrc) {
if (!finalSrc || typeof finalSrc !== 'string' || finalSrc.indexOf('data:image/') !== 0) return null;
try {
localStorage.setItem(LOBBY_IDLE_DOWN_PREFIX + characterId, finalSrc);
localStorage.setItem(appliedStorageKey, JSON.stringify({
characterId: characterId,
activeTab: clampIndex(state.activeTab, 1, 3),
activeColor: clampIndex(state.activeColor, 1, 8),
activeSkin: clampIndex(state.activeSkin, 1, 3),
activeItem: clampIndex(state.activeItem, 1, 8),
appliedAt: Date.now()
}));
persistConfirmedState();
syncLobbyTintStorageKeys();
postLobbyStyleToServer(state.activeColor, state.activeSkin);
} catch (e) { /* ignore */ }
syncLiveAvatarElements(finalSrc, 'ตัวละครที่ปรับแต่งแล้ว');
try {
window.dispatchEvent(new CustomEvent('customize-popup:applied', {
detail: {
characterId: characterId,
activeColor: clampIndex(state.activeColor, 1, 8),
activeSkin: clampIndex(state.activeSkin, 1, 3),
avatarSrc: finalSrc
}
}));
} catch (e2) { /* ignore */ }
return finalSrc;
})
.catch(function () { return null; })
.then(resolve);
}
if (isRoomLobbyPage() && typeof window.validateLobbyThemeColorChange === 'function') {
window.validateLobbyThemeColorChange(
clampIndex(state.activeColor, 1, 8),
clampIndex(state.activeSkin, 1, 3),
function (ok, err) {
if (!ok) {
if (err) try { alert(err); } catch (e) { /* ignore */ }
resolve(null);
return;
}
runApply();
}
);
return;
}
runApply();
});
});
}
function removeLegacyCustomizeOverlays() {
if (!isRoomLobbyPage()) return;
var wrong = document.getElementById('customize-popup-overlay');
if (wrong && wrong.parentNode) wrong.parentNode.removeChild(wrong);
var old = document.getElementById('room-cz-overlay');
if (old && !old.querySelector('.customize-popup-dialog') && old.parentNode) {
old.parentNode.removeChild(old);
}
var legacyStyle = document.getElementById('room-cz-style');
if (legacyStyle && legacyStyle.parentNode) legacyStyle.parentNode.removeChild(legacyStyle);
}
function createPopupIfNeeded() {
removeLegacyCustomizeOverlays();
var existing = document.getElementById(overlayId);
if (existing && existing.querySelector('.customize-popup-dialog')) {
bindRefs(existing);
return;
}
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
var overlay = document.createElement('div');
overlay.id = overlayId;
overlay.className = 'customize-popup-overlay is-hidden';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'ห้องแต่งตัว');
overlay.setAttribute('aria-hidden', 'true');
overlay.innerHTML = '' +
'<div class="customize-popup-dialog" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว">' +
' <div class="customize-popup-coin-wrap">' +
' <img class="customize-popup-coin-bg" src="' + asset('coin-bg.png') + '" alt="" decoding="async">' +
' <span class="customize-popup-coin-val">150</span>' +
' </div>' +
' <div class="customize-popup-content">' +
' <div class="customize-popup-char-col">' +
' <div class="customize-popup-player-name" id="customize-popup-player-name">MONE</div>' +
' <img class="customize-popup-char-bg" src="' + asset('char-bg.png') + '" alt="" decoding="async">' +
' <img class="customize-popup-char-preview" src="' + asset('face-1.png') + '" alt="ตัวอย่างตัวละคร" decoding="async">' +
' </div>' +
' <div class="customize-popup-panel-shell">' +
' <img class="customize-popup-cloth-bg" src="' + asset('cloth-bg.png') + '" alt="" decoding="async">' +
' <button type="button" class="customize-popup-close" aria-label="ปิดห้องแต่งตัว"></button>' +
' <div class="customize-popup-panel">' +
' <div class="customize-popup-theme-row">' +
' <img class="customize-popup-theme-label" src="' + asset('theme-color-txt.png') + '" alt="ธีมสี" decoding="async">' +
' <div class="customize-popup-theme-frame">' +
' <img src="' + asset('color-frame-session.png') + '" alt="" decoding="async">' +
' <div class="customize-popup-color-list customize-popup-color-row"></div>' +
' </div>' +
' </div>' +
' <div class="customize-popup-theme-row">' +
' <img class="customize-popup-theme-label" src="' + asset('theme-skin-txt.png') + '" alt="สีผิว" decoding="async">' +
' <div class="customize-popup-theme-frame">' +
' <img src="' + asset('color-frame-session.png') + '" alt="" decoding="async">' +
' <div class="customize-popup-color-list customize-popup-skin-row"></div>' +
' </div>' +
' </div>' +
' <div class="customize-popup-tabs"></div>' +
' <div class="customize-popup-items-wrap">' +
' <ul class="customize-popup-items"></ul>' +
' <div class="customize-popup-scroll is-hidden">' +
' <div class="customize-popup-scroll-track"></div>' +
' <div class="customize-popup-scroll-thumb"></div>' +
' </div>' +
' </div>' +
' <button type="button" class="customize-popup-confirm" aria-label="ยืนยัน">' +
' <img src="' + asset('btn-cf.png') + '" alt="ยืนยัน" decoding="async">' +
' </button>' +
' </div>' +
' </div>' +
' </div>' +
'</div>';
document.body.appendChild(overlay);
bindRefs(overlay);
bindPopupEvents();
renderAll();
}
function bindRefs(root) {
refs.overlay = root;
refs.dialog = root.querySelector('.customize-popup-dialog');
refs.panelShell = root.querySelector('.customize-popup-panel-shell');
refs.closeBtn = root.querySelector('.customize-popup-close');
refs.confirmBtn = root.querySelector('.customize-popup-confirm');
refs.previewFace = root.querySelector('.customize-popup-char-preview');
refs.nameEl = root.querySelector('#customize-popup-player-name');
refs.coinVal = root.querySelector('.customize-popup-coin-val');
refs.colorRow = root.querySelector('.customize-popup-color-row');
refs.skinRow = root.querySelector('.customize-popup-skin-row');
refs.tabRow = root.querySelector('.customize-popup-tabs');
refs.itemsEl = root.querySelector('.customize-popup-items');
refs.scrollEl = root.querySelector('.customize-popup-scroll');
refs.scrollTrack = root.querySelector('.customize-popup-scroll-track');
refs.scrollThumb = root.querySelector('.customize-popup-scroll-thumb');
}
function syncScale() {
if (!refs.dialog) return;
var w = refs.dialog.clientWidth || 0;
var h = refs.dialog.clientHeight || 0;
if (!w || !h) return;
// Scale from full popup canvas, not right panel only.
// This prevents left character column from shifting when right panel is tweaked.
var scaleW = w / 1600;
var scaleH = h / 951;
var vw = window.innerWidth || 0;
var vh = window.innerHeight || 0;
var minScale = (vw <= 1200 || vh <= 820) ? 0.32 : 0.45;
var scale = Math.max(minScale, Math.min(1, Math.min(scaleW, scaleH)));
refs.dialog.style.setProperty('--cc-scale', String(scale.toFixed(4)));
if (refs.panelShell) {
var shellW = refs.panelShell.clientWidth || 0;
var shellH = refs.panelShell.clientHeight || 0;
if (shellW && shellH) {
var shellScaleW = shellW / 990;
var shellScaleH = shellH / 951;
var shellScale = Math.max(0.28, Math.min(1, Math.min(shellScaleW, shellScaleH)));
refs.panelShell.style.setProperty('--cc-shell-scale', String(shellScale.toFixed(4)));
}
}
}
function getPlayerName() {
if (typeof window.getProfileDisplayName === 'function') {
var fromLobby = String(window.getProfileDisplayName() || '').trim();
if (fromLobby) return fromLobby;
}
var name = '';
try {
name = (localStorage.getItem('roomLobbyDisplayName') || '').trim() ||
(localStorage.getItem('playerName') || '').trim() || '';
} catch (e) { /* ignore */ }
return name || 'MONE';
}
function syncLobbyTintStorageKeys() {
try {
localStorage.setItem('lobbyThemeColor', String(clampIndex(state.activeColor, 1, 8)));
localStorage.setItem('lobbySkinTone', String(clampIndex(state.activeSkin, 1, 3)));
if (clampIndex(state.activeTab, 1, 3) === 1) {
localStorage.setItem('lobbyItem_face', String(clampIndex(state.activeItem, 1, 8)));
}
} catch (e) { /* ignore */ }
}
function readLobbyTintIntoState() {
try {
var c = localStorage.getItem('lobbyThemeColor');
if (c) state.activeColor = clampIndex(c, 1, 8);
var s = localStorage.getItem('lobbySkinTone');
if (s) state.activeSkin = clampIndex(s, 1, 3);
var f = localStorage.getItem('lobbyItem_face');
if (f) state.activeItem = clampIndex(f, 1, 8);
} catch (e) { /* ignore */ }
}
function swatchButtonHtml(hex, isActive) {
return '' +
'<span class="customize-popup-color-swatch customize-popup-color-swatch--hex" style="background-color:' + hex + '"></span>' +
'<img class="customize-popup-color-highlight" src="' + asset('color-select.png') + '" alt="" decoding="async">';
}
function renderColorRow() {
if (!refs.colorRow) return;
refs.colorRow.setAttribute('role', 'radiogroup');
refs.colorRow.setAttribute('aria-label', 'ธีมสี');
refs.colorRow.textContent = '';
for (var i = 1; i <= 8; i += 1) {
var hex = THEME_SWATCH_HEX[i - 1] || '#ffffff';
var taken = isRoomLobbyPage() && typeof window.isLobbyThemeIndexAvailable === 'function'
&& !window.isLobbyThemeIndexAvailable(i, true);
var btn = document.createElement('button');
btn.type = 'button';
var cls = 'customize-popup-color-btn';
if (state.activeColor === i) cls += ' is-active';
if (taken) cls += ' is-taken';
btn.className = cls;
btn.setAttribute('aria-label', taken ? ('สีที่ ' + i + ' มีคนใช้แล้ว') : ('เลือกสีที่ ' + i));
btn.setAttribute('aria-pressed', state.activeColor === i ? 'true' : 'false');
if (taken) btn.setAttribute('aria-disabled', 'true');
btn.innerHTML = swatchButtonHtml(hex, state.activeColor === i);
(function (idx, isTaken) {
btn.addEventListener('click', function () {
if (isRoomLobbyPage() && typeof window.isLobbyThemeIndexAvailable === 'function') {
if (!window.isLobbyThemeIndexAvailable(idx, true)) return;
}
state.activeColor = idx;
renderColorRow();
renderPreview();
});
})(i, taken);
refs.colorRow.appendChild(btn);
}
}
function renderSkinRow() {
if (!refs.skinRow) return;
refs.skinRow.setAttribute('role', 'radiogroup');
refs.skinRow.setAttribute('aria-label', 'สีผิว');
refs.skinRow.textContent = '';
for (var i = 1; i <= 3; i += 1) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'customize-popup-color-btn' + (state.activeSkin === i ? ' is-active' : '');
btn.setAttribute('aria-label', 'เลือกสีผิวที่ ' + i);
btn.setAttribute('aria-pressed', state.activeSkin === i ? 'true' : 'false');
var skinHex = SKIN_SWATCH_HEX[i - 1] || '#eaa78a';
btn.innerHTML = swatchButtonHtml(skinHex, state.activeSkin === i);
(function (idx) {
btn.addEventListener('click', function () {
state.activeSkin = idx;
renderSkinRow();
renderPreview();
});
})(i);
refs.skinRow.appendChild(btn);
}
}
/** ใช้รูปปุ่มเดี่ยว 256×100 (-a.png) ทุก state — ไฟล์ tab-N.png เป็นแถบ 3 ปุ่มของ mockup ใช้เป็น sprite ปุ่มเดี่ยวไม่ได้ */
function tabImageName(index) {
if (index === 1) return 'tab-1-face-a.png';
if (index === 2) return 'tab-2-hair-a.png';
return 'tab-3-cloth-a.png';
}
function renderTabs() {
if (!refs.tabRow) return;
if (state.activeTab === 1) {
refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(-27px * var(--cc-scale))');
} else if (state.activeTab === 3) {
refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(0px * var(--cc-scale))');
} else {
refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(0px * var(--cc-scale))');
}
refs.tabRow.textContent = '';
for (var i = 1; i <= 3; i += 1) {
var active = state.activeTab === i;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'customize-popup-tab-btn' + (active ? ' is-active' : '');
btn.setAttribute('aria-label', 'หมวด ' + i);
btn.innerHTML = '' +
'<img class="customize-popup-tab-img" src="' + asset(tabImageName(i)) + '" alt="" decoding="async">' +
'<img class="customize-popup-tab-btn-line" src="' + asset('tab-line.png') + '" alt="" decoding="async">';
(function (idx) {
btn.addEventListener('click', function () {
state.activeTab = idx;
renderTabs();
renderItems();
});
})(i);
refs.tabRow.appendChild(btn);
}
}
function renderItems() {
if (!refs.itemsEl) return;
refs.itemsEl.setAttribute('role', 'radiogroup');
refs.itemsEl.setAttribute('aria-label', 'รายการปรับแต่งตัวละคร');
refs.itemsEl.textContent = '';
for (var i = 1; i <= 8; i += 1) {
var li = document.createElement('li');
li.className = 'customize-popup-item' + (state.activeItem === i ? ' is-active' : '');
li.setAttribute('role', 'radio');
li.setAttribute('aria-checked', state.activeItem === i ? 'true' : 'false');
li.setAttribute('aria-label', 'ไอเท็มที่ ' + i);
li.tabIndex = 0;
var price = (state.activeTab === 1 ? (i <= 2 ? 50 : i <= 5 ? 100 : 200) : (i <= 3 ? 120 : 220));
li.innerHTML = '' +
'<img class="customize-popup-item-bg" src="' + asset('item-bg.png') + '" alt="" decoding="async">' +
'<img class="customize-popup-item-face" src="' + asset('face-' + i + '.png') + '" alt="" decoding="async">' +
'<img class="customize-popup-item-price" src="' + asset('price-frame.png') + '" alt="" decoding="async">' +
'<span class="customize-popup-item-price-text">' + price + '</span>' +
'<img class="customize-popup-item-highlight" src="' + asset('item-select.png') + '" alt="" decoding="async">';
(function (idx) {
function selectItem() {
state.activeItem = idx;
renderItems();
renderPreview();
}
li.addEventListener('click', function () {
selectItem();
});
li.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectItem();
}
});
})(i);
refs.itemsEl.appendChild(li);
}
syncScrollUi();
}
/** โหลดรูปตัวละครจริง (idle ทิศ down) แบบไม่ย้อม — ใช้เป็น fallback ที่สีถูกต้อง แทน heuristic ที่ย้อมเหลืองทั้งรูป */
function loadRawCharacterImageSrc(characterId) {
if (!characterId) return Promise.resolve('');
var enc = encodeURIComponent(characterId);
var rawCandidates = [
characterAssetsBasePath() + enc + '_down_idle.png',
characterAssetsBasePath() + enc + '_down.png',
characterAssetsBasePath() + enc + '_down_0.png'
];
return resolveFirstImage(rawCandidates).then(function (img) {
return (img && img.src) ? img.src : '';
}).catch(function () { return ''; });
}
function renderPreview() {
if (!refs.previewFace) return;
var token = ++previewRenderToken;
var domAvatar = resolveDomAvatarSource();
var fallbackSrc = domAvatar.src || asset('face-' + state.activeItem + '.png');
refs.previewFace.src = fallbackSrc;
refs.previewFace.alt = domAvatar.alt;
var tint = getSelectedTint();
// resolve characterId: localStorage → default char จาก API (กันเครื่องที่ยังไม่เคยเลือกตัวละคร → preview ว่าง/เหลือง)
resolveApplyCharacterId(function (characterId) {
if (token !== previewRenderToken || !refs.previewFace) return;
composeTintedCharacterPreview(characterId, tint)
.then(function (outSrc) {
if (token !== previewRenderToken || !refs.previewFace) return;
if (outSrc) {
refs.previewFace.src = outSrc;
refs.previewFace.alt = 'ตัวอย่างตัวละครที่ปรับแต่งสีแล้ว';
return;
}
// composite ล้ม → โชว์รูปตัวละครจริงแบบไม่ย้อม (สีถูกต้อง) แทน heuristic ที่ย้อมเหลืองทั้งรูป
return loadRawCharacterImageSrc(characterId).then(function (rawSrc) {
if (token !== previewRenderToken || !refs.previewFace) return;
refs.previewFace.src = rawSrc || fallbackSrc;
refs.previewFace.alt = domAvatar.alt;
});
})
.catch(function () {
if (token !== previewRenderToken || !refs.previewFace) return;
refs.previewFace.src = fallbackSrc;
refs.previewFace.alt = domAvatar.alt;
});
});
}
function syncCoins() {
if (!refs.coinVal) return;
var c = '150';
try {
var raw = localStorage.getItem('jdCoins');
if (raw != null && raw !== '') c = String(Math.max(0, parseInt(raw, 10) || 0));
} catch (e) { /* ignore */ }
refs.coinVal.textContent = c;
}
function syncPlayerName() {
if (!refs.nameEl) return;
refs.nameEl.textContent = getPlayerName().toUpperCase();
}
function syncScrollUi() {
if (!refs.itemsEl || !refs.scrollEl || !refs.scrollThumb) return;
var total = refs.itemsEl.scrollHeight || 0;
var visible = refs.itemsEl.clientHeight || 0;
// Hide scrollbar when overflow is only tiny visual jitter.
var overflowPx = Math.max(0, total - visible);
var overflow = overflowPx > 12;
refs.scrollEl.classList.toggle('is-hidden', !overflow);
refs.itemsEl.style.overflowY = overflow ? 'auto' : 'hidden';
if (!overflow) {
refs.itemsEl.scrollTop = 0;
return;
}
var maxScroll = Math.max(1, total - visible);
var ratio = Math.max(0, Math.min(1, refs.itemsEl.scrollTop / maxScroll));
var minBarPct = 18;
var maxBarPct = 42;
var rawBarPct = (visible / Math.max(1, total)) * 100;
var barPct = Math.max(minBarPct, Math.min(maxBarPct, rawBarPct));
var topPct = 0;
var travelPct = Math.max(0, 100 - topPct - barPct);
refs.scrollThumb.style.height = barPct.toFixed(2) + '%';
refs.scrollThumb.style.top = (topPct + (travelPct * ratio)).toFixed(2) + '%';
}
function setItemsScrollByRatio(ratio) {
if (!refs.itemsEl) return;
var total = refs.itemsEl.scrollHeight || 0;
var visible = refs.itemsEl.clientHeight || 0;
var maxScroll = Math.max(0, total - visible);
var safe = Math.max(0, Math.min(1, ratio));
refs.itemsEl.scrollTop = maxScroll * safe;
syncScrollUi();
}
function scrollRatioFromClientY(clientY, offsetY) {
if (!refs.scrollEl || !refs.scrollThumb) return 0;
var rect = refs.scrollEl.getBoundingClientRect();
var thumbRect = refs.scrollThumb.getBoundingClientRect();
var thumbH = Math.max(1, thumbRect.height);
var trackTop = rect.top;
var maxTop = rect.height - thumbH;
if (maxTop <= 0) return 0;
var thumbTop = clientY - trackTop - Math.max(0, offsetY || 0);
thumbTop = Math.max(0, Math.min(maxTop, thumbTop));
return thumbTop / maxTop;
}
function stopScrollbarDrag() {
if (!scrollbarDrag.active) return;
scrollbarDrag.active = false;
if (refs.scrollThumb) refs.scrollThumb.classList.remove('is-dragging');
try { document.body.style.userSelect = ''; } catch (e) { /* ignore */ }
}
function startScrollbarDrag(e) {
if (!refs.scrollThumb || !refs.scrollEl || refs.scrollEl.classList.contains('is-hidden')) return;
e.preventDefault();
var thumbRect = refs.scrollThumb.getBoundingClientRect();
scrollbarDrag.active = true;
scrollbarDrag.pointerOffsetY = e.clientY - thumbRect.top;
refs.scrollThumb.classList.add('is-dragging');
try { document.body.style.userSelect = 'none'; } catch (err) { /* ignore */ }
}
function renderAll() {
renderColorRow();
renderSkinRow();
renderTabs();
renderItems();
renderPreview();
syncCoins();
syncPlayerName();
syncScale();
requestAnimationFrame(syncScale);
requestAnimationFrame(syncScrollUi);
}
function openPopup() {
createPopupIfNeeded();
if (!refs.overlay) return;
resetStateToConfirmed();
readLobbyTintIntoState();
renderAll();
refs.overlay.classList.remove('is-hidden');
refs.overlay.setAttribute('aria-hidden', 'false');
syncCoins();
syncPlayerName();
syncScale();
syncScrollUi();
renderPreview();
if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
requestAnimationFrame(syncScale);
requestAnimationFrame(syncScrollUi);
}
function closePopup() {
if (!refs.overlay) return;
stopScrollbarDrag();
/* ย้าย focus ออกจาก overlay ก่อนซ่อน — กันคำเตือน "aria-hidden on focused element" (เช่นปุ่มยืนยันที่เพิ่งกด) */
try {
if (refs.overlay.contains(document.activeElement) && document.activeElement.blur) {
document.activeElement.blur();
}
} catch (e) { /* ignore */ }
refs.overlay.classList.add('is-hidden');
refs.overlay.setAttribute('aria-hidden', 'true');
}
function bindPopupEvents() {
if (!refs.overlay) return;
refs.overlay.addEventListener('click', function (e) {
if (e.target === refs.overlay) closePopup();
});
refs.closeBtn && refs.closeBtn.addEventListener('click', closePopup);
refs.confirmBtn && refs.confirmBtn.addEventListener('click', function () {
applyCustomizeSelection().finally(function () {
closePopup();
});
});
refs.itemsEl && refs.itemsEl.addEventListener('scroll', syncScrollUi, { passive: true });
refs.scrollThumb && refs.scrollThumb.addEventListener('mousedown', startScrollbarDrag);
refs.scrollTrack && refs.scrollTrack.addEventListener('mousedown', function (e) {
if (refs.scrollEl && refs.scrollEl.classList.contains('is-hidden')) return;
if (e.target === refs.scrollThumb) return;
e.preventDefault();
var ratio = scrollRatioFromClientY(e.clientY, (refs.scrollThumb.getBoundingClientRect().height || 0) / 2);
setItemsScrollByRatio(ratio);
});
document.addEventListener('mousemove', function (e) {
if (!scrollbarDrag.active) return;
e.preventDefault();
var ratio = scrollRatioFromClientY(e.clientY, scrollbarDrag.pointerOffsetY);
setItemsScrollByRatio(ratio);
});
document.addEventListener('mouseup', function () {
stopScrollbarDrag();
});
window.addEventListener('resize', function () {
syncScale();
syncScrollUi();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && refs.overlay && !refs.overlay.classList.contains('is-hidden')) {
stopScrollbarDrag();
closePopup();
}
});
}
function bindTriggers() {
triggerSelectors.forEach(function (sel) {
document.querySelectorAll(sel).forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
openPopup();
});
});
});
}
/** ซ่อมรูป avatar ที่ถูก "อบ" (bake) ไว้ผิดสีจากโค้ดเวอร์ชันเก่า (heuristic = ชุดขาว+ขอบสี)
→ คอมโพสิตใหม่จากเลเยอร์จริงด้วยสีที่บันทึกไว้ แล้วเขียนทับ bake เดิม (self-heal ทุกเครื่อง) */
function rebakeAvatarFromSavedColors() {
if (isRoomLobbyPage()) return; // room-lobby มีระบบ tint ของตัวเองอยู่แล้ว
var characterId = getStoredCharacterId();
if (!characterId) return;
var hasSavedColor = false;
try { hasSavedColor = !!localStorage.getItem('lobbyThemeColor'); } catch (e) { /* ignore */ }
if (!hasSavedColor) return; // ยังไม่เคยเลือกสี → คงรูป default ไว้ ไม่ไปอบทับ
readLobbyTintIntoState();
composeTintedCharacterPreview(characterId, getSelectedTint()).then(function (outSrc) {
if (!outSrc || typeof outSrc !== 'string' || outSrc.indexOf('data:image/') !== 0) return; // composite ไม่สำเร็จ → ไม่เขียนทับด้วย fallback
try { localStorage.setItem(LOBBY_IDLE_DOWN_PREFIX + characterId, outSrc); } catch (e) { /* ignore */ }
syncLiveAvatarElements(outSrc, 'ตัวละครที่ปรับแต่งแล้ว');
}).catch(function () { /* ignore */ });
}
function init() {
createPopupIfNeeded();
if (triggerSelectors.length) bindTriggers();
rebakeAvatarFromSavedColors();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
window.openCustomizePopup = openPopup;
window.closeCustomizePopup = closePopup;
window.refreshCustomizeColorRow = renderColorRow;
/** ใช้โดย lobby.js เพื่อ recompute สีตัวละครจาก index บนทุกเครื่อง */
window.jdComposeTintedCharacter = composeTintedCharacterByIndex;
})();