(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 = '' + ''; 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 '' + '' + ''; } 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; })();