Files
justice/www/html/Main-Lobby/lobby.js
T

1067 lines
38 KiB
JavaScript

(function () {
'use strict';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
/** รูป composite idle ทิศ down จากหน้า Character (preview-idle-layer-down) — ตรงกับ character.js */
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
function getStoredLobbyIdleDownDataUrl(safeId) {
if (!safeId) return '';
try {
var v = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + safeId) || '';
if (typeof v === 'string' && v.indexOf('data:image/') === 0 && v.length > 80) return v;
} catch (e) { /* ignore */ }
return '';
}
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = typeof appPath === 'function' ? appPath('/Main-Lobby/IMAGE/guide') : '/Main-Lobby/IMAGE/guide';
/**
* ตำแหน่งแถบคำแนะนำ + จุดไฮไลต์ต่อ guide-01…05 (ปรับลำดับให้ตรงไฟล์รูป)
* position: 'bottom' | 'top' — ตาม mock แถบล่าง/บน
* highlight: selector ของปุ่มใน lobby (null = ไม่วงกรอบ)
*/
var GUIDE_STEPS = [
{ position: 'bottom', highlight: null, pad: 10 },
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 },
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 },
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 },
{ position: 'bottom', highlight: '#btn-daily', pad: 10 },
];
function checkLogin() {
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLogin()) return;
function toast(msg) {
var el = document.getElementById('lobby-toast');
if (!el) return;
el.textContent = msg;
el.classList.add('lobby-toast--show');
clearTimeout(toast._t);
toast._t = setTimeout(function () {
el.classList.remove('lobby-toast--show');
}, 2600);
}
function padAgent(n) {
var s = String(n);
while (s.length < 6) s = '0' + s;
return s;
}
function ensureAgentId() {
var k = 'agentDisplayId';
var v = localStorage.getItem(k);
if (!v || !/^\d{6}$/.test(v)) {
v = padAgent(100000 + Math.floor(Math.random() * 899999));
try {
localStorage.setItem(k, v);
} catch (e) { /* ignore */ }
}
return v;
}
function getSelectedCharacterId() {
try {
return (localStorage.getItem(CHAR_KEY) || '').trim();
} catch (e) {
return '';
}
}
/** ใช้เฉพาะ id ที่ปลอดเป็น segment ใน URL — กันพาธแปลก/อักขระที่เซิร์ฟไม่มีไฟล์ */
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ให้ถือเป็นว่าง
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
}
function mainMenuCharFallbackUrl() {
return typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
/**
* URL รูปโลบี้ — ลองทิศ down ก่อน (idle แล้ว walk) เพื่อไม่ยิง _up_idle_ ฯลฯ โดยไม่จำเป็น
* ถ้า down ไม่มีค่อยลอง up / left / right
* @param {string} safeId
* @param {boolean} preferIdle
*/
function characterSpriteCandidateUrls(safeId, preferIdle) {
if (!safeId) return [];
var enc = encodeURIComponent(safeId);
var q = '?ch=' + encodeURIComponent(safeId);
var dirsPrimary = ['down'];
var dirsFallback = ['up', 'left', 'right'];
var wantIdle = preferIdle !== false;
var urls = [];
function pushForDir(d) {
if (wantIdle) {
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle_0.png' + q);
}
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_0.png' + q);
}
for (var a = 0; a < dirsPrimary.length; a++) pushForDir(dirsPrimary[a]);
for (var b = 0; b < dirsFallback.length; b++) pushForDir(dirsFallback[b]);
return urls;
}
/** หลังลองครบแล้วไม่มีรูป — ลดการยิงซ้ำในเซสชัน (ไม่บล็อกการลองใหม่หลังอัปโหลดไฟล์แล้ว — ลบคีย์ตอนโหลดสำเร็จ) */
var CHAR_ASSET_MISS_PREFIX = 'jdCharAssetMissing:';
function markCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.setItem(CHAR_ASSET_MISS_PREFIX + safeId, '1');
} catch (e) { /* ignore */ }
}
function clearCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.removeItem(CHAR_ASSET_MISS_PREFIX + safeId);
} catch (e) { /* ignore */ }
}
/**
* ลอง URL ตามลำดับด้วย Image — ตรงกับพฤติกรรม <img> ใน character-grid
* @param {string[]} urls
* @param {(url: string | null) => void} cb
*/
function probeFirstLoadableImageUrl(urls, cb) {
var i = 0;
function next() {
if (i >= urls.length) {
cb(null);
return;
}
var u = urls[i++];
var img = new Image();
img.onload = function () {
if (img.naturalWidth > 0) cb(u);
else next();
};
img.onerror = function () {
next();
};
img.src = u;
}
next();
}
/**
* @param {string} safeId
* @param {(url: string | null) => void} cb
*/
function characterLobbySpriteFirstLiveUrl(safeId, cb) {
if (!safeId) {
cb(null);
return;
}
var urls = characterSpriteCandidateUrls(safeId, true);
if (!urls.length) {
cb(null);
return;
}
probeFirstLoadableImageUrl(urls, function (url) {
if (url) {
clearCharacterDownAssetMissing(safeId);
cb(url);
} else {
markCharacterDownAssetMissing(safeId);
cb(null);
}
});
}
/**
* id จาก localStorage หรือถ้าว่าง — ตัวสุดท้ายในรายการ API (ใหม่สุดตามลำดับเซิร์ฟ)
* @param {(safeId: string) => void} callback
*/
function resolveLobbyCharacterId(callback) {
var norm = normalizeCharacterAssetId(getSelectedCharacterId());
if (norm) {
callback(norm);
return;
}
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) {
callback('');
return;
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
if (id) { try { localStorage.setItem(CHAR_KEY, id); } catch (e) {} } // persist ตัวละคร default ให้ห้อง/เกมอ่านไปใช้ join
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
function ensurePlayerKey() {
var k;
try {
k = localStorage.getItem(PLAYER_KEY);
} catch (e) {
k = '';
}
if (!k || String(k).length < 8) {
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
try {
localStorage.setItem(PLAYER_KEY, k);
} catch (e2) { /* ignore */ }
}
return k;
}
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch((typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) return;
var c = String(Math.max(0, parseInt(d.coins, 10) || 0));
try {
localStorage.setItem('jdCoins', c);
} catch (e) { /* ignore */ }
var coinsEl = document.getElementById('lobby-profile-coins');
if (coinsEl) coinsEl.textContent = c;
})
.catch(function () { /* offline */ });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
lobbyBakedCharUrl = urlOrNull;
applyLobbyCharacterAppearance();
}
resolveLobbyCharacterId(function (id) {
lobbyResolvedCharId = id || '';
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
if (idleCached) {
applyCharAssets(idleCached);
return;
}
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
});
}
function applyProfileTexts() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
var coinsEl = document.getElementById('lobby-profile-coins');
if (nameEl) nameEl.textContent = name.toUpperCase();
if (agentEl) agentEl.textContent = ensureAgentId();
if (coinsEl) coinsEl.textContent = coins;
}
/** @param {string | null} urlOrNull — URL ที่ HEAD ผ่านแล้ว หรือ null = ใช้ char-main */
function applyProfileAvatar(urlOrNull) {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fb = mainMenuCharFallbackUrl();
av.onerror = null;
if (!urlOrNull) {
av.src = fb;
return;
}
av.onerror = function () {
av.onerror = null;
av.src = fb;
};
av.src = urlOrNull;
}
function applyCenterCharacterResolved(urlOrNull) {
var centerEl = document.getElementById('lobby-character-img');
if (!centerEl) return;
function showSceneOnly() {
centerEl.classList.remove('lobby-character-img--visible');
centerEl.removeAttribute('src');
}
function revealChar() {
centerEl.classList.add('lobby-character-img--visible');
}
function showFallbackCenterCharacter() {
var fb = mainMenuCharFallbackUrl();
centerEl.onload = revealChar;
centerEl.onerror = function () {
centerEl.onerror = null;
showSceneOnly();
};
centerEl.src = fb;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
function showWithChar(url) {
centerEl.onload = revealChar;
centerEl.onerror = function () {
showFallbackCenterCharacter();
};
centerEl.src = url;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
if (!urlOrNull) {
showFallbackCenterCharacter();
return;
}
showWithChar(urlOrNull);
}
// คำนวณ offset จากความสูงจอด้วย linear interpolation:
// 1080 -> 18cqh, 650 -> 25cqh
function applyCharacterFootOffsetByViewport() {
var root = document.documentElement;
if (!root) return;
var h = Math.max(1, window.innerHeight || 0);
var h1 = 650;
var y1 = 25;
var h2 = 1080;
var y2 = 18;
var t = (h - h1) / (h2 - h1);
var cqhValue = y1 + ((y2 - y1) * t);
// กันหลุดช่วงเมื่อจอเล็ก/ใหญ่เกินช่วงอ้างอิง
cqhValue = Math.max(18, Math.min(25, cqhValue));
root.style.setProperty('--lobby-character-foot-offset', cqhValue.toFixed(3) + 'cqh');
}
function syncLeaderboardScrollbarThumb() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
if (!scrollEl || !thumbEl) return;
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var minThumb = parseFloat(styles.minHeight) || 18;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
thumbEl.style.display = 'none';
return;
}
thumbEl.style.display = 'block';
var trackHeight = scrollEl.clientHeight;
var visibleRatio = scrollEl.clientHeight / Math.max(1, scrollEl.scrollHeight);
var thumbHeight = Math.max(minThumb, Math.round(trackHeight * visibleRatio));
var maxY = Math.max(0, trackHeight - topInset - thumbHeight);
var y = topInset + (maxY * (scrollEl.scrollTop / maxScroll));
thumbEl.style.height = thumbHeight + 'px';
thumbEl.style.transform = 'translateY(' + y.toFixed(2) + 'px)';
}
// วาง leaderboard ให้ต่อจากปุ่ม daily โดยไม่ทับกัน
function placeLeaderboardBelowDaily() {
var dailyBtn = document.getElementById('btn-daily');
var leaderboard = document.querySelector('.lobby-leaderboard');
var lobbyUi = document.querySelector('.lobby-ui');
if (!dailyBtn || !leaderboard || !lobbyUi) return;
var boardStyle = window.getComputedStyle(leaderboard);
if (boardStyle.position !== 'absolute') {
leaderboard.style.top = '';
return;
}
var uiRect = lobbyUi.getBoundingClientRect();
var dailyRect = dailyBtn.getBoundingClientRect();
var boardRect = leaderboard.getBoundingClientRect();
var cssGap = boardStyle.getPropertyValue('--lobby-daily-leaderboard-gap').trim();
var gapPx = 8;
if (cssGap) {
var n = parseFloat(cssGap);
if (!Number.isNaN(n)) {
if (cssGap.endsWith('cqh')) gapPx = (uiRect.height * n) / 100;
else if (cssGap.endsWith('px') || /^[+-]?\d+(\.\d+)?$/.test(cssGap)) gapPx = n;
else if (cssGap.endsWith('vh')) gapPx = ((window.innerHeight || 0) * n) / 100;
else if (cssGap.endsWith('%')) gapPx = (uiRect.height * n) / 100;
}
}
var nextTop = (dailyRect.bottom - uiRect.top) + gapPx;
var maxTop = Math.max(0, uiRect.height - boardRect.height - Math.max(0, gapPx));
var clampedTop = Math.max(0, Math.min(nextTop, maxTop));
leaderboard.style.top = clampedTop.toFixed(2) + 'px';
}
var leaderboardLayoutRaf = 0;
function scheduleLeaderboardPlacement() {
if (leaderboardLayoutRaf) cancelAnimationFrame(leaderboardLayoutRaf);
leaderboardLayoutRaf = requestAnimationFrame(function () {
leaderboardLayoutRaf = 0;
placeLeaderboardBelowDaily();
syncLeaderboardScrollbarThumb();
});
}
function bindLeaderboardScrollSync() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.scrollSyncBound === '1') return;
scrollEl.addEventListener('scroll', syncLeaderboardScrollbarThumb, { passive: true });
scrollEl.dataset.scrollSyncBound = '1';
}
var guideStep = 1;
var guideImg = document.getElementById('guide-img');
var guideBar = document.getElementById('lobby-guide-bar');
var guideDim = document.getElementById('lobby-guide-dim');
var guideFocus = document.getElementById('lobby-guide-focus');
function getGuideStepConfig(step) {
return GUIDE_STEPS[step - 1] || { position: 'bottom', highlight: null, pad: 10 };
}
function hideGuideFocus() {
if (!guideFocus) return;
guideFocus.classList.add('hidden');
guideFocus.style.left = '0';
guideFocus.style.top = '0';
guideFocus.style.width = '0';
guideFocus.style.height = '0';
}
function scheduleGuideFocusUpdate() {
requestAnimationFrame(function () {
requestAnimationFrame(updateGuideFocus);
});
}
function updateGuideFocus() {
if (!guideFocus || !guideBar || guideBar.classList.contains('hidden')) {
hideGuideFocus();
return;
}
var cfg = getGuideStepConfig(guideStep);
if (!cfg.highlight) {
hideGuideFocus();
return;
}
var target = document.querySelector(cfg.highlight);
if (!target) {
hideGuideFocus();
return;
}
var pad = typeof cfg.pad === 'number' ? cfg.pad : 8;
var r = target.getBoundingClientRect();
if (r.width < 4 || r.height < 4) {
hideGuideFocus();
return;
}
var cs = window.getComputedStyle(target);
var br = parseInt(cs.borderRadius, 10) || 0;
var radius = Math.max(br + 4, 14);
guideFocus.classList.remove('hidden');
guideFocus.style.left = (r.left - pad) + 'px';
guideFocus.style.top = (r.top - pad) + 'px';
guideFocus.style.width = (r.width + pad * 2) + 'px';
guideFocus.style.height = (r.height + pad * 2) + 'px';
guideFocus.style.borderRadius = radius + 'px';
}
function syncGuideBodyClass() {
if (!guideBar) return;
var open = !guideBar.classList.contains('hidden');
if (open) {
document.body.classList.add('lobby-guide-active');
var cfg = getGuideStepConfig(guideStep);
document.body.classList.toggle('lobby-guide-top-step', cfg.position === 'top');
var hasFocus = !!cfg.highlight;
if (guideDim) guideDim.classList.toggle('hidden', hasFocus);
} else {
document.body.classList.remove('lobby-guide-active');
document.body.classList.remove('lobby-guide-top-step');
if (guideDim) guideDim.classList.add('hidden');
}
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
function showGuideStep(n) {
if (!guideImg || !guideBar) return;
if (n < 1 || n > MAX_GUIDE) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
guideStep = n;
var cfg = getGuideStepConfig(guideStep);
guideImg.src = ML_GUIDE + '/guide-' + (n < 10 ? '0' + n : String(n)) + '.png';
guideImg.alt = 'คำแนะนำ ขั้นที่ ' + n;
guideBar.classList.toggle('lobby-guide-bar--top', cfg.position === 'top');
var nextBtn = document.getElementById('btn-guide-next');
if (nextBtn) {
nextBtn.setAttribute('aria-label', n >= MAX_GUIDE ? 'รับทราบ' : 'ถัดไป');
}
syncGuideBodyClass();
guideImg.onload = function () {
scheduleGuideFocusUpdate();
};
guideImg.onerror = function () {
scheduleGuideFocusUpdate();
};
scheduleGuideFocusUpdate();
}
function initGuide() {
if (!guideBar || !guideImg) return;
try {
if (localStorage.getItem(GUIDE_KEY) === '1') {
guideBar.classList.add('hidden');
syncGuideBodyClass();
return;
}
} catch (e) { /* ignore */ }
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-guide-next')?.addEventListener('click', function () {
if (guideStep >= MAX_GUIDE) {
if (guideBar) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
}
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
showGuideStep(guideStep + 1);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!guideBar || guideBar.classList.contains('hidden')) return;
e.preventDefault();
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
syncGuideBodyClass();
});
window.addEventListener('resize', function () {
if (guideBar && !guideBar.classList.contains('hidden')) {
scheduleGuideFocusUpdate();
}
});
window.addEventListener('orientationchange', function () {
setTimeout(scheduleGuideFocusUpdate, 350);
});
/* ใช้ capture + data-href บน .lobby-footer-center — ลดโอกาสถูกเลเยอร์อื่นแย่งคลิกก่อนถึงปุ่ม */
var footerCenter = document.querySelector('.lobby-footer-center');
if (footerCenter) {
footerCenter.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-href]');
if (!btn || !footerCenter.contains(btn)) return;
var href = btn.getAttribute('data-href');
if (!href) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.location.href = href;
}, true);
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
openLobbyCustomize();
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
try {
var k = 'mlAiChatSession';
var v = localStorage.getItem(k);
if (!v) {
v = 'ml-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(k, v);
}
return v;
} catch (e) {
return 'ml-' + String(Date.now());
}
}
var aiChatCloseBtn = document.getElementById('lobby-ai-chat-close-btn');
var aiChatPanel = document.getElementById('lobby-ai-chat-panel');
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (btnAiChat) btnAiChat.style.display = hidden ? '' : 'none';
if (hidden || !aiChatToggleImg || !aiChatCloseBtn) return;
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
if (aiChatCloseBtn && aiChatPanel) {
aiChatCloseBtn.addEventListener('click', function () {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
});
}
if (btnAiChat && aiChatPanel) {
btnAiChat.addEventListener('click', function () {
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
inp.focus();
} catch (e) { /* ignore */ }
}
} else {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
syncAiChatPanelUi();
}
});
}
syncAiChatPanelUi();
var aiChatForm = document.getElementById('lobby-ai-chat-form');
var aiChatInput = document.getElementById('lobby-ai-chat-input');
var aiChatSendBtn = aiChatForm ? aiChatForm.querySelector('button[type="submit"]') : null;
var aiChatLoadingEl = null;
function appendAiMessage(text, isAi) {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el) return;
if (isAi) removeAiChatLoading();
var div = document.createElement('div');
div.className = 'lobby-ai-chat-msg ' + (isAi ? 'lobby-ai-chat-msg-ai' : 'lobby-ai-chat-msg-user');
div.textContent = (isAi ? 'AI: ' : '') + text;
el.appendChild(div);
el.scrollTop = 1e9;
}
function showAiChatLoading() {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el || aiChatLoadingEl) return;
aiChatLoadingEl = document.createElement('div');
aiChatLoadingEl.className = 'lobby-ai-chat-msg lobby-ai-chat-msg-ai lobby-ai-chat-typing';
aiChatLoadingEl.setAttribute('aria-label', 'กำลังพิมพ์');
aiChatLoadingEl.innerHTML = '<span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span>';
el.appendChild(aiChatLoadingEl);
el.scrollTop = 1e9;
}
function removeAiChatLoading() {
if (aiChatLoadingEl && aiChatLoadingEl.parentNode) {
aiChatLoadingEl.parentNode.removeChild(aiChatLoadingEl);
aiChatLoadingEl = null;
}
}
function setAiChatWaiting(waiting) {
if (aiChatInput) aiChatInput.disabled = waiting;
if (aiChatSendBtn) aiChatSendBtn.disabled = waiting;
if (waiting) showAiChatLoading();
else removeAiChatLoading();
}
if (aiChatForm && aiChatInput) {
aiChatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = (aiChatInput.value || '').trim();
if (!text) return;
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.response != null) appendAiMessage(data.response, true);
else if (data.error) appendAiMessage('[ข้อผิดพลาด: ' + data.error + ']', true);
})
.catch(function () {
appendAiMessage('[ส่งไม่สำเร็จ]', true);
})
.finally(function () {
setAiChatWaiting(false);
});
});
}
var howto = document.getElementById('lobby-howto-overlay');
function openHowto() {
if (howto) howto.classList.remove('hidden');
}
function closeHowto() {
if (howto) howto.classList.add('hidden');
}
/** ปุ่ม ? — คู่มือทีละขั้น ตาม /Main-Lobby/IMAGE/guide */
function openGuideTutorial() {
if (!guideBar || !guideImg) return;
closeHowto();
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-tutorial')?.addEventListener('click', openGuideTutorial);
document.getElementById('btn-howto-got-it')?.addEventListener('click', closeHowto);
howto?.addEventListener('click', function (e) {
if (e.target === howto) closeHowto();
});
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า (เก็บ localStorage) ===== */
var CUSTOMIZE_ASSET = (typeof appPath === 'function' ? appPath('/Game') : '/Game') + '/img/03-5-Customize/';
var customizeOverlay = document.getElementById('lobby-customize-overlay');
/* ---- Phase 2: ทาสีตัวละครจริง (tint layer mask ด้วยสีที่เลือก) ---- */
var lobbyResolvedCharId = '';
var lobbyBakedCharUrl = null;
var LOBBY_LAYER_NAMES = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var lobbySwatchColorCache = {};
function lobbyHasSavedTint() {
try { return !!(localStorage.getItem('lobbyThemeColor') || localStorage.getItem('lobbySkinTone')); } catch (e) { return false; }
}
function lobbyCharLayerUrl(charId, name) {
return BASE + '/img/characters/' + encodeURIComponent(charId) + '_down_idle_layer_' + name + '.png';
}
function lobbyLoadImg(src, cb) {
var img = new Image();
img.onload = function () { cb(img); };
img.onerror = function () { cb(null); };
img.src = src;
}
function lobbySampleSwatchColor(group, idx, cb) {
var key = group + '-' + idx;
if (lobbySwatchColorCache[key]) { cb(lobbySwatchColorCache[key]); return; }
var src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
lobbyLoadImg(src, function (img) {
if (!img || !img.naturalWidth) { cb(null); return; }
try {
var c = document.createElement('canvas');
c.width = 1; c.height = 1;
var x = c.getContext('2d');
x.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, 1, 1);
var d = x.getImageData(0, 0, 1, 1).data;
var rgb = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')';
lobbySwatchColorCache[key] = rgb;
cb(rgb);
} catch (e) { cb(null); }
});
}
function lobbyTintMask(img, color) {
var c = document.createElement('canvas');
c.width = img.naturalWidth; c.height = img.naturalHeight;
var x = c.getContext('2d');
x.drawImage(img, 0, 0);
x.globalCompositeOperation = 'source-in';
x.fillStyle = color;
x.fillRect(0, 0, c.width, c.height);
return c;
}
function lobbyCompositeTinted(charId, cb) {
if (!charId) { cb(null); return; }
// กัน init รันก่อนตัวแปรถูกประกาศ (resolve แบบ sync ตอน localStorage มี char)
if (!lobbySwatchColorCache) lobbySwatchColorCache = {};
if (!LOBBY_LAYER_NAMES) LOBBY_LAYER_NAMES = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var colorIdx = '', skinIdx = '';
try { colorIdx = localStorage.getItem('lobbyThemeColor') || ''; skinIdx = localStorage.getItem('lobbySkinTone') || ''; } catch (e) {}
function withColors(themeColor, skinColor) {
var loaded = {};
var pending = LOBBY_LAYER_NAMES.length;
LOBBY_LAYER_NAMES.forEach(function (name) {
lobbyLoadImg(lobbyCharLayerUrl(charId, name), function (img) {
loaded[name] = img;
if (--pending === 0) finish(themeColor, skinColor, loaded);
});
});
}
function finish(themeColor, skinColor, loaded) {
var ref = null, i;
for (i = 0; i < LOBBY_LAYER_NAMES.length; i++) {
if (loaded[LOBBY_LAYER_NAMES[i]]) { ref = loaded[LOBBY_LAYER_NAMES[i]]; break; }
}
if (!ref) { cb(null); return; }
var c = document.createElement('canvas');
c.width = ref.naturalWidth; c.height = ref.naturalHeight;
var x = c.getContext('2d');
LOBBY_LAYER_NAMES.forEach(function (name) {
var img = loaded[name];
if (!img || !img.naturalWidth) return;
if (themeColor && (name === 'bodyColor' || name === 'hairColor')) x.drawImage(lobbyTintMask(img, themeColor), 0, 0);
else if (skinColor && name === 'headColor') x.drawImage(lobbyTintMask(img, skinColor), 0, 0);
else x.drawImage(img, 0, 0);
});
try { cb(c.toDataURL('image/png')); } catch (e) { cb(null); }
}
var need = 0, themeColor = null, skinColor = null;
function maybeGo() { if (need === 0) withColors(themeColor, skinColor); }
if (colorIdx) { need++; lobbySampleSwatchColor('color', colorIdx, function (rgb) { themeColor = rgb; need--; maybeGo(); }); }
if (skinIdx) { need++; lobbySampleSwatchColor('skin', skinIdx, function (rgb) { skinColor = rgb; need--; maybeGo(); }); }
if (need === 0) withColors(null, null);
}
function applyLobbyCharacterAppearance() {
if (lobbyResolvedCharId && lobbyHasSavedTint()) {
lobbyCompositeTinted(lobbyResolvedCharId, function (dataUrl) {
applyProfileAvatar(dataUrl || lobbyBakedCharUrl);
applyCenterCharacterResolved(dataUrl || lobbyBakedCharUrl);
});
return;
}
applyProfileAvatar(lobbyBakedCharUrl);
applyCenterCharacterResolved(lobbyBakedCharUrl);
}
var CUSTOMIZE_ITEMS = {
face: [
{ idx: 1, price: 0 }, { idx: 2, price: 0 }, { idx: 3, price: 50 }, { idx: 4, price: 50 },
{ idx: 5, price: 50 }, { idx: 6, price: 100 }, { idx: 7, price: 100 }, { idx: 8, price: 200 }
],
hair: [],
cloth: []
};
function lobbyMakeSwatch(group, idx, label) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lobby-customize-swatch';
btn.setAttribute('data-idx', String(idx));
btn.setAttribute('aria-label', label);
var img = document.createElement('img');
img.src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
img.alt = '';
img.decoding = 'async';
btn.appendChild(img);
btn.addEventListener('click', function () { lobbySelectSwatch(group, idx); });
return btn;
}
function lobbySelectSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'lobby-theme-colors' : 'lobby-skin-tones');
if (!wrap) return;
[].forEach.call(wrap.children, function (c) {
c.classList.toggle('is-selected', c.getAttribute('data-idx') === String(idx));
});
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
applyLobbyCharacterAppearance();
}
function lobbyBuildSwatches() {
var colorWrap = document.getElementById('lobby-theme-colors');
var skinWrap = document.getElementById('lobby-skin-tones');
if (colorWrap && !colorWrap.childElementCount) {
for (var i = 1; i <= 8; i++) colorWrap.appendChild(lobbyMakeSwatch('color', i, 'ธีมสี ' + i));
}
if (skinWrap && !skinWrap.childElementCount) {
for (var j = 1; j <= 3; j++) skinWrap.appendChild(lobbyMakeSwatch('skin', j, 'สีผิว ' + j));
}
}
function lobbyRenderCustomizeItems(tab) {
var grid = document.getElementById('lobby-customize-items');
if (!grid) return;
grid.innerHTML = '';
var items = CUSTOMIZE_ITEMS[tab] || [];
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'lobby-customize-empty';
empty.textContent = 'ยังไม่เปิดให้บริการ';
grid.appendChild(empty);
return;
}
var savedKey = 'lobbyItem_' + tab;
var saved = '';
try { saved = localStorage.getItem(savedKey) || ''; } catch (e) {}
items.forEach(function (it) {
var cell = document.createElement('button');
cell.type = 'button';
cell.className = 'lobby-customize-item' + (String(it.idx) === saved ? ' is-selected' : '');
cell.setAttribute('data-idx', String(it.idx));
var img = document.createElement('img');
img.className = 'lobby-customize-item-img';
img.src = CUSTOMIZE_ASSET + tab + '-' + it.idx + '.png';
img.alt = '';
img.decoding = 'async';
cell.appendChild(img);
if (it.price > 0) {
var price = document.createElement('span');
price.className = 'lobby-customize-item-price';
price.textContent = String(it.price);
cell.appendChild(price);
}
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('is-selected'); });
cell.classList.add('is-selected');
try { localStorage.setItem(savedKey, String(it.idx)); } catch (e) {}
});
grid.appendChild(cell);
});
}
function lobbySetCustomizeTab(tab) {
var bar = document.getElementById('lobby-customize-tabbar');
if (bar) {
var map = { face: 'tab-1-face.png', hair: 'tab-2-hair.png', cloth: 'tab-3-cloth.png' };
bar.src = CUSTOMIZE_ASSET + (map[tab] || map.face);
}
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.setAttribute('aria-selected', t.getAttribute('data-tab') === tab ? 'true' : 'false');
});
lobbyRenderCustomizeItems(tab);
}
function openLobbyCustomize() {
if (!customizeOverlay) {
window.location.href = BASE + '/character.html';
return;
}
lobbyBuildSwatches();
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) lobbySelectSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) lobbySelectSwatch('skin', s);
} catch (e) {}
lobbySetCustomizeTab('face');
customizeOverlay.classList.remove('hidden');
customizeOverlay.setAttribute('aria-hidden', 'false');
}
function closeLobbyCustomize() {
if (!customizeOverlay) return;
customizeOverlay.classList.add('hidden');
customizeOverlay.setAttribute('aria-hidden', 'true');
}
function setupLobbyCustomize() {
if (!customizeOverlay) return;
document.getElementById('lobby-customize-close')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-backdrop')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-confirm')?.addEventListener('click', function () {
closeLobbyCustomize();
toast('บันทึกการแต่งตัวแล้ว');
});
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.addEventListener('click', function () { lobbySetCustomizeTab(t.getAttribute('data-tab')); });
});
}
setupLobbyCustomize();
})();