(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 ''; } } 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; } // 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 loadImageCached(url) { if (!url) return Promise.resolve(null); if (previewImagePromiseCache[url]) return previewImagePromiseCache[url]; previewImagePromiseCache[url] = new Promise(function (resolve) { var img = new Image(); img.decoding = 'async'; img.onload = function () { resolve(img.naturalWidth > 0 ? img : null); }; img.onerror = function () { resolve(null); }; img.src = url; if (img.complete && img.naturalWidth > 0) resolve(img); }); return previewImagePromiseCache[url]; } 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); } return fetchCharacterLayerManifest(characterId).then(function (manifest) { var manifestFrame = firstManifestFrame(manifest, dir); if (manifestFrame) { 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; var c = document.createElement('canvas'); c.width = baseImg.naturalWidth; c.height = baseImg.naturalHeight; 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); } if (!anyTintColorLayer) return null; var out = c.toDataURL('image/png'); previewCompositeCache[cacheKey] = out; return out; }); } 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) { var c = document.createElement('canvas'); c.width = rawImg.naturalWidth; c.height = rawImg.naturalHeight; 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); } if (!anyTintColorLayer) return null; var out = c.toDataURL('image/png'); previewCompositeCache[cacheKey] = out; return out; }); }); }); } 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() { var characterId = getStoredCharacterId(); if (!characterId) return Promise.resolve(null); var tint = getSelectedTint(); var domAvatar = resolveDomAvatarSource(); var fallbackSrc = domAvatar.src || ''; return 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(); } 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; }); } 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 = '' + '
' +
' 150' +
'
' +
'
' +
'
' +
' ' +
'
' +
'
' +
' ' +
'
' +
'
' +
' ' +
'
';
}
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 btn = document.createElement('button');
btn.type = 'button';
btn.className = 'customize-popup-color-btn' + (state.activeColor === i ? ' is-active' : '');
btn.setAttribute('aria-label', 'เลือกสีที่ ' + i);
btn.setAttribute('aria-pressed', state.activeColor === i ? 'true' : 'false');
btn.innerHTML = swatchButtonHtml(hex, state.activeColor === i);
(function (idx) {
btn.addEventListener('click', function () {
state.activeColor = idx;
renderColorRow();
renderPreview();
});
})(i);
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);
}
}
function tabImageName(index, isActive) {
if (index === 1) return isActive ? 'tab-1-face-a.png' : 'tab-1-face.png';
if (index === 2) return isActive ? 'tab-2-hair-a.png' : 'tab-2-hair.png';
return isActive ? 'tab-3-cloth-a.png' : 'tab-3-cloth.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 = '' +
'
';
(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 = '' +
'
' +
'
' +
'
' +
'' + price + '' +
'
';
(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();
}
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 characterId = getStoredCharacterId();
composeTintedCharacterPreview(characterId, getSelectedTint())
.then(function (outSrc) {
if (token !== previewRenderToken || !refs.previewFace) return;
if (outSrc) {
refs.previewFace.src = outSrc;
refs.previewFace.alt = 'ตัวอย่างตัวละครที่ปรับแต่งสีแล้ว';
return;
}
return composeHeuristicTintFromSource(fallbackSrc, getSelectedTint()).then(function (heuristicSrc) {
if (token !== previewRenderToken || !refs.previewFace) return;
refs.previewFace.src = heuristicSrc || fallbackSrc;
refs.previewFace.alt = heuristicSrc ? 'ตัวอย่างตัวละครที่ปรับแต่งสีแล้ว' : 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();
requestAnimationFrame(syncScale);
requestAnimationFrame(syncScrollUi);
}
function closePopup() {
if (!refs.overlay) return;
stopScrollbarDrag();
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();
});
});
});
}
function init() {
createPopupIfNeeded();
if (triggerSelectors.length) bindTriggers();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
window.openCustomizePopup = openPopup;
window.closeCustomizePopup = closePopup;
})();