minigame 2 add more design 2.9

This commit is contained in:
2026-05-06 19:40:59 +00:00
parent f1664b6aec
commit 2ce8cfe199
15 changed files with 3368 additions and 676 deletions
+47 -27
View File
@@ -2,23 +2,20 @@
'use strict';
function checkLoginStatus() {
const isLoggedIn = localStorage.getItem('isLoggedIn');
var isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn || isLoggedIn !== 'true') {
window.location.href = '/Login/';
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLoginStatus()) {
return;
}
if (!checkLoginStatus()) return;
const BASE = '/Game';
/** mapId ฉาก LobbyA (/Game/data/maps/mlsbbxfe.json — ไม่ใช่ mn8nx46h ที่เป็น LobbyB) */
const LOBBY_A_MAP_ID = 'mlsbbxfe';
/** ผู้เล่นจริง + Bot รวมกัน = 6 ที่นั่ง (เลขปุ่ม = จำนวนผู้เล่น, ที่เหลือเป็น Bot) */
const LOBBY_TOTAL_SLOTS = 6;
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var LOBBY_A_MAP_ID = 'mlsbbxfe';
var LOBBY_TOTAL_SLOTS = 6;
var selectedSlot = 3;
var isPrivate = false;
@@ -55,7 +52,7 @@
}
btnBack?.addEventListener('click', function () {
window.location.href = '/Main-Menu/';
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
inputPrivatePin?.addEventListener('input', function (e) {
@@ -63,6 +60,16 @@
updateConfirmButtonEnabled();
});
function updateModeImages() {
[tabPublic, tabPrivate].forEach(function (btn) {
if (!btn) return;
var img = btn.querySelector('.create-mode-btn-img');
if (!img) return;
var isActive = btn.classList.contains('create-mode-btn--active');
img.src = isActive ? btn.getAttribute('data-img-h') : btn.getAttribute('data-img');
});
}
function setTab(privateMode) {
isPrivate = privateMode;
tabPublic?.classList.toggle('create-mode-btn--active', !privateMode);
@@ -70,21 +77,30 @@
tabPublic?.setAttribute('aria-selected', privateMode ? 'false' : 'true');
tabPrivate?.setAttribute('aria-selected', privateMode ? 'true' : 'false');
if (privateBlock) privateBlock.classList.toggle('create-view--hidden', !privateMode);
updateModeImages();
updateConfirmButtonEnabled();
}
tabPublic?.addEventListener('click', function () { setTab(false); });
tabPrivate?.addEventListener('click', function () { setTab(true); });
function selectSlot(n) {
selectedSlot = Math.min(6, Math.max(1, parseInt(n, 10) || 3));
var buttons = playerBtnsWrap ? playerBtnsWrap.querySelectorAll('.create-num-btn') : [];
function updatePlayerImages() {
if (!playerBtnsWrap) return;
var buttons = playerBtnsWrap.querySelectorAll('.create-num-btn');
buttons.forEach(function (btn) {
var v = parseInt(btn.getAttribute('data-slot'), 10);
var on = v === selectedSlot;
btn.classList.toggle('create-num-btn--active', on);
var img = btn.querySelector('.create-num-btn-img');
if (img) {
img.src = on ? btn.getAttribute('data-img-h') : btn.getAttribute('data-img');
}
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
});
}
function selectSlot(n) {
selectedSlot = Math.min(6, Math.max(1, parseInt(n, 10) || 3));
updatePlayerImages();
updateSummary();
}
@@ -94,12 +110,20 @@
for (var i = 1; i <= 6; i++) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'create-num-btn' + (i === selectedSlot ? ' create-num-btn--active' : '');
b.className = 'create-num-btn';
b.setAttribute('data-slot', String(i));
b.setAttribute('data-img', 'IMAGE/player-' + i + '.png');
b.setAttribute('data-img-h', 'IMAGE/player-' + i + '-h.png');
b.setAttribute('aria-label', 'เลือก ' + i + ' ผู้เล่น รวม ' + LOBBY_TOTAL_SLOTS + ' ที่นั่ง (Bot ' + botCountForSlot(i) + ' ตัว)');
b.setAttribute('title', i + ' ผู้เล่น + ' + botCountForSlot(i) + ' Bot');
b.setAttribute('aria-pressed', i === selectedSlot ? 'true' : 'false');
b.textContent = String(i);
var img = document.createElement('img');
img.className = 'create-num-btn-img';
img.alt = String(i);
img.decoding = 'async';
img.src = i === selectedSlot ? 'IMAGE/player-' + i + '-h.png' : 'IMAGE/player-' + i + '.png';
b.appendChild(img);
b.addEventListener('click', function () {
selectSlot(this.getAttribute('data-slot'));
});
@@ -148,7 +172,7 @@
isPrivate: isPrivate,
maxPlayers: maxPlayersForSlot(selectedSlot),
};
fetch(BASE + '/api/spaces', {
fetch(SERVER + '/api/spaces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
@@ -169,15 +193,12 @@
return;
}
if (isPrivate) {
try {
localStorage.setItem('roomPinHint_' + sid, getPrivatePinDigits());
} catch (e) { /* ignore */ }
try { localStorage.setItem('roomPinHint_' + sid, getPrivatePinDigits()); } catch (e) {}
}
try {
localStorage.setItem('lastCreatedSpaceId', sid);
localStorage.setItem('lastCreatedSpaceName', name);
} catch (e) { /* ignore */ }
/* room-lobby ใช้ ?space= เป็น spaceId ของห้องที่เพิ่งสร้าง ไม่ใช่ mapId (LobbyA = mapId ใน POST เท่านั้น) */
} catch (e) {}
var lobbyUrl = BASE + '/room-lobby.html?space=' + encodeURIComponent(sid) + '&nick=' + encodeURIComponent(getNick());
lobbyUrl += '&displayRoom=' + encodeURIComponent(name);
try {
@@ -185,9 +206,8 @@
var prevSpaceName = localStorage.getItem('prevSpaceName');
if (caseId) lobbyUrl += '&caseId=' + encodeURIComponent(caseId);
if (prevSpaceName) lobbyUrl += '&prevSpaceName=' + encodeURIComponent(prevSpaceName);
} catch (e) { /* ignore */ }
/* replace = ลบ Create Room ออกจาก history — Back จาก Loading กลับ Main-Menu */
window.location.replace('/Loading/?redirect=' + encodeURIComponent(lobbyUrl));
} catch (e) {}
window.location.replace((typeof appPath === 'function' ? appPath('/Loading/') : '/Loading/') + '?redirect=' + encodeURIComponent(lobbyUrl));
})
.catch(function () {
if (statusEl) statusEl.textContent = 'เชื่อมต่อเซิร์ฟเวอร์ไม่ได้';
+77 -28
View File
@@ -7,64 +7,78 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.021">
<link rel="stylesheet" href="style.css?v=1.006">
</head>
<body>
<div class="create-bg" aria-hidden="true">
<div class="create-bg-grid"></div>
<img src="/Main-Menu/bg.png" alt="" class="create-bg-img">
<img src="../Main-Menu/bg.png" alt="" class="create-bg-img">
</div>
<div class="create-char-layer" aria-hidden="true">
<img src="/Main-Menu/char-main.png" alt="" class="create-char-img">
<img src="../Main-Menu/char-main.png" alt="" class="create-char-img">
</div>
<div class="create-vignette" aria-hidden="true"></div>
<div class="create-ui">
<button type="button" class="menu-btn-back" id="btn-back" aria-label="ย้อนกลับ">
<img src="/Main-Menu/IMAGE/btn-back.png" alt="ย้อนกลับ" class="menu-btn-back-img" role="presentation" decoding="async">
<img src="../Main-Menu/IMAGE/btn-back.png" alt="ย้อนกลับ" class="menu-btn-back-img" decoding="async">
</button>
<main class="create-content">
<h1 class="create-title">สร้างห้องเกม</h1>
<img src="IMAGE/txt-createroom.png" alt="สร้างห้องเกม" class="create-title-img" decoding="async">
<img src="IMAGE/line.png" alt="" class="create-title-line" aria-hidden="true" decoding="async">
<section class="create-panel" aria-label="ฟอร์มสร้างห้อง">
<div class="create-field">
<label class="create-label" for="create-room-name">ชื่อห้อง :</label>
<!-- ชื่อห้อง -->
<div class="create-field create-field--inline create-field--room">
<img src="IMAGE/txt-roomname.png" alt="ชื่อห้อง :" class="create-label-img" decoding="async">
<div class="create-input-shell">
<input type="text" id="create-room-name" class="create-input" maxlength="48" autocomplete="off" spellcheck="false" placeholder="ตั้งชื่อห้อง">
<input type="text" id="create-room-name" class="create-input" maxlength="48" autocomplete="off" spellcheck="false" placeholder="ตั้งชื่อห้อง" aria-label="ชื่อห้อง">
</div>
</div>
<div class="create-field">
<span class="create-label" id="mode-label">โหมด :</span>
<div class="create-mode-row" role="tablist" aria-labelledby="mode-label">
<button type="button" class="create-mode-btn create-mode-btn--active" id="tab-public" role="tab" aria-selected="true">ห้องสาธารณะ</button>
<button type="button" class="create-mode-btn" id="tab-private" role="tab" aria-selected="false">ห้องส่วนตัว</button>
<img src="IMAGE/line.png" alt="" class="create-line" aria-hidden="true" decoding="async">
<!-- โหมด -->
<div class="create-field create-field--inline create-field--mode">
<img src="IMAGE/txt-mode.png" alt="โหมด :" class="create-label-img" decoding="async">
<div class="create-mode-row" role="tablist" aria-label="โหมดห้อง">
<button type="button" class="create-mode-btn create-mode-btn--active" id="tab-public" role="tab" aria-selected="true" data-img="IMAGE/btn-publish.png" data-img-h="IMAGE/btn-publish-h.png">
<img src="IMAGE/btn-publish-h.png" alt="ห้องสาธารณะ" class="create-mode-btn-img" decoding="async">
</button>
<button type="button" class="create-mode-btn" id="tab-private" role="tab" aria-selected="false" data-img="IMAGE/btn-private.png" data-img-h="IMAGE/btn-private-h.png">
<img src="IMAGE/btn-private.png" alt="ห้องส่วนตัว" class="create-mode-btn-img" decoding="async">
</button>
</div>
</div>
<!-- PASSWORD (ซ่อน default) -->
<div id="create-private-block" class="create-private-block create-view--hidden" role="tabpanel">
<div class="create-pass-row">
<div>
<span class="create-label create-label--inline">PASSWORD :</span>
<span class="create-pass-sub">(รหัสผ่าน 4 ตัว)</span>
</div>
<div class="create-input-shell create-input-shell--pin">
<input type="text" id="create-private-pin" class="create-input create-input--pin" placeholder="0000" maxlength="4" inputmode="numeric" autocomplete="off" aria-label="รหัสผ่าน 4 หลัก">
</div>
<img src="IMAGE/txt-password.png" alt="PASSWORD : (รหัสผ่าน 4 ตัว)" class="create-label-img create-label-img--password" decoding="async">
<div class="create-input-shell create-input-shell--pin">
<input type="text" id="create-private-pin" class="create-input create-input--pin" placeholder="0000" maxlength="4" inputmode="numeric" autocomplete="off" aria-label="รหัสผ่าน 4 หลัก">
</div>
</div>
<div class="create-field create-field--players">
<span class="create-label" id="players-label">จำนวนผู้เล่น :</span>
<div class="create-num-grid" id="create-player-btns" role="group" aria-labelledby="players-label"></div>
<img src="IMAGE/line.png" alt="" class="create-line" aria-hidden="true" decoding="async">
<!-- จำนวนผู้เล่น -->
<div class="create-field create-field--inline create-field--players">
<img src="IMAGE/txt-players.png" alt="จำนวนผู้เล่น :" class="create-label-img" decoding="async">
<div class="create-num-grid" id="create-player-btns" role="group" aria-label="จำนวนผู้เล่น"></div>
</div>
<div class="create-summary" id="create-summary" aria-live="polite">สรุปจำนวน : 3 ผู้เล่น + 3 Bot</div>
<img src="IMAGE/line.png" alt="" class="create-line" aria-hidden="true" decoding="async">
<!-- สรุป -->
<div class="create-summary-wrap">
<div class="create-summary" id="create-summary" aria-live="polite">สรุปจำนวน : 3 ผู้เล่น + 3 Bot</div>
</div>
<!-- ยืนยัน -->
<button type="button" class="create-confirm" id="btn-confirm" aria-label="ยืนยันสร้างห้อง">
<span class="create-confirm-glow"></span>
<span class="create-confirm-text">ยืนยัน</span>
<img src="IMAGE/btn-confirm.png" alt="ยืนยัน" class="create-confirm-img" decoding="async">
</button>
<p class="create-status" id="create-status" role="status"></p>
@@ -72,6 +86,41 @@
</main>
</div>
<script src="create-room.js?v=0.028"></script>
<script src="../app-base.js"></script>
<script src="create-room.js?v=1.004"></script>
<script>
(function(){
var content = document.querySelector('.create-content');
if(!content) return;
var tid = 0;
function autoFit(){
clearTimeout(tid);
tid = setTimeout(function(){
var isLand = window.matchMedia('(orientation:landscape) and (max-width:1200px)').matches
|| window.matchMedia('(orientation:landscape) and (max-height:620px)').matches;
if(!isLand){ content.style.zoom = ''; return; }
content.style.zoom = '1';
requestAnimationFrame(function(){
var vpH = window.innerHeight;
var rect = content.getBoundingClientRect();
var need = rect.top + rect.height;
if(need > vpH){
var s = Math.max(0.4, (vpH * 0.97) / need);
content.style.zoom = String(Math.floor(s * 1000) / 1000);
} else {
content.style.zoom = '1';
}
});
}, 30);
}
autoFit();
window.addEventListener('resize', autoFit);
window.addEventListener('orientationchange', function(){ setTimeout(autoFit, 300); });
var panel = document.querySelector('.create-panel');
if(panel){
new MutationObserver(function(){ setTimeout(autoFit, 80); }).observe(panel, {childList:true, subtree:true, attributes:true, attributeFilter:['class']});
}
})();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+32 -23
View File
@@ -1177,16 +1177,22 @@
}
/**
* าลบจาก screen Y ใน worldToScreen ใหบลอก/กรดเลอนตามฉาก BG stackTowerScrollBgPx เลอน ( drawStackTowerScrollBgFullCanvas)
* าลบจาก screen Y ใน worldToScreen งก drawStackTowerScrollBgFullCanvas (auto + boost ตามความส + post-50)
*/
function getStackTowerWorldLayerScrollScreenOffsetYPlay(zPan) {
if (!isStackTowerMissionUiMapPlay() || !stackTowerScrollBgDrawActive() || !stackTowerScrollBgImagesReady()) return 0;
if (stackTowerScrollBgOff) return 0;
if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0;
const zPn = Number(zPan) > 0 ? zPan : zoom;
const zRef = stackTowerScrollBgZoomRefPlay > 0 ? stackTowerScrollBgZoomRefPlay : zPn;
const bgZm = Math.max(0.28, Math.min(4.25, zPn / zRef));
const raw = stackTowerScrollBgPx / bgZm;
return stackTowerScrollBgFlowDown ? -raw : raw;
const chPx = Math.max(1, Math.round(canvas && canvas.height ? canvas.height : 720));
const boostScreen = getStackTowerBgScrollHeightBoostPx() * zPn;
const post50Screen = getStackTowerPost50BgScrollExtraPxPlay(chPx);
let autoSigned = 0;
if (stackTowerScrollBgDrawActive() && stackTowerScrollBgImagesReady() && !stackTowerScrollBgOff) {
autoSigned = stackTowerScrollBgFlowDown ? -stackTowerScrollBgPx : stackTowerScrollBgPx;
}
const totalScreen = autoSigned + boostScreen + post50Screen;
return totalScreen / bgZm;
}
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
@@ -4498,7 +4504,7 @@
const L = m.layers[li];
const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * getStackTowerBlockWorldScalePlay();
const cxW = L.cx * tileSize;
const yb = floorY - (li + 1 - hidPop) * layerWorldH;
const yb = floorY - (li + 1) * layerWorldH;
const xl = cxW - lw / 2;
const [sx, sy] = worldToScreen(xl, yb);
const drawW = lw * zoom;
@@ -4690,8 +4696,7 @@
const u = Math.max(0, Math.min(1, (nt - t0) / dur));
const alpha = 1 - u * u;
const worldCx = m.topCenterX * tileSize;
const hidHm = getStackTowerVisualHiddenLayerCountPlay();
const stackTopY = floorY - (nLay - hidHm) * layerWorldH;
const stackTopY = floorY - nLay * layerWorldH;
const worldCy = stackTopY - tileSize * 1.55 - u * tileSize * 0.5;
const [sx, sy] = worldToScreen(worldCx, worldCy);
const im = ensureStackTowerHeartMinusImgPlay();
@@ -8658,7 +8663,10 @@
return Math.min(lh * 20, tiers * lh * 0.98);
}
/** progress ≥50% + ชั้นพอ: ไม่วาดครึ่งล่างของกอง — ซ้อนเฉพาะครึ่งบนจากพื้น (เฉพาะภาพ ไม่แก้ logic เกม) */
/**
* progress 50% + นพอ: ไมวาดชนลาง (าม draw) ตำแหน Y งใชนจร (floorY-(i+1)*h)
* เพอไมใหกองลอยหลดจากฐานแมปเมอเทยบกร
*/
function getStackTowerVisualHiddenLayerCountPlay() {
if (!isStackTowerMissionHudActivePlay() || !stackMini) return 0;
const p = Number(stackMini.progressPct) || 0;
@@ -8668,11 +8676,15 @@
return Math.floor(n / 2);
}
/** world px ตามความสูงหอ (mnn93hpi) — ใช้เลื่อนกล้องให้บล็อก/ฉากไปกับมุมมอง */
/**
* world px ตามความสงหอจร (mnn93hpi) z แลวไดกเซลจอสำหรบเลอนแถบ BG
* ใช n × layerWorldH (ความสงชนตามบลอก) คลมดวย maxPx จากแมป
*/
function getStackTowerBgScrollHeightBoostPx() {
if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0;
const cfg = getStackTowerCameraFollowPlayConfig();
if (!cfg.enabled || cfg.pxPerLayer <= 0) return 0;
const lh = Math.max(10, Number(stackMini.layerWorldH) || Math.max(14, (tileSize || 32) * 0.31));
const n = stackMini.layers ? stackMini.layers.length : 0;
let layerCount = n;
if (stackFall) {
@@ -8680,10 +8692,10 @@
const u = Math.min(1, Math.max(0, (performance.now() - stackFall.t0) / dur));
layerCount = n + u * 0.9;
}
return Math.min(cfg.maxPx, layerCount * cfg.pxPerLayer);
return Math.min(cfg.maxPx, layerCount * lh);
}
/** กล้อง stack: จุดกลางจาก land / release ของแมปเท่านั้น (Tower ไม่เลื่อน cy ตามหอ / post-50 / ตามบล็อก) */
/** กล้อง stack: จุดกลางจาก land / release ของแมป (Tower ไม่เลื่อน cy ตามกอง) */
function getStackCameraCentersPx() {
const w = mapData.width || 20, h = mapData.height || 15;
const ts = tileSize;
@@ -9297,14 +9309,13 @@
const swingLiftDraw = getStackTowerSwingLiftWorldPx(m.layers.length, layerWorldH);
const swingDrawY = m.swingWorldY - swingLiftDraw;
const hidLayers = towerHudLive ? getStackTowerVisualHiddenLayerCountPlay() : 0;
const visualLiftWorld = hidLayers * layerWorldH;
const nLay = m.layers.length;
for (let i = hidLayers; i < nLay; i++) {
const L = m.layers[i];
const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * wMul;
const cxW = L.cx * tileSize;
const yb = floorY - (i + 1 - hidLayers) * layerWorldH;
const yb = floorY - (i + 1) * layerWorldH;
const xl = cxW - lw / 2;
const [sx, sy] = worldToScreen(xl, yb);
const drawW = lw * zoom;
@@ -9331,7 +9342,7 @@
const Ltop = m.layers[ti];
const lwT = (Ltop.w != null ? Ltop.w : m.initialWidthTiles) * tileSize * wMul;
const cxWT = Ltop.cx * tileSize;
const ybT = floorY - (ti + 1 - hidLayers) * layerWorldH;
const ybT = floorY - (ti + 1) * layerWorldH;
const xlT = cxWT - lwT / 2;
const [sxT, syT] = worldToScreen(xlT, ybT);
const drawWT = lwT * zoom;
@@ -9356,8 +9367,7 @@
const xSlide = rotSign * ue * tileSize * 0.62;
const cxW = s.xLeft0 + s.twWorld / 2 + xSlide;
const cyW = s.yTop0 + s.stripH * 0.5 + yFall;
const cyWDraw = isStackTowerMissionUiMapPlay() ? cyW + visualLiftWorld : cyW;
const [cxs, cys] = worldToScreen(cxW, cyWDraw);
const [cxs, cys] = worldToScreen(cxW, cyW);
const drawW = s.twWorld * zoom;
const drawH = s.stripH * zoom;
const hue = (m.layers.length * 47) % 360;
@@ -9371,9 +9381,8 @@
} else {
const twWorld = m.widthTiles * tileSize * wMul;
const drawStripPx = stripH * zoom;
const swingDrawYDraw = isStackTowerMissionUiMapPlay() ? swingDrawY + visualLiftWorld : swingDrawY;
const cableTopY = isStackTowerMissionUiMapPlay()
? (swingDrawYDraw - tileSize * 6)
? (swingDrawY - tileSize * 6)
: Math.max(0, swingDrawY - tileSize * 6);
const craneX = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize;
let blockLeftWorld;
@@ -9385,12 +9394,11 @@
const u = stackEaseDropPhys(t, stackFall.sloppyN);
blockLeftWorld = stackFall.xLeft0 + (stackFall.xLeft1 - stackFall.xLeft0) * u;
blockTopWorld = stackFall.y0 + (stackFall.y1 - stackFall.y0) * u;
if (isStackTowerMissionUiMapPlay()) blockTopWorld += visualLiftWorld;
fallTiltRad = stackFall.tiltMax * Math.sin(Math.PI * u);
} else {
const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize;
blockLeftWorld = swingXW - twWorld / 2;
blockTopWorld = isStackTowerMissionUiMapPlay() ? swingDrawYDraw : swingDrawY;
blockTopWorld = swingDrawY;
}
const cxMid = blockLeftWorld + twWorld / 2;
const [tcx, tcy] = worldToScreen(craneX, cableTopY);
@@ -9398,7 +9406,7 @@
let slingBx = bcx;
let slingBy = bcy;
if (stackFall && stackFall.releaseAttachWorldX != null) {
const attY = stackFall.releaseAttachWorldY + (isStackTowerMissionUiMapPlay() ? visualLiftWorld : 0);
const attY = stackFall.releaseAttachWorldY;
const p = worldToScreen(stackFall.releaseAttachWorldX, attY);
slingBx = p[0];
slingBy = p[1];
@@ -16381,7 +16389,8 @@
}
if (isStackTowerMissionUiMapPlay() && stackMini && canvas && zDraw > 0) {
const nLay0 = stackMini.layers ? stackMini.layers.length : 0;
if (nLay0 === 0) {
const hasMotion = !!(stackFall || stackMini.settling);
if (nLay0 === 0 && !hasMotion) {
const rawFrac = mapData && mapData.stackTowerFloorScreenFrac;
const frac = Number(rawFrac);
/** ค่าน้อย = พื้นขึ้นบนจอ (จุดเริ่มตรงฐานศิลป์) — แมป override ได้ที่ stackTowerFloorScreenFrac */
+1 -1
View File
@@ -3202,7 +3202,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0376"></script>
<script src="js/play.js?v=0.0381"></script>
<div class="version-tag">v —</div>
</body>
</html>
+4 -2
View File
@@ -25,7 +25,7 @@
<div class="loading-bar-fill" id="loading-bar-fill"></div>
<span class="loading-pct" id="loading-pct">0%</span>
</div>
<p class="loading-status">กำลังเข้าสู่การสืบสวน...</p>
<img src="IMAGE/loading-txt.png" alt="กำลังเข้าสู่การสืบสวน..." class="loading-status-img" decoding="async">
</div>
<div class="loading-carousel">
<button type="button" class="loading-arrow loading-arrow--prev" id="btn-prev" aria-label="สไลด์ก่อนหน้า">
@@ -40,7 +40,9 @@
</div>
<p class="loading-page-ind" id="slide-page">1 / 7</p>
<button type="button" class="loading-skip" id="btn-skip">ข้าม</button>
<button type="button" class="loading-debug-freeze" id="btn-debug-freeze">Freeze: OFF</button>
</main>
<script src="loading.js?v=0.02"></script>
<script src="../app-base.js"></script>
<script src="loading.js?v=0.03"></script>
</body>
</html>
+76 -11
View File
@@ -3,40 +3,58 @@
var params = new URLSearchParams(window.location.search);
var redirect = params.get('redirect');
var debugMode = params.get('debug') === '1' || localStorage.getItem('debugLoadingFreeze') === '1';
if (debugMode) document.body.classList.add('loading-debug-mode');
function ap(p) {
return typeof appPath === 'function' ? appPath(p) : p;
}
if (!redirect) {
window.location.replace('/Main-Menu/');
return;
redirect = ap('/Main-Menu/');
}
var safe = false;
try {
var u = new URL(redirect, window.location.origin);
if (u.origin === window.location.origin && /^\/Game\/room-lobby\.html$/i.test(u.pathname)) {
var suffix = '/Game/room-lobby.html';
if (u.origin === window.location.origin && (u.pathname === suffix || u.pathname.endsWith(suffix))) {
safe = true;
}
} catch (e) {
safe = false;
}
if (!safe) {
window.location.replace('/Game/lobby.html');
return;
redirect = ap('/Game/lobby.html');
}
var totalSlides = 7;
var slides = [
't-01.png',
't-02.png',
't-03.png',
't-04.png',
't-05.png',
't-06.png',
't-07.png'
];
var totalSlides = slides.length;
var idx = 0;
var slideImg = document.getElementById('slide-img');
var pageEl = document.getElementById('slide-page');
var fill = document.getElementById('loading-bar-fill');
var pctEl = document.getElementById('loading-pct');
var barWrap = document.getElementById('loading-bar-wrap');
var debugFreezeBtn = document.getElementById('btn-debug-freeze');
function pad2(n) {
return n < 10 ? '0' + n : String(n);
}
function normalizeIndex(i) {
return ((i % totalSlides) + totalSlides) % totalSlides;
}
function showSlide(i) {
idx = (i + totalSlides * 10) % totalSlides;
if (slideImg) slideImg.src = 'IMAGE/t-' + pad2(idx + 1) + '.png?v=1';
idx = normalizeIndex(i);
if (slideImg) slideImg.src = 'IMAGE/' + slides[idx] + '?v=1';
if (pageEl) pageEl.textContent = (idx + 1) + ' / ' + totalSlides;
}
@@ -46,10 +64,25 @@
document.getElementById('btn-next')?.addEventListener('click', function () {
showSlide(idx + 1);
});
showSlide(0);
var durationMs = 6500;
var freezeLoading = params.get('freeze') === '1' || localStorage.getItem('debugLoadingFreeze') === '1';
var freezePctRaw = parseInt(
params.get('freezePct') || localStorage.getItem('debugLoadingFreezePct') || '0',
10
);
var freezePct = Math.max(0, Math.min(100, Number.isNaN(freezePctRaw) ? 0 : freezePctRaw));
var customDurationRaw = parseInt(
params.get('durationMs') || localStorage.getItem('debugLoadingDurationMs') || '',
10
);
if (!Number.isNaN(customDurationRaw) && customDurationRaw > 0) {
durationMs = customDurationRaw;
}
var startTs = null;
var navigated = false;
var currentPct = freezePct;
function goLobby() {
if (navigated) return;
@@ -60,14 +93,46 @@
document.getElementById('btn-skip')?.addEventListener('click', goLobby);
function setDebugFreezeLabel() {
if (!debugFreezeBtn) return;
var on = localStorage.getItem('debugLoadingFreeze') === '1';
debugFreezeBtn.textContent = on ? 'Freeze: ON' : 'Freeze: OFF';
}
if (debugFreezeBtn) {
debugFreezeBtn.addEventListener('click', function () {
var on = localStorage.getItem('debugLoadingFreeze') === '1';
if (on) {
localStorage.removeItem('debugLoadingFreeze');
localStorage.removeItem('debugLoadingFreezePct');
} else {
localStorage.setItem('debugLoadingFreeze', '1');
localStorage.setItem('debugLoadingFreezePct', String(currentPct));
}
setDebugFreezeLabel();
window.location.reload();
});
setDebugFreezeLabel();
}
function paintProgress(p) {
currentPct = p;
if (fill) fill.style.setProperty('--loading-fill-scale', String(p / 100));
if (pctEl) pctEl.textContent = p + '%';
if (barWrap) barWrap.setAttribute('aria-valuenow', String(p));
}
if (freezeLoading) {
paintProgress(freezePct);
return;
}
function tick(ts) {
if (navigated) return;
if (startTs == null) startTs = ts;
var t = Math.min(1, (ts - startTs) / durationMs);
var p = Math.min(100, Math.floor(t * 100));
if (fill) fill.style.width = p + '%';
if (pctEl) pctEl.textContent = p + '%';
if (barWrap) barWrap.setAttribute('aria-valuenow', String(p));
paintProgress(p);
if (t >= 1) {
goLobby();
return;
+73 -45
View File
@@ -36,50 +36,53 @@ body.loading-page {
flex-direction: column;
align-items: center;
justify-content: center;
padding: max(12px, env(safe-area-inset-top)) max(14px, env(safe-area-inset-right)) max(16px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
gap: clamp(0.75rem, 2.5vh, 1.25rem);
padding: max(10px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
gap: clamp(0.4rem, 1.35vh, 0.85rem);
}
.loading-top {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.65rem;
gap: 0rem;
width: 100%;
max-width: min(92vw, 420px);
max-width: min(95vw, 680px);
}
.loading-icon {
width: clamp(56px, 14vw, 88px);
height: auto;
width: auto;
height: clamp(52px, 22vh, 223px);
max-height: 22vh;
object-fit: contain;
filter: drop-shadow(0 0 16px rgba(34, 211, 238, 0.65));
filter: drop-shadow(0 0 14px rgba(34, 211, 238, 0.55));
}
.loading-bar-wrap {
--loading-bar-height: min(16.2vh, 141px);
--loading-bar-fill-inset-x: 5.7%;
--loading-bar-fill-inset-y: 25.5%;
position: relative;
width: 100%;
height: clamp(36px, 8vw, 44px);
border-radius: 6px;
border: 2px solid transparent;
background:
linear-gradient(#1a1030, #1a1030) padding-box,
linear-gradient(90deg, #22d3ee, #e879f9, #22d3ee) border-box;
box-shadow:
0 0 18px rgba(236, 72, 153, 0.45),
0 0 24px rgba(34, 211, 238, 0.25),
inset 0 0 20px rgba(0, 0, 0, 0.35);
width: calc(var(--loading-bar-height) * (658 / 141));
max-width: 95vw;
height: var(--loading-bar-height);
background: url(IMAGE/loading-bg.png) center / 100% 100% no-repeat;
border: none;
border-radius: 0;
box-shadow: none;
overflow: hidden;
margin-top: -3rem;
}
.loading-bar-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0%;
background: linear-gradient(90deg, rgba(168, 85, 247, 0.85), rgba(236, 72, 153, 0.9));
border-radius: 4px 0 0 4px;
left: var(--loading-bar-fill-inset-x);
top: var(--loading-bar-fill-inset-y);
bottom: var(--loading-bar-fill-inset-y);
width: calc(100% - (var(--loading-bar-fill-inset-x) * 2));
background: url(IMAGE/loading.png) left center / 100% 100% no-repeat;
border-radius: 4px;
transform-origin: left center;
transform: scaleX(var(--loading-fill-scale, 0));
}
.loading-pct {
@@ -89,20 +92,20 @@ body.loading-page {
align-items: center;
justify-content: center;
height: 100%;
font-size: clamp(0.95rem, 2.8vw, 1.15rem);
font-size: clamp(1.05rem, 3vw, 2.55rem);
font-weight: 700;
color: #fff;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
text-shadow: 0 0 10px rgba(255, 255, 255, 0.45);
}
.loading-status {
margin: 0;
font-size: clamp(0.88rem, 2.2vw, 1.05rem);
font-weight: 600;
color: #67e8f9;
text-align: center;
text-shadow: 0 0 14px rgba(34, 211, 238, 0.55);
letter-spacing: 0.02em;
.loading-status-img {
display: block;
width: min(50vw, 380px);
max-width: 92%;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 10px rgba(34, 211, 238, 0.42));
margin-top: -1.2rem;
}
.loading-carousel {
@@ -110,9 +113,9 @@ body.loading-page {
flex-direction: row;
align-items: center;
justify-content: center;
gap: clamp(4px, 1.2vw, 12px);
gap: 0;
width: 100%;
max-width: min(96vw, 920px);
max-width: min(96vw, 990px);
}
.loading-arrow {
@@ -123,6 +126,7 @@ body.loading-page {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform 0.15s, filter 0.2s;
display: block;
}
.loading-arrow:hover,
@@ -146,7 +150,7 @@ body.loading-page {
.loading-slide-frame {
flex: 1;
min-width: 0;
max-height: min(52vh, 520px);
max-height: min(54vh, 551px);
display: flex;
align-items: center;
justify-content: center;
@@ -154,24 +158,25 @@ body.loading-page {
.loading-slide-img {
display: block;
max-width: 100%;
max-height: min(52vh, 520px);
max-width: min(96vw, 921px);
max-height: min(54vh, 551px);
width: auto;
height: auto;
object-fit: contain;
filter: drop-shadow(0 8px 32px rgba(0, 0, 0, 0.45));
filter: none;
}
.loading-page-ind {
margin: 0;
font-size: clamp(0.8rem, 2vw, 0.95rem);
margin: -2.35rem 0 0;
font-size: clamp(0.78rem, 1.8vw, 1.5rem);
font-weight: 600;
color: rgba(248, 250, 252, 0.9);
text-shadow: 0 0 10px rgba(34, 211, 238, 0.35);
color: #7feeff;
text-shadow: 0 0 10px rgba(102, 240, 255, 0.52);
}
.loading-skip {
margin-top: 0.25rem;
display: none;
margin-top: 0.15rem;
padding: 0.4rem 1rem;
border: 1px solid rgba(148, 163, 184, 0.45);
border-radius: 8px;
@@ -186,3 +191,26 @@ body.loading-page {
border-color: #22d3ee;
color: #e2e8f0;
}
.loading-debug-freeze {
/* display: block; */
display: none;
position: fixed;
right: max(10px, env(safe-area-inset-right));
bottom: max(10px, env(safe-area-inset-bottom));
z-index: 999;
pointer-events: auto !important;
padding: 0.35rem 0.65rem;
border: 1px solid rgba(34, 211, 238, 0.55);
border-radius: 8px;
background: rgba(12, 24, 44, 0.72);
color: #a5f3fc;
font-family: inherit;
font-size: 0.75rem;
cursor: pointer;
}
.loading-debug-freeze:hover {
filter: brightness(1.08);
}
+102 -30
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0156">
<link rel="stylesheet" href="style.css?v=0.0170">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
@@ -21,7 +21,7 @@
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="/Main-Menu/char-main.png" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="64" height="64" decoding="async">
<img src="../Main-Menu/char-main.png" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="64" height="64" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
@@ -61,24 +61,94 @@
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-inner">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" role="presentation" decoding="async">
<span>Mixie Kim</span>
<span>9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" role="presentation" decoding="async">
<span>Golden L.</span>
<span>9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" role="presentation" decoding="async">
<span>Lilly 991</span>
<span>9552</span>
</li>
</ul>
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
@@ -89,10 +159,10 @@
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="/Main-Menu/">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="/Quiz-Battle/">
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
@@ -104,12 +174,13 @@
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="/Main-Lobby/IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="/Main-Lobby/IMAGE/guide/btn-next.png" alt="" decoding="async">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
@@ -119,21 +190,21 @@
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="/Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="/Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="/Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="/Game/img/chat-btn-send.png" alt="" decoding="async">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="/Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
@@ -150,6 +221,7 @@
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="lobby.js?v=0.0156"></script>
<script src="../app-base.js"></script>
<script src="lobby.js?v=0.0158"></script>
</body>
</html>
+238 -24
View File
@@ -1,15 +1,29 @@
(function () {
'use strict';
var BASE = '/Game';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = '/Main-Lobby/IMAGE/guide';
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 = '/Login/';
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
@@ -58,8 +72,8 @@
var enc = encodeURIComponent(id);
var q = '?ch=' + encodeURIComponent(id);
return [
BASE + '/img/characters/' + enc + '_down.png' + q,
BASE + '/img/characters/' + enc + '_down_0.png' + q
SERVER + '/img/characters/' + enc + '_down.png' + q,
SERVER + '/img/characters/' + enc + '_down_0.png' + q
];
}
@@ -84,7 +98,7 @@
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch('/Admin/api/player-coins.php?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
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;
@@ -145,6 +159,101 @@
});
}
// คำนวณ 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';
}
function applyProfile() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
@@ -160,7 +269,7 @@
var cid = getSelectedCharacterId();
if (!cid) {
av.onerror = null;
av.src = '/Main-Menu/char-main.png';
av.src = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
} else {
var urls = characterDownUrls(cid);
var avStep = 0;
@@ -169,7 +278,7 @@
if (avStep === 1) av.src = urls[1];
else {
av.onerror = null;
av.src = '/Main-Menu/char-main.png';
av.src = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
};
av.src = urls[0];
@@ -181,19 +290,78 @@
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');
document.body.classList.toggle('lobby-guide-top-step', guideStep >= 4);
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 (guideDim) {
guideDim.classList.toggle('hidden', !open);
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
@@ -202,6 +370,7 @@
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 */ }
@@ -209,14 +378,22 @@
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', n >= 4);
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() {
@@ -253,9 +430,19 @@
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) {
@@ -280,7 +467,7 @@
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = '/Main-Menu/';
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
@@ -302,21 +489,22 @@
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function updateAiChatHeaderUi() {
if (!aiChatPanel || !aiChatToggleImg || !aiChatCloseBtn) return;
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (!hidden) {
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
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();
});
}
@@ -325,7 +513,7 @@
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
updateAiChatHeaderUi();
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
@@ -334,9 +522,11 @@
}
} 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');
@@ -387,7 +577,7 @@
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(BASE + '/api/ai-chat', {
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
@@ -428,14 +618,38 @@
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') syncMainLobbyCharacterUi();
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Menu — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css?v=1.2">
</head>
<body>
<!-- พื้นหลังจาก Login -->
@@ -43,6 +43,7 @@
</div>
</div>
<script src="../app-base.js"></script>
<script src="menu.js"></script>
</body>
</html>
+5 -5
View File
@@ -5,7 +5,7 @@
function checkLoginStatus() {
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn || isLoggedIn !== 'true') {
window.location.href = '/Login/';
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
@@ -18,22 +18,22 @@
// ปุ่มต่างๆ
document.getElementById('btn-back')?.addEventListener('click', function () {
window.location.href = '/Main-Lobby/';
window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/';
});
document.getElementById('btn-create-room')?.addEventListener('click', function () {
window.location.href = '/Create%20Room/';
window.location.href = typeof appPath === 'function' ? appPath('/Create%20Room/') : '/Create%20Room/';
});
document.getElementById('btn-join')?.addEventListener('click', function () {
window.location.href = '/Join%20Room/';
window.location.href = typeof appPath === 'function' ? appPath('/Join%20Room/') : '/Join%20Room/';
});
document.getElementById('btn-exit-game')?.addEventListener('click', function () {
if (confirm('คุณต้องการออกจากเกมหรือไม่?')) {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('loginType');
window.location.href = '/Login/';
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
}
});
})();
File diff suppressed because it is too large Load Diff