Files
justice/www/html/Main-Lobby/lobby.js
T
2026-07-31 03:36:58 +00:00

1095 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 }, // guide-01 ทักทาย
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 }, // guide-02 ห้องแต่งตัว
{ position: 'bottom', highlight: '#btn-daily', pad: 10 }, // guide-03 DAILY
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 }, // guide-04 START MISSION / QUIZ BATTLE
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 }, // guide-05 เทพความรู้
];
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 จริง ถือเป็นว่าง (fallback ไปตัวจริงจาก API) กัน 404 รัว
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)) : '';
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
var DISPLAY_NAME_STORAGE_KEY = 'roomLobbyDisplayName';
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 */ });
}
function lbEscapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
/** วาดกระดานผู้นำจากข้อมูลจริง (top = [{rank,name,score,coins}]) */
function renderLeaderboardRows(top) {
var listEl = document.querySelector('.lobby-leaderboard-list');
if (!listEl || !Array.isArray(top) || !top.length) return; /* ว่าง → คงตัวอย่างเดิมไว้ */
var html = '';
for (var i = 0; i < top.length; i++) {
var r = top[i] || {};
var rank = parseInt(r.rank, 10) || (i + 1);
var name = lbEscapeHtml(r.name || 'ผู้เล่น');
var score = Math.max(0, parseInt(r.score, 10) || 0);
var caseName = lbEscapeHtml((r.caseName || '').toString());
var caseLabel = caseName ? ('(คดี : ' + caseName + ')') : '';
var head = rank <= 3
? '<img src="IMAGE/leaderboard-' + rank + '.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">'
: '<span class="lobby-rank-num">' + rank + '</span>';
html += '<li>' + head +
'<div class="lobby-rank-info">' +
'<span class="lobby-rank-name">' + name + '</span>' +
'<span class="lobby-rank-case">' + caseLabel + '</span>' +
'</div>' +
'<span class="lobby-rank-score">' + score + '</span>' +
'</li>';
}
listEl.innerHTML = html;
scheduleLeaderboardPlacement();
}
/** ดึงกระดานผู้นำจากเซิร์ฟเวอร์ (อันดับตามคะแนนสะสม high score) */
function fetchAndRenderLeaderboard() {
var base = (typeof appPath === 'function' ? appPath('/Admin/api/leaderboard.php') : '/Admin/api/leaderboard.php');
var url = base + '?limit=50&playerKey=' + encodeURIComponent(ensurePlayerKey());
fetch(url, { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok || !Array.isArray(d.top)) return;
renderLeaderboardRows(d.top);
})
.catch(function () { /* ออฟไลน์ → คงตัวอย่างเดิม */ });
}
/** สีที่เลือกไว้ใน localStorage (เครื่องนี้) — index theme 18 / skin 13 */
function getLocalLobbyStyle() {
var theme = 0, skin = 0;
try {
theme = parseInt(localStorage.getItem('lobbyThemeColor') || '', 10) || 0;
skin = parseInt(localStorage.getItem('lobbySkinTone') || '', 10) || 0;
} catch (e) { /* ignore */ }
return { theme: theme, skin: skin };
}
function mirrorLobbyStyleToLocal(theme, skin) {
try {
if (theme >= 1 && theme <= 8) localStorage.setItem('lobbyThemeColor', String(theme));
if (skin >= 1 && skin <= 3) localStorage.setItem('lobbySkinTone', String(skin));
} catch (e) { /* ignore */ }
}
/** ดึงสีที่เลือกจากเซิร์ฟเวอร์ (cross-device) ตาม jdPlayerKey */
function fetchServerLobbyStyle(cb) {
var key;
try { key = ensurePlayerKey(); } catch (e) { key = ''; }
if (!key) { cb(null); return; }
var base = (typeof appPath === 'function' ? appPath('/Admin/api/player-lobby-style.php') : '/Admin/api/player-lobby-style.php');
fetch(base + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit', cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || !d.ok) { cb(null); return; }
cb({ theme: parseInt(d.colorThemeIndex, 10) || 0, skin: parseInt(d.skinToneIndex, 10) || 0 });
})
.catch(function () { cb(null); });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
fetchAndRenderLeaderboard();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
// วาด cache ทันทีกันจอว่าง แล้วค่อย recompute จากสีจริง (cross-device) ทับ
if (idleCached) applyCharAssets(idleCached);
function fallbackToSprite() {
if (idleCached) return; // วาดไปแล้ว
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
}
// สีจาก server เป็นแหล่งความจริงข้ามเครื่อง, ถ้าไม่มีค่อยใช้ local
fetchServerLobbyStyle(function (server) {
var local = getLocalLobbyStyle();
var theme = (server && server.theme) || local.theme || 0;
var skin = (server && server.skin) || local.skin || 0;
if (theme >= 1 && theme <= 8) mirrorLobbyStyleToLocal(theme, skin);
// ยังไม่เคยเลือกสี → ใช้รูปตามเดิม (ไม่ recolor)
if (!(theme >= 1 && theme <= 8) || typeof window.jdComposeTintedCharacter !== 'function') {
fallbackToSprite();
return;
}
window.jdComposeTintedCharacter(id, theme, skin).then(function (dataUrl) {
if (dataUrl && typeof dataUrl === 'string' && dataUrl.indexOf('data:image/') === 0) {
try { localStorage.setItem(LOBBY_IDLE_DOWN_PREFIX + id, dataUrl); } catch (e) { /* ignore */ }
applyCharAssets(dataUrl);
} else {
fallbackToSprite();
}
}).catch(function () { fallbackToSprite(); });
});
});
}
function getProfileDisplayName() {
if (typeof window.jdReadDisplayName === 'function') {
return window.jdReadDisplayName();
}
var fromRoomLobby = '';
var fromPlayerName = '';
try {
fromRoomLobby = (localStorage.getItem(DISPLAY_NAME_STORAGE_KEY) || '').trim();
fromPlayerName = (localStorage.getItem('playerName') || '').trim();
} catch (e) { /* ignore */ }
return fromRoomLobby || fromPlayerName || 'MONE';
}
function applyProfileTexts() {
var name = getProfileDisplayName();
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');
var trackEl = document.getElementById('lobby-leaderboard-scroll-track');
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';
if (trackEl) trackEl.style.display = 'none';
return;
}
if (trackEl) trackEl.style.display = 'block';
thumbEl.style.display = 'block';
var trackHeight = (trackEl && trackEl.clientHeight) ? trackEl.clientHeight : 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';
}
function bindLeaderboardWheelSupport() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.wheelSupportBound === '1') return;
scrollEl.addEventListener('wheel', function (e) {
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) return;
scrollEl.scrollTop += e.deltaY;
syncLeaderboardScrollbarThumb();
e.preventDefault();
}, { passive: false });
scrollEl.dataset.wheelSupportBound = '1';
}
function bindLeaderboardThumbDrag() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
var trackEl = document.getElementById('lobby-leaderboard-scroll-track');
if (!scrollEl || !thumbEl || !trackEl || thumbEl.dataset.dragBound === '1') return;
var dragging = false;
var dragOffsetY = 0;
function getThumbMetrics() {
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var thumbHeight = thumbEl.clientHeight || parseFloat(thumbEl.style.height) || 18;
var trackHeight = trackEl.clientHeight || scrollEl.clientHeight || 1;
var maxTranslate = Math.max(topInset, trackHeight - thumbHeight);
var travel = Math.max(1, maxTranslate - topInset);
return { topInset: topInset, thumbHeight: thumbHeight, trackHeight: trackHeight, maxTranslate: maxTranslate, travel: travel };
}
function stopDrag() {
if (!dragging) return;
dragging = false;
thumbEl.classList.remove('is-dragging');
document.body.classList.remove('lobby-leaderboard-dragging');
}
thumbEl.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) return;
var metrics = getThumbMetrics();
var thumbRect = thumbEl.getBoundingClientRect();
dragOffsetY = Math.max(0, Math.min(metrics.thumbHeight, e.clientY - thumbRect.top));
dragging = true;
thumbEl.classList.add('is-dragging');
document.body.classList.add('lobby-leaderboard-dragging');
e.preventDefault();
e.stopPropagation();
});
window.addEventListener('mousemove', function (e) {
if (!dragging) return;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
stopDrag();
return;
}
var metrics = getThumbMetrics();
var trackRect = trackEl.getBoundingClientRect();
var pointerY = e.clientY - trackRect.top;
var nextTranslate = pointerY - dragOffsetY;
nextTranslate = Math.max(metrics.topInset, Math.min(metrics.maxTranslate, nextTranslate));
var ratio = (nextTranslate - metrics.topInset) / metrics.travel;
scrollEl.scrollTop = ratio * maxScroll;
syncLeaderboardScrollbarThumb();
e.preventDefault();
});
window.addEventListener('mouseup', stopDrag);
window.addEventListener('blur', stopDrag);
thumbEl.dataset.dragBound = '1';
}
function bindLeaderboardDragScroll() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.dragScrollBound === '1') return;
var dragging = false;
var moved = false;
var startY = 0;
var startScrollTop = 0;
function stopDrag() {
if (!dragging) return;
dragging = false;
window.setTimeout(function () { moved = false; }, 0);
document.body.classList.remove('lobby-leaderboard-dragging');
scrollEl.classList.remove('is-dragging');
}
scrollEl.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) return;
dragging = true;
moved = false;
startY = e.clientY;
startScrollTop = scrollEl.scrollTop;
document.body.classList.add('lobby-leaderboard-dragging');
scrollEl.classList.add('is-dragging');
e.preventDefault();
});
window.addEventListener('mousemove', function (e) {
if (!dragging) return;
var deltaY = e.clientY - startY;
if (Math.abs(deltaY) > 2) moved = true;
scrollEl.scrollTop = startScrollTop - deltaY;
syncLeaderboardScrollbarThumb();
e.preventDefault();
});
window.addEventListener('mouseup', stopDrag);
window.addEventListener('blur', stopDrag);
scrollEl.addEventListener('mouseleave', function (e) {
if (!dragging) return;
if (e.buttons === 0) stopDrag();
});
scrollEl.addEventListener('click', function (e) {
if (!moved) return;
e.preventDefault();
e.stopPropagation();
}, true);
scrollEl.dataset.dragScrollBound = '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 () {
if (typeof window.openCustomizePopup === 'function') {
window.openCustomizePopup();
return;
}
window.location.href = BASE + '/character.html';
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
if (typeof window.openDailyPopup === 'function') {
window.openDailyPopup();
return;
}
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
if (typeof window.openProfilePopup === 'function') {
window.openProfilePopup();
return;
}
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();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
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' || e.key === DISPLAY_NAME_STORAGE_KEY) {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
})();