update design

This commit is contained in:
2026-05-27 08:02:22 +00:00
parent c5b086e8f1
commit 740c858224
91 changed files with 6556 additions and 2928 deletions
+34
View File
@@ -0,0 +1,34 @@
@echo off
set SRC=https://srv1361159.hstgr.cloud
set ROOT=%~dp0..
echo Download app-base.js and index.html ...
echo.
curl --ssl-no-revoke -fsSL "%SRC%/app-base.js" -o "%ROOT%\app-base.js"
if errorlevel 1 goto try_powershell
curl --ssl-no-revoke -fsSL "%SRC%/Create%%20Room/index.html" -o "%~dp0index.html"
if errorlevel 1 goto try_powershell
echo.
echo OK - press Ctrl+Shift+R on Create Room page
goto end
:try_powershell
echo curl failed, trying PowerShell...
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0FIX-NOW.ps1"
if errorlevel 1 goto manual
goto end
:manual
echo.
echo AUTO DOWNLOAD FAILED - do this manually:
echo 1) Open in browser: %SRC%/Create%%20Room/
echo 2) Ctrl+U view source, Ctrl+A Ctrl+C
echo 3) Notepad - paste - Save As:
echo %ROOT%\Create Room\index.html
echo 4) Open %SRC%/app-base.js - Save As:
echo %ROOT%\app-base.js
echo.
:end
pause
+25
View File
@@ -0,0 +1,25 @@
$ErrorActionPreference = 'Stop'
$src = 'https://srv1361159.hstgr.cloud'
$root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$cr = Join-Path $root 'Create Room'
[System.Net.ServicePointManager]::CheckCertificateRevocationList = $false
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Save-Url($url, $path) {
Write-Host " $url"
Write-Host " -> $path"
try {
Invoke-WebRequest -Uri $url -OutFile $path -UseBasicParsing
} catch {
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($url, $path)
}
}
Write-Host 'Downloading...'
Save-Url "$src/app-base.js" (Join-Path $root 'app-base.js')
Save-Url "$src/Create%20Room/index.html" (Join-Path $cr 'index.html')
Write-Host ''
Write-Host 'Done. Open Create Room and Ctrl+Shift+R'
Read-Host 'Press Enter'
@@ -0,0 +1,13 @@
Paste this block as the FIRST <script> before other scripts in index.html
(if download fails, edit index.html in Notepad and add at line ~89):
<script>
(function(){var P='https://srv1361159.hstgr.cloud/Game',h=(location.hostname||'').toLowerCase();
if(h!=='localhost'&&h!=='127.0.0.1')return;
try{localStorage.setItem('isLoggedIn','true');localStorage.setItem('loginType','guest');
if(!localStorage.getItem('playerName'))localStorage.setItem('playerName','DevTester');}catch(e){}
window.GAME_API_FETCH_BASE=P;window.__JD_GAME_API_BASE__=P;
if(!window.fetch||window.__JD_INLINE__)return;window.__JD_INLINE__=1;
var f=window.fetch.bind(window);var bad=/^https?:\/\/(?:localhost|127\.0\.0\.1):(3004|3001|3010|13010)\/Game/i;
window.fetch=function(u,o){if(typeof u==='string'&&bad.test(u))u=P+u.slice(u.indexOf('/Game')+5);return f(u,o);};})();
</script>
+29 -1
View File
@@ -1,6 +1,8 @@
(function () {
'use strict';
if (typeof ensureLocalDevLogin === 'function') ensureLocalDevLogin();
function checkLoginStatus() {
var isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn || isLoggedIn !== 'true') {
@@ -13,7 +15,33 @@
if (!checkLoginStatus()) return;
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var PROD_GAME_API = 'https://srv1361159.hstgr.cloud/Game';
function resolveCreateRoomApiBase() {
if (typeof resolveGameApiBase === 'function') {
return resolveGameApiBase().replace(/\/$/, '');
}
if (typeof window.__JD_GAME_API_BASE__ === 'string' && window.__JD_GAME_API_BASE__) {
return window.__JD_GAME_API_BASE__.replace(/\/$/, '');
}
var pageGame = BASE.replace(/\/$/, '');
try {
var h = (location.hostname || '').toLowerCase();
if (h === 'localhost' || h === '127.0.0.1') {
try {
if (localStorage.getItem('jdUseLocalGameApi') === '1') {
var port = localStorage.getItem('jdGameNodePort') || '13010';
return 'http://127.0.0.1:' + String(port).replace(/\D/g, '') + '/Game';
}
} catch (e) { /* ignore */ }
return PROD_GAME_API;
}
} catch (e2) { /* ignore */ }
return pageGame;
}
var SERVER = resolveCreateRoomApiBase();
var LOBBY_A_MAP_ID = 'mlsbbxfe';
var LOBBY_TOTAL_SLOTS = 6;
+1 -1
View File
@@ -87,7 +87,7 @@
</div>
<script src="../app-base.js"></script>
<script src="create-room.js?v=1.004"></script>
<script src="create-room.js?v=20260527"></script>
<script>
(function(){
var content = document.querySelector('.create-content');
+78
View File
@@ -0,0 +1,78 @@
/**
* AppServ localhost — ใช้ Game API + Socket บน production
* ไม่ต้องรัน node server.js บน Windows (หลีกเลี่ยง EACCES port 3001/3010)
* ปิดโหมดนี้: localStorage.setItem('jdUseLocalGameApi','1') แล้วรัน node ที่พอร์ต 13010
*/
(function (g) {
'use strict';
var loc = g.location || {};
var h = (loc.hostname || '').toLowerCase();
if (h !== 'localhost' && h !== '127.0.0.1') return;
var PROD = 'https://srv1361159.hstgr.cloud';
var PROD_GAME = PROD + '/Game';
function useLocalNode() {
try { return localStorage.getItem('jdUseLocalGameApi') === '1'; } catch (e) { return false; }
}
function localApiBase() {
var port = '13010';
try {
var p = localStorage.getItem('jdGameNodePort');
if (p) port = String(p).replace(/\D/g, '') || port;
} catch (e) { /* ignore */ }
return 'http://127.0.0.1:' + port + '/Game';
}
var apiBase = useLocalNode() ? localApiBase() : PROD_GAME;
var nodeOrigin = useLocalNode()
? apiBase.replace(/\/Game\/?$/, '')
: PROD;
g.__JD_GAME_API_BASE__ = apiBase;
g.GAME_API_FETCH_BASE = apiBase;
g.GAME_NODE_ORIGIN = nodeOrigin;
g.GAME_SERVER = nodeOrigin;
g.ensureLocalDevLogin = function () {
try {
if (localStorage.getItem('isLoggedIn') !== 'true') {
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('loginType', 'guest');
}
if (!(localStorage.getItem('playerName') || '').trim()) {
localStorage.setItem('playerName', 'DevTester');
}
if (!(localStorage.getItem('roomLobbyDisplayName') || '').trim()) {
localStorage.setItem('roomLobbyDisplayName', 'DevTester');
}
} catch (e) { /* ignore */ }
};
g.ensureLocalDevLogin();
g.resolveGameApiBase = function () {
return String(g.__JD_GAME_API_BASE__ || apiBase).replace(/\/$/, '');
};
if (g.__JD_LOCAL_DEV_FETCH_PATCHED__ || typeof g.fetch !== 'function') return;
var nativeFetch = g.fetch.bind(g);
var badPort = /https?:\/\/(?:localhost|127\.0\.0\.1):(3004|3001|3010|13010)\/Game/i;
g.fetch = function jdLocalDevFetch(input, init) {
if (typeof input === 'string') {
if (badPort.test(input)) {
input = PROD_GAME + input.slice(input.indexOf('/Game') + 5);
} else if (!useLocalNode() && input.indexOf('/Game/api') >= 0 && input.indexOf('://') < 0) {
var base = typeof g.appPath === 'function' ? g.appPath('/Game') : '/Game';
if (input.indexOf(base + '/api') === 0) {
input = PROD_GAME + input.slice(base.length);
} else if (input.indexOf('/Game/api') === 0) {
input = PROD_GAME + input.slice('/Game'.length);
}
}
}
return nativeFetch(input, init);
};
g.__JD_LOCAL_DEV_FETCH_PATCHED__ = true;
})(typeof window !== 'undefined' ? window : this);
@@ -0,0 +1,23 @@
@echo off
chcp 65001 >nul
set SRC=https://srv1361159.hstgr.cloud
set ROOT=%~dp0..
echo Sync from production...
echo.
curl -fsSL "%SRC%/app-base.js" -o "%ROOT%\app-base.js" || goto fail
curl -fsSL "%SRC%/Create%%20Room/jd-local-dev.js" -o "%~dp0jd-local-dev.js" || goto fail
curl -fsSL "%SRC%/Create%%20Room/create-room.js" -o "%~dp0create-room.js" || goto fail
curl -fsSL "%SRC%/Create%%20Room/index.html" -o "%~dp0index.html" || goto fail
if not exist "%ROOT%\Game\js" mkdir "%ROOT%\Game\js"
curl -fsSL "%SRC%/Game/js/game-http-base.js" -o "%ROOT%\Game\js\game-http-base.js" || goto fail
echo.
echo OK — Ctrl+Shift+R ที่ Create Room
pause
exit /b 0
:fail
echo Download failed. Run FIX-NOW.bat instead (only app-base.js).
pause
exit /b 1
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
public/css
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
public/js
+14
View File
@@ -0,0 +1,14 @@
If room-lobby is BLACK on localhost, paste this as FIRST script in room-lobby.html (before app-base.js):
<script>
(function(){var P='https://srv1361159.hstgr.cloud',PG=P+'/Game',h=(location.hostname||'').toLowerCase();
if(h!=='localhost'&&h!=='127.0.0.1')return;
window.GAME_NODE_ORIGIN=P;window.GAME_SERVER=P;window.GAME_API_FETCH_BASE=PG;
if(window.fetch&&!window.__JD_RL_FETCH__){window.__JD_RL_FETCH__=1;var f=window.fetch.bind(window);
var bad=/^https?:\/\/(?:localhost|127\.0\.0\.1):(3004|3001|3010|13010)\/Game/i;
window.fetch=function(u,o){if(typeof u==='string'&&bad.test(u))u=PG+u.slice(u.indexOf('/Game')+5);return f(u,o);};}
document.write('<script src="'+P+'/Game/socket.io/socket.io.js"><\/script>');})();
</script>
PowerShell (if SSL error, add -UseBasicParsing):
Invoke-WebRequest "https://srv1361159.hstgr.cloud/Game/room-lobby.html" -OutFile "C:\AppServ\www\srv1361159.hstgr.cloud\Game\room-lobby.html" -UseBasicParsing
+1 -1
View File
@@ -138,7 +138,7 @@
</script>
<div class="container">
<h1>ตัวละคร (4 ทิศ)</h1>
<p class="character-page-links"><a href="index.html">กลับหน้าหลัก</a> · <a href="lobby.html">ล็อบบี้</a></p>
<p class="character-page-links"><a href="index.html">กลับหน้าหลัก</a> · <a href="/Main-Menu/">เมนูหลัก</a> · <a href="/Main-Lobby/">Main Lobby</a></p>
<section class="char-upload char-upload-simple">
<h2>อัปโหลดตัวละคร (บน · ล่าง · ซ้าย · ขวา)</h2>
@@ -0,0 +1,613 @@
@font-face {
font-family: 'NotoSansThai';
src:
url('../../../FONTS/NotoSansThai-ExtraBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
.customize-popup-dialog,
.customize-popup-dialog * {
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
}
.customize-popup-overlay {
position: fixed;
inset: 0;
z-index: 10090;
display: flex;
align-items: center;
justify-content: center;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
background: rgba(0, 0, 0, 0.8);
}
.customize-popup-overlay.is-hidden,
.customize-popup-overlay.hidden,
#room-cz-overlay.is-hidden,
#room-cz-overlay.hidden {
display: none !important;
}
/* room-lobby ใช้ #room-cz-overlay + คลาส customize-popup (production) */
#room-cz-overlay.customize-popup-overlay {
position: fixed;
inset: 0;
z-index: 10090;
display: flex;
align-items: center;
justify-content: center;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
background: rgba(0, 0, 0, 0.8);
}
.customize-popup-dialog {
position: relative;
width: min(1600px, calc(100vw - 24px));
max-height: calc(100dvh - 24px);
min-height: 0;
--cc-scale: 1;
--cc-layout-shift-x: calc(64px * var(--cc-scale));
}
.customize-popup-content {
display: flex;
align-items: flex-start;
justify-content: center;
gap: calc(14px * var(--cc-scale));
min-height: 0;
transform: translateX(var(--cc-layout-shift-x));
}
.customize-popup-char-col {
position: relative;
width: min(500px, 36vw);
min-width: 330px;
height: calc(860px * var(--cc-scale));
padding-top: calc(18px * var(--cc-scale));
flex: 0 0 auto;
--cc-char-preview-left: 40%;
--cc-char-preview-top: calc(130px * var(--cc-scale));
--cc-char-preview-size: calc(800px * var(--cc-scale));
}
.customize-popup-char-bg {
position: absolute;
left: var(--cc-char-preview-left);
top: var(--cc-char-preview-top);
transform: translateX(-50%);
width: var(--cc-char-preview-size);
height: var(--cc-char-preview-size);
aspect-ratio: 789 / 789;
object-fit: contain;
display: block;
z-index: 1;
}
.customize-popup-char-preview {
position: absolute;
left: var(--cc-char-preview-left);
top: var(--cc-char-preview-top);
transform: translateX(-50%);
width: var(--cc-char-preview-size);
height: var(--cc-char-preview-size);
object-fit: contain;
image-rendering: auto;
z-index: 2;
}
.customize-popup-player-name {
position: absolute;
left: var(--cc-char-preview-left);
top: calc(114px * var(--cc-scale));
transform: translateX(-50%);
width: var(--cc-char-preview-size);
margin: 0;
text-align: center;
color: #ffffff;
font-size: calc(55px * var(--cc-scale));
font-weight: 700;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
letter-spacing: 0.02em;
white-space: nowrap;
z-index: 3;
}
.customize-popup-coin-wrap {
position: absolute;
top: calc(8px * var(--cc-scale));
left: calc(0px * var(--cc-scale));
width: calc(237px * var(--cc-scale));
height: calc(97px * var(--cc-scale));
z-index: 4;
}
.customize-popup-coin-bg {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.customize-popup-coin-val {
position: absolute;
top: 50%;
left: 58%;
transform: translate(-50%, -50%);
color: #fff;
font-size: calc(30px * var(--cc-scale));
font-weight: 700;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
text-shadow: 0 0 8px rgba(54, 255, 255, 0.45);
white-space: nowrap;
}
.customize-popup-panel-shell {
position: relative;
width: min(
990px,
calc((100dvh - 24px) * (990 / 951)),
calc(100vw - 24px - min(500px, 36vw) - calc(14px * var(--cc-scale)))
);
aspect-ratio: 990 / 951;
max-height: calc(100dvh - 24px);
min-width: 0;
flex: 0 0 auto;
overflow: hidden;
--cc-shell-scale: var(--cc-scale);
}
.customize-popup-cloth-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
pointer-events: none;
}
.customize-popup-close {
position: absolute;
top: calc(78px * var(--cc-shell-scale));
right: calc(86px * var(--cc-shell-scale));
width: calc(84px * var(--cc-shell-scale));
height: calc(84px * var(--cc-shell-scale));
border: none;
background: #ffffff;
cursor: pointer;
border-radius: 0;
z-index: 3;
opacity: 0;
color: transparent;
font-size: 0;
line-height: 0;
}
.customize-popup-panel {
position: absolute;
top: calc(165px * var(--cc-shell-scale));
left: calc(118px * var(--cc-shell-scale));
right: calc(119px * var(--cc-shell-scale));
bottom: calc(39px * var(--cc-shell-scale));
min-width: 0;
display: flex;
flex-direction: column;
}
@media (max-width: 1200px) {
.customize-popup-dialog {
width: min(1280px, calc(100vw - 24px));
}
.customize-popup-char-col {
width: min(430px, 34vw);
min-width: 300px;
height: calc(740px * var(--cc-scale));
}
.customize-popup-tabs {
--customize-tab-line-shift-x: calc(12px * var(--cc-shell-scale));
--customize-tab-line-extend-left: calc(10px * var(--cc-shell-scale));
--customize-tab-right-line-shift-x: calc(-12px * var(--cc-shell-scale));
}
}
@media (max-height: 820px) and (min-width: 961px) {
.customize-popup-content {
gap: calc(10px * var(--cc-scale));
transform: translateX(calc(36px * var(--cc-scale)));
}
.customize-popup-char-col {
width: min(420px, 32vw);
min-width: 280px;
height: calc(700px * var(--cc-scale));
}
.customize-popup-panel-shell {
width: min(
920px,
calc((100dvh - 24px) * (990 / 951)),
calc(100vw - 24px - min(420px, 32vw) - calc(10px * var(--cc-scale)))
);
}
}
@media (max-width: 960px) {
.customize-popup-content {
flex-direction: column;
align-items: center;
transform: none;
}
.customize-popup-coin-wrap {
left: calc(8px * var(--cc-scale));
}
.customize-popup-char-col {
width: min(520px, 84vw);
min-width: 0;
height: calc(760px * var(--cc-scale));
}
.customize-popup-panel-shell {
width: min(920px, calc(100vw - 24px));
flex: 0 0 auto;
}
}
.customize-popup-theme-row {
position: relative;
display: block;
height: calc(97px * var(--cc-shell-scale));
margin-bottom: calc(-3px * var(--cc-shell-scale));
}
.customize-popup-theme-label {
position: absolute;
left: calc(43px * var(--cc-shell-scale));
top: 50%;
transform: translateY(-50%);
z-index: 2;
width: calc(100px * var(--cc-shell-scale));
height: calc(79px * var(--cc-shell-scale));
object-fit: contain;
display: block;
}
.customize-popup-theme-frame {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: calc(97px * var(--cc-shell-scale));
width: auto;
z-index: 1;
}
.customize-popup-theme-frame > img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.customize-popup-color-list {
position: absolute;
top: calc(16px * var(--cc-shell-scale));
right: calc(18px * var(--cc-shell-scale));
bottom: calc(16px * var(--cc-shell-scale));
left: calc(170px * var(--cc-shell-scale));
display: flex;
align-items: center;
gap: calc(16px * var(--cc-shell-scale));
}
.customize-popup-color-btn {
position: relative;
display: inline-block;
vertical-align: top;
width: calc(46px * var(--cc-shell-scale));
height: calc(43px * var(--cc-shell-scale));
border: none;
background: transparent;
padding: 0;
cursor: pointer;
isolation: isolate;
overflow: visible;
}
.customize-popup-color-btn > .customize-popup-color-swatch {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.customize-popup-color-btn > .customize-popup-color-swatch--hex {
border-radius: calc(8px * var(--cc-shell-scale));
border: calc(2px * var(--cc-shell-scale)) solid rgba(0, 0, 0, 0.35);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
box-sizing: border-box;
}
.customize-popup-color-btn > .customize-popup-color-highlight {
position: absolute !important;
left: 50%;
top: 50%;
width: calc(78px * var(--cc-shell-scale));
height: calc(75px * var(--cc-shell-scale));
transform: translate(-50%, -50%);
object-fit: contain;
display: none !important;
z-index: 5;
pointer-events: none;
}
.customize-popup-color-btn.is-active > .customize-popup-color-highlight {
display: block !important;
}
.customize-popup-tabs {
display: flex;
align-items: flex-end;
--customize-tab-gap: calc(-5px * var(--cc-shell-scale));
--customize-tab-line-bottom: calc(4px * var(--cc-shell-scale)); /* ระยะจากล่างของเส้นใต้แท็บ */
--customize-tab-line-shift-x: calc(25px * var(--cc-shell-scale));
--customize-tab-line-extend-left: calc(21px * var(--cc-shell-scale));
--customize-tab-line-extend-right: calc(0px * var(--cc-shell-scale));
--customize-tab-left-shift-x: calc(0px * var(--cc-shell-scale));
--customize-tab-mid-shift-x: calc(0px * var(--cc-shell-scale));
--customize-tab-right-shift-x: calc(0px * var(--cc-shell-scale));
--customize-tab-mid-line-shift-x: calc(0px * var(--cc-shell-scale)); /* เส้นใต้ปุ่มกลาง(ทรงผม) ขยับซ้าย/ขวา */
--customize-tab-right-line-shift-x: calc(-28px * var(--cc-shell-scale)); /* เส้นใต้ปุ่มขวา(ชุด) ขยับซ้าย/ขวา */
gap: 0;
margin: calc(6px * var(--cc-shell-scale)) 0 calc(8px * var(--cc-shell-scale));
}
.customize-popup-tab-btn + .customize-popup-tab-btn {
margin-left: var(--customize-tab-gap);
}
.customize-popup-tab-btn {
position: relative;
width: calc(256px * var(--cc-shell-scale));
height: calc(100px * var(--cc-shell-scale));
border: none;
background: transparent;
padding: 0;
cursor: pointer;
flex: 0 0 auto;
}
.customize-popup-tab-btn:first-child {
transform: translateX(var(--customize-tab-left-shift-x));
}
.customize-popup-tab-btn:nth-child(2) {
transform: translateX(var(--customize-tab-mid-shift-x));
}
.customize-popup-tab-btn:last-child {
transform: translateX(var(--customize-tab-right-shift-x));
}
.customize-popup-tab-img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.customize-popup-tab-btn-line {
position: absolute;
left: calc(
(-0.5 * var(--customize-tab-gap))
+ var(--customize-tab-line-shift-x)
- var(--customize-tab-line-extend-left)
);
bottom: var(--customize-tab-line-bottom) !important;
width: calc(
100% + var(--customize-tab-gap)
+ var(--customize-tab-line-extend-left)
+ var(--customize-tab-line-extend-right)
);
height: auto;
display: block;
pointer-events: none;
}
.customize-popup-tab-btn:first-child .customize-popup-tab-btn-line {
left: calc(var(--customize-tab-line-shift-x) - var(--customize-tab-line-extend-left));
width: calc(
100% + (0.5 * var(--customize-tab-gap))
+ var(--customize-tab-line-extend-left)
+ var(--customize-tab-line-extend-right)
);
}
.customize-popup-tab-btn:nth-child(2) .customize-popup-tab-btn-line {
left: calc(
(-0.5 * var(--customize-tab-gap))
+ var(--customize-tab-line-shift-x)
- var(--customize-tab-line-extend-left)
+ var(--customize-tab-mid-line-shift-x)
);
}
.customize-popup-tab-btn:last-child .customize-popup-tab-btn-line {
left: calc(
(-0.5 * var(--customize-tab-gap))
+ var(--customize-tab-line-shift-x)
- var(--customize-tab-line-extend-left)
+ var(--customize-tab-right-line-shift-x)
);
width: calc(
100% + (0.5 * var(--customize-tab-gap))
+ var(--customize-tab-line-extend-left)
+ var(--customize-tab-line-extend-right)
);
}
.customize-popup-tab-btn.is-active .customize-popup-tab-btn-line {
display: none;
}
.customize-popup-items-wrap {
position: relative;
flex: 1 1 auto;
min-height: 0;
padding-right: calc(2px * var(--cc-shell-scale));
padding-left: calc(2px * var(--cc-shell-scale));
}
.customize-popup-items {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: calc(12px * var(--cc-shell-scale));
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
}
.customize-popup-items::-webkit-scrollbar {
width: 0;
height: 0;
}
.customize-popup-item {
position: relative;
min-height: calc(171px * var(--cc-shell-scale));
isolation: isolate;
}
.customize-popup-item-bg {
position: relative;
z-index: 1;
width: calc(181px * var(--cc-shell-scale));
height: calc(171px * var(--cc-shell-scale));
max-width: 100%;
object-fit: contain;
display: block;
margin: 0 auto;
}
.customize-popup-item-face {
position: absolute;
z-index: 2;
left: 50%;
top: calc(30px * var(--cc-shell-scale));
transform: translateX(-50%);
width: calc(101px * var(--cc-shell-scale));
height: calc(101px * var(--cc-shell-scale));
object-fit: contain;
display: block;
}
.customize-popup-item-price {
position: absolute;
z-index: 2;
left: 50%;
bottom: calc(-9px * var(--cc-shell-scale));
transform: translateX(-50%);
width: calc(121px * var(--cc-shell-scale));
height: calc(51px * var(--cc-shell-scale));
object-fit: contain;
display: block;
}
.customize-popup-item-price-text {
position: absolute;
z-index: 2;
left: 54%;
bottom: calc(5px * var(--cc-shell-scale));
transform: translateX(-50%);
color: #ffffff;
font-weight: 700;
font-size: calc(16px * var(--cc-shell-scale));
}
.customize-popup-item:first-child .customize-popup-item-price,
.customize-popup-item:first-child .customize-popup-item-price-text {
display: none !important;
}
.customize-popup-item-highlight {
position: absolute;
left: 50%;
top: 50%;
width: calc(191px * var(--cc-shell-scale));
height: calc(181px * var(--cc-shell-scale));
transform: translate(-50%, -50%);
object-fit: contain;
display: none;
z-index: 1;
pointer-events: none;
}
.customize-popup-item.is-active .customize-popup-item-highlight {
display: block;
}
.customize-popup-scroll {
position: absolute;
top: 0;
right: calc(-17px * var(--cc-shell-scale));
bottom: 0;
width: calc(14px * var(--cc-shell-scale));
pointer-events: auto;
}
.customize-popup-scroll.is-hidden {
display: none !important;
}
.customize-popup-scroll-track {
position: absolute;
inset: 0;
border-radius: 999px;
background: #1b2740;
border: 1px solid #22324f;
box-shadow: inset 0 0 6px rgba(8, 13, 24, 0.45);
pointer-events: auto;
cursor: pointer;
}
.customize-popup-scroll-thumb {
position: absolute;
left: 0;
top: 0;
width: 100%;
border-radius: 999px;
background: #f04cdc;
border: 1px solid #ffb8f5;
box-shadow: 0 0 6px rgba(240, 76, 220, 0.55);
pointer-events: auto;
cursor: grab;
}
.customize-popup-scroll-thumb.is-dragging {
cursor: grabbing;
}
.customize-popup-confirm {
align-self: center;
width: calc(351px * var(--cc-shell-scale));
height: calc(128px * var(--cc-shell-scale));
border: none;
background: transparent;
padding: 0;
cursor: pointer;
margin-top: calc(8px * var(--cc-shell-scale));
margin-bottom: calc(-42px * var(--cc-shell-scale));
}
.customize-popup-confirm img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
+592
View File
@@ -0,0 +1,592 @@
/* Shared profile popup styles (copied from room-lobby baseline) */
.room-lobby-profile-overlay {
position: fixed;
inset: 0;
z-index: 10060;
display: flex;
align-items: center;
justify-content: center;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.room-lobby-profile-overlay.is-hidden {
display: none !important;
}
.room-lobby-profile-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.8);
}
.room-lobby-profile-dialog {
position: relative;
z-index: 1;
width: min(
1701px,
calc(100vw - 24px),
calc((100dvh - 24px) * (1701 / 951))
);
height: auto;
aspect-ratio: 1701 / 951;
max-height: calc(100dvh - 24px);
padding: 0;
background: url(../img/03-6-Profile/profile-pop-bg.png) center/100% 100% no-repeat;
border: none;
border-radius: 0;
box-shadow: none;
color: #eaf8ff;
box-sizing: border-box;
--rp-scale: 1;
--rp-close-hit-size: calc(62px * var(--rp-scale));
--rp-close-hit-top: 9.5%;
--rp-close-hit-right: 5.5%;
}
.room-lobby-profile-title {
display: none !important;
}
.room-lobby-profile-close {
position: absolute;
top: var(--rp-close-hit-top);
right: var(--rp-close-hit-right);
width: var(--rp-close-hit-size);
height: var(--rp-close-hit-size);
border: none;
border-radius: 0;
background: rgba(0, 255, 255, 0);
color: transparent;
font-size: 0;
line-height: 1;
cursor: pointer;
text-shadow: none;
z-index: 3;
-webkit-tap-highlight-color: transparent;
opacity: 0.5;
}
.room-lobby-profile-content {
position: absolute;
top: 17%;
right: 7%;
bottom: 11%;
left: 7%;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.3fr);
gap: 0;
align-items: stretch;
height: auto;
padding: 0;
box-sizing: border-box;
}
.room-lobby-profile-left-pane,
.room-lobby-profile-right-pane {
border: 2px solid rgba(92, 233, 255, 0.65);
border-radius: 12px;
background: transparent;
padding: clamp(10px, 1.4vw, 16px);
box-sizing: border-box;
}
.room-lobby-profile-left-pane {
border: none;
border-radius: 0;
background: transparent;
padding: calc(3px * var(--rp-scale)) calc(4px * var(--rp-scale)) calc(5px * var(--rp-scale));
width: 100%;
height: 97%;
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) calc(58px * var(--rp-scale));
justify-items: center;
align-items: stretch;
row-gap: calc(10px * var(--rp-scale));
}
.room-lobby-profile-inner-frame {
width: min(100%, 100%);
max-width: 100%;
aspect-ratio: 621 / 626;
height: 100%;
max-height: 100%;
min-width: 0;
background: url(../img/03-6-Profile/profile-char-frame.png) center/100% 100% no-repeat;
--pf-scale: var(--rp-scale, 1);
font-size: calc(16px * var(--pf-scale));
padding: calc(12px * var(--pf-scale)) calc(11px * var(--pf-scale)) calc(9px * var(--pf-scale));
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
align-self: stretch;
overflow: hidden;
margin-top: -1.5%;
}
.room-lobby-profile-coins {
margin: calc(13px * var(--pf-scale)) 0 calc(10px * var(--pf-scale));
display: flex;
align-items: center;
justify-content: center;
gap: calc(8px * var(--pf-scale));
font-size: calc(30px * var(--pf-scale));
font-weight: 700;
width: 100%;
text-align: center;
line-height: 1;
}
.room-lobby-profile-coins strong {
color: #fff;
font-size: calc(48px * var(--pf-scale));
font-weight: 700;
line-height: 1;
}
.room-lobby-profile-coins-icon {
width: calc(30px * var(--pf-scale));
height: calc(30px * var(--pf-scale));
}
.room-lobby-profile-coins-label {
height: calc(62px * var(--pf-scale));
width: auto;
object-fit: contain;
margin-top: calc(6px * var(--pf-scale));
}
.room-lobby-profile-char-card {
border: none;
border-radius: 0;
width: 100%;
min-width: 0;
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: calc(3px * var(--pf-scale));
background: transparent;
padding: calc(4px * var(--pf-scale)) 0 calc(6px * var(--pf-scale));
box-sizing: border-box;
overflow: hidden;
}
.room-lobby-profile-char-card #room-lobby-profile-overlay-avatar {
width: 100%;
max-width: 100%;
height: auto;
object-fit: contain;
max-height: 83%;
margin-top: calc(-28px * var(--pf-scale));
margin-bottom: calc(-9px * var(--pf-scale));
}
.room-lobby-profile-overlay-name {
margin-top: 0;
font-size: calc(38px * var(--pf-scale));
font-weight: 700;
color: #fff;
line-height: 1;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
width: 100%;
max-width: 100%;
min-width: 0;
flex: 1 1 auto;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.room-lobby-profile-overlay-name-row {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
gap: calc(6px * var(--pf-scale));
width: 100%;
min-width: 0;
padding-left: calc(92px * var(--pf-scale));
padding-right: calc(92px * var(--pf-scale));
box-sizing: border-box;
margin-top: calc(-6px * var(--pf-scale));
flex-shrink: 0;
min-height: calc(70px * var(--pf-scale));
overflow: visible;
}
.room-lobby-profile-edit-btn {
padding: 0;
margin: 0;
position: absolute;
right: 4%;
top: 50%;
transform: translateY(-50%);
width: calc(88px * var(--pf-scale));
height: calc(88px * var(--pf-scale));
aspect-ratio: 1 / 1;
border: none;
border-radius: 0;
background: transparent;
cursor: pointer;
}
.room-lobby-profile-edit-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-name-sep {
margin-top: calc(-10px * var(--pf-scale));
width: min(100%, 505px);
height: auto;
object-fit: contain;
}
.room-lobby-profile-overlay-agent {
margin-top: calc(4px * var(--pf-scale));
font-size: calc(18px * var(--pf-scale));
color: #F856E2;
font-weight: 500;
line-height: 1;
margin-bottom: calc(4px * var(--pf-scale));
flex-shrink: 0;
}
.room-lobby-profile-logout {
margin-top: 0;
margin-bottom: 7%;
width: min(50%, calc(256px * var(--rp-scale)));
max-width: calc(256px * var(--rp-scale));
aspect-ratio: 256 / 94;
height: auto;
border-radius: 0;
border: none;
background: transparent;
color: inherit;
font-size: 0;
font-weight: 400;
cursor: pointer;
align-self: center;
display: inline-flex;
align-items: center;
justify-content: center;
}
.room-lobby-profile-logout img {
width: 100%;
height: 100%;
object-fit: fill;
display: block;
}
.room-lobby-profile-right-pane {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: calc(14px * var(--rp-scale));
padding-top: calc(6px * var(--rp-scale));
margin-left: calc(-32px * var(--rp-scale));
min-width: 0;
border: none;
border-radius: 0;
background: transparent;
padding: 0;
}
.room-lobby-profile-archiv-groups {
--room-lobby-profile-archiv-overlap: calc(-38px * var(--rp-scale));
--room-lobby-profile-archiv-group-scale: 1.031;
display: flex;
align-items: flex-end;
justify-content: flex-start;
padding-left: calc(45px * var(--rp-scale));
height: calc(60px * var(--rp-scale));
overflow: visible;
transform: scale(var(--room-lobby-profile-archiv-group-scale));
transform-origin: left bottom;
margin-left: 1%;
position: relative;
z-index: 4;
}
.room-lobby-profile-archiv-group-btn {
width: calc(190px * var(--rp-scale));
height: calc(60px * var(--rp-scale));
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.room-lobby-profile-archiv-group-btn + .room-lobby-profile-archiv-group-btn {
margin-left: var(--room-lobby-profile-archiv-overlap);
}
.room-lobby-profile-archiv-group-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-frame {
position: relative;
z-index: 1;
flex: 0 0 auto;
width: min(100%, calc(872px * var(--rp-scale)));
aspect-ratio: 872 / 515;
max-height: 100%;
overflow: hidden;
isolation: isolate;
background: url(../img/03-6-Profile/archiv-frame.png) center/contain no-repeat;
padding: 4.8% 9.5% 3.8% 11.2%;
box-sizing: border-box;
margin-top: -4.5%;
margin-left: 1%;
pointer-events: none;
}
.room-lobby-profile-achievements-list {
list-style: none;
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: calc(16px * var(--rp-scale));
padding-right: calc(6px * var(--rp-scale));
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
scrollbar-gutter: stable;
pointer-events: auto;
}
.room-lobby-profile-achievements-list::-webkit-scrollbar {
width: 0;
height: 0;
}
.room-lobby-profile-archiv-item {
--room-lobby-profile-archiv-icon-col-width: calc((131px + 14px) * var(--rp-scale));
display: flex;
flex: 0 0 auto;
justify-content: flex-start;
align-items: center;
gap: calc(14px * var(--rp-scale));
min-height: calc(123px * var(--rp-scale));
}
.room-lobby-profile-archiv-icon-bg {
width: calc(131px * var(--rp-scale));
height: calc(123px * var(--rp-scale));
aspect-ratio: 131 / 123;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-main {
--room-lobby-profile-archiv-title-progress-offset: calc(-8px * var(--rp-scale));
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.room-lobby-profile-archiv-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: clamp(8px, 0.8vw, 12px);
}
.room-lobby-profile-archiv-name {
width: calc(421px * var(--rp-scale));
height: calc(72px * var(--rp-scale));
max-width: 100%;
aspect-ratio: 421 / 72;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-count {
color: #ffe95a;
font-size: calc(38px * var(--rp-scale));
line-height: 1;
font-weight: 700;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
}
.room-lobby-profile-archiv-progress-wrap {
position: relative;
margin-top: var(--room-lobby-profile-archiv-title-progress-offset);
width: calc(571px * var(--rp-scale));
max-width: 100%;
aspect-ratio: 571 / 56;
height: calc(56px * var(--rp-scale));
}
.room-lobby-profile-archiv-progress-bg,
.room-lobby-profile-archiv-progress {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-progress {
clip-path: inset(0 calc(100% - var(--archiv-progress, 0%)) 0 0);
}
.room-lobby-profile-archiv-line {
width: calc(100% + var(--room-lobby-profile-archiv-icon-col-width));
margin-left: calc(-1 * var(--room-lobby-profile-archiv-icon-col-width));
height: calc(4px * var(--rp-scale));
aspect-ratio: auto;
object-fit: fill;
display: block;
margin-bottom: -3%;
}
.room-lobby-profile-archiv-scroll {
position: absolute;
top: 10%;
right: 4.6%;
bottom: 8%;
width: calc(13px * var(--rp-scale));
display: flex;
justify-content: center;
pointer-events: none;
}
.room-lobby-profile-archiv-scroll.is-hidden {
display: none !important;
}
.room-lobby-profile-archiv-scroll-bg,
.room-lobby-profile-archiv-scroll-bar {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 100%;
display: block;
border-radius: 999px;
}
.room-lobby-profile-archiv-scroll-bg {
top: 0;
bottom: 0;
height: 100%;
background: #1b2740;
border: 1px solid #22324f;
box-shadow: inset 0 0 6px rgba(8, 13, 24, 0.45);
}
.room-lobby-profile-archiv-scroll-bar {
top: 0;
height: 42%;
min-height: 18%;
background: #f04cdc;
border: 1px solid #ffb8f5;
box-shadow: 0 0 6px rgba(240, 76, 220, 0.55);
}
.room-lobby-profile-settings {
position: relative;
width: min(100%, calc(824px * var(--rp-scale)));
aspect-ratio: 824 / 169;
margin-top: auto;
}
.room-lobby-profile-settings-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
margin-left: 7%;
margin-top: -4%;
}
.room-lobby-profile-settings-row {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px calc(11px * var(--rp-scale)) 13px calc(163px * var(--rp-scale));
box-sizing: border-box;
}
.room-lobby-profile-setting-item {
display: inline-flex;
align-items: center;
gap: calc(14px * var(--rp-scale));
}
.room-lobby-profile-setting-label {
width: calc(99px * var(--rp-scale));
height: calc(39px * var(--rp-scale));
aspect-ratio: 99 / 39;
object-fit: contain;
display: block;
}
.room-lobby-profile-toggle {
margin: 0;
width: calc(152px * var(--rp-scale));
height: calc(94px * var(--rp-scale));
aspect-ratio: 152 / 94;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
margin-left: -11%;
}
.room-lobby-profile-toggle img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
/* Profile popup: compact by width */
@media (max-width: 1100px) {
.room-lobby-profile-content {
top: 13%;
right: 4%;
bottom: 8%;
left: 4%;
gap: clamp(8px, 1vw, 14px);
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.12fr);
}
.room-lobby-profile-left-pane {
grid-template-rows: minmax(0, 1fr) auto;
row-gap: clamp(3px, 0.5vw, 6px);
}
.room-lobby-profile-logout {
width: min(58%, 100%);
}
.room-lobby-profile-char-card #room-lobby-profile-overlay-avatar {
width: 74%;
max-width: 74%;
max-height: 60%;
}
}
/* Profile popup: compact by width (small) */
@media (max-width: 900px) {
.room-lobby-profile-content {
top: 11%;
right: 3%;
bottom: 7%;
left: 3%;
gap: 8px;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.08fr);
}
.room-lobby-profile-logout {
width: min(62%, 100%);
}
.room-lobby-profile-char-card #room-lobby-profile-overlay-avatar {
width: 64%;
max-width: 64%;
max-height: 56%;
}
}
/* Profile popup: compact by height without breaking object ratios */
@media (max-height: 700px) {
.room-lobby-profile-content {
top: 15%;
right: 5.2%;
bottom: 9%;
left: 5.2%;
gap: calc(10px * var(--rp-scale));
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.22fr);
}
.room-lobby-profile-right-pane {
gap: calc(10px * var(--rp-scale));
}
.room-lobby-profile-archiv-groups {
--room-lobby-profile-archiv-group-scale: 1;
}
}
@media (max-height: 560px) {
.room-lobby-profile-content {
top: 13.5%;
right: 4%;
bottom: 7.5%;
left: 4%;
gap: calc(8px * var(--rp-scale));
}
.room-lobby-profile-left-pane {
row-gap: calc(6px * var(--rp-scale));
}
.room-lobby-profile-logout {
margin-bottom: 3.5%;
}
}
+49 -494
View File
@@ -1,8 +1,16 @@
* { box-sizing: border-box; }
@font-face {
font-family: 'NotoSansThai';
src:
url('/Create%20Room/FONTS/NotoSansThai-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
body { font-family: sans-serif; margin: 0; background: #1a1b26; color: #c0caf5; }
body.room-lobby-page,
body.room-lobby-page * {
font-family: 'Kanit', system-ui, sans-serif !important;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif !important;
}
.container { max-width: 720px; margin: 1rem auto; padding: 1rem; }
@@ -301,18 +309,16 @@ body.room-lobby--lobby-b .room-lobby-ready-fixed {
pointer-events: auto;
}
.room-lobby-top-right-actions {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 0.45rem;
display: none !important;
}
.room-lobby-top-right-action-btn {
width: 161px;
height: 176px;
min-width: 161px;
min-height: 176px;
position: relative;
flex: 0 0 auto;
width: clamp(100px, 11vw, 161px);
height: auto;
min-width: 0;
min-height: 0;
max-width: 161px;
max-height: 176px;
aspect-ratio: 161 / 176;
padding: 0;
margin: 0;
@@ -321,14 +327,13 @@ body.room-lobby--lobby-b .room-lobby-ready-fixed {
background: transparent;
box-shadow: none;
cursor: pointer;
overflow: visible;
isolation: isolate;
}
.room-lobby-top-right-action-btn img {
width: 161px;
height: 176px;
min-width: 161px;
min-height: 176px;
width: 100%;
height: auto;
max-width: 161px;
max-height: 176px;
aspect-ratio: 161 / 176;
object-fit: contain;
display: block;
@@ -412,473 +417,8 @@ body.room-lobby--lobby-b .room-lobby-ready-fixed {
object-fit: contain;
}
/* Profile overlay (open from btn-profile-set) */
.room-lobby-profile-overlay {
position: fixed;
inset: 0;
z-index: 10060;
display: flex;
align-items: center;
justify-content: center;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.room-lobby-profile-overlay.is-hidden {
display: none !important;
}
.room-lobby-profile-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.8);
}
.room-lobby-profile-dialog {
position: relative;
z-index: 1;
width: min(
1701px,
calc(100vw - 24px),
calc((100dvh - 24px) * (1701 / 951))
);
height: auto;
aspect-ratio: 1701 / 951;
max-height: calc(100dvh - 24px);
padding: 0;
background: url(../img/03-6-Profile/profile-pop-bg.png) center/100% 100% no-repeat;
border: none;
border-radius: 0;
box-shadow: none;
color: #eaf8ff;
box-sizing: border-box;
--rp-scale: 1;
--rp-close-hit-size: calc(62px * var(--rp-scale));
--rp-close-hit-top: 5.6%;
--rp-close-hit-right: 2.9%;
}
.room-lobby-profile-title {
display: none !important;
}
.room-lobby-profile-close {
position: absolute;
top: var(--rp-close-hit-top);
right: var(--rp-close-hit-right);
width: var(--rp-close-hit-size);
height: var(--rp-close-hit-size);
border: none;
border-radius: 0;
background: rgba(0, 255, 255, 0.5);
color: transparent;
font-size: 0;
line-height: 1;
cursor: pointer;
text-shadow: none;
z-index: 3;
-webkit-tap-highlight-color: transparent;
opacity: 0.5;
}
.room-lobby-profile-content {
position: absolute;
top: 17%;
right: 7%;
bottom: 11%;
left: 7%;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.3fr);
gap: 0;
align-items: stretch;
height: auto;
padding: 0;
box-sizing: border-box;
}
.room-lobby-profile-left-pane,
.room-lobby-profile-right-pane {
border: 2px solid rgba(92, 233, 255, 0.65);
border-radius: 12px;
background: transparent;
padding: clamp(10px, 1.4vw, 16px);
box-sizing: border-box;
}
.room-lobby-profile-left-pane {
border: none;
border-radius: 0;
background: transparent;
padding: calc(3px * var(--rp-scale)) calc(4px * var(--rp-scale)) calc(5px * var(--rp-scale));
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) calc(58px * var(--rp-scale));
justify-items: center;
align-items: stretch;
row-gap: calc(10px * var(--rp-scale));
}
.room-lobby-profile-inner-frame {
width: min(100%, 98%);
max-width: 100%;
aspect-ratio: 621 / 626;
height: 100%;
max-height: 100%;
min-width: 0;
background: url(../img/03-6-Profile/profile-char-frame.png) center/100% 100% no-repeat;
--pf-scale: var(--rp-scale, 1);
font-size: calc(16px * var(--pf-scale));
padding: calc(12px * var(--pf-scale)) calc(11px * var(--pf-scale)) calc(9px * var(--pf-scale));
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
align-self: stretch;
overflow: hidden;
}
.room-lobby-profile-coins {
margin: calc(10px * var(--pf-scale)) 0 calc(10px * var(--pf-scale));
display: flex;
align-items: center;
justify-content: center;
gap: calc(8px * var(--pf-scale));
font-size: calc(30px * var(--pf-scale));
font-weight: 700;
width: 100%;
text-align: center;
line-height: 1;
}
.room-lobby-profile-coins strong {
color: #fff;
font-size: calc(48px * var(--pf-scale));
font-weight: 700;
line-height: 1;
}
.room-lobby-profile-coins-icon {
width: calc(30px * var(--pf-scale));
height: calc(30px * var(--pf-scale));
}
.room-lobby-profile-coins-label {
height: calc(62px * var(--pf-scale));
width: auto;
object-fit: contain;
margin-top: calc(6px * var(--pf-scale));
}
.room-lobby-profile-char-card {
border: none;
border-radius: 0;
width: 100%;
min-width: 0;
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: calc(3px * var(--pf-scale));
background: transparent;
padding: calc(4px * var(--pf-scale)) 0 calc(6px * var(--pf-scale));
box-sizing: border-box;
overflow: hidden;
}
.room-lobby-profile-char-card #room-lobby-profile-overlay-avatar {
width: 100%;
max-width: 100%;
height: auto;
object-fit: contain;
max-height: 83%;
margin-top: calc(-28px * var(--pf-scale));
margin-bottom: calc(-9px * var(--pf-scale));
}
.room-lobby-profile-overlay-name {
margin-top: 0;
font-size: calc(38px * var(--pf-scale));
font-weight: 700;
color: #fff;
line-height: 1;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
width: 100%;
max-width: 100%;
min-width: 0;
flex: 1 1 auto;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.room-lobby-profile-overlay-name-row {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
gap: calc(6px * var(--pf-scale));
width: 100%;
min-width: 0;
padding-left: calc(92px * var(--pf-scale));
padding-right: calc(92px * var(--pf-scale));
box-sizing: border-box;
margin-top: calc(-6px * var(--pf-scale));
flex-shrink: 0;
min-height: calc(70px * var(--pf-scale));
overflow: visible;
}
.room-lobby-profile-edit-btn {
padding: 0;
margin: 0;
position: absolute;
right: 4%;
top: 50%;
transform: translateY(-50%);
width: calc(88px * var(--pf-scale));
height: calc(88px * var(--pf-scale));
aspect-ratio: 1 / 1;
border: none;
border-radius: 0;
background: transparent;
cursor: pointer;
}
.room-lobby-profile-edit-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-name-sep {
margin-top: calc(-10px * var(--pf-scale));
width: min(100%, 505px);
height: auto;
object-fit: contain;
}
.room-lobby-profile-overlay-agent {
margin-top: calc(4px * var(--pf-scale));
font-size: calc(18px * var(--pf-scale));
color: #F856E2;
font-weight: 500;
line-height: 1;
margin-bottom: calc(4px * var(--pf-scale));
flex-shrink: 0;
}
.room-lobby-profile-logout {
margin-top: 0;
width: min(50%, calc(256px * var(--rp-scale)));
max-width: calc(256px * var(--rp-scale));
aspect-ratio: 256 / 94;
height: auto;
border-radius: 0;
border: none;
background: transparent;
color: inherit;
font-size: 0;
font-weight: 400;
cursor: pointer;
align-self: center;
display: inline-flex;
align-items: center;
justify-content: center;
}
.room-lobby-profile-logout img {
width: 100%;
height: 100%;
object-fit: fill;
display: block;
}
.room-lobby-profile-right-pane {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: calc(14px * var(--rp-scale));
padding-top: calc(6px * var(--rp-scale));
margin-left: calc(-32px * var(--rp-scale));
min-width: 0;
border: none;
border-radius: 0;
background: transparent;
padding: 0;
}
.room-lobby-profile-archiv-groups {
display: flex;
align-items: flex-end;
justify-content: flex-start;
padding-left: calc(30px * var(--rp-scale));
height: calc(60px * var(--rp-scale));
overflow: visible;
}
.room-lobby-profile-archiv-group-btn {
width: calc(190px * var(--rp-scale));
height: calc(60px * var(--rp-scale));
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.room-lobby-profile-archiv-group-btn + .room-lobby-profile-archiv-group-btn {
margin-left: calc(-14px * var(--rp-scale));
}
.room-lobby-profile-archiv-group-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-frame {
position: relative;
width: min(100%, calc(872px * var(--rp-scale)));
aspect-ratio: 872 / 515;
max-height: 100%;
background: url(../img/03-6-Profile/archiv-frame.png) center/100% 100% no-repeat;
padding: 4.8% 6.5% 5.8% 4.2%;
box-sizing: border-box;
}
.room-lobby-profile-achievements-list {
list-style: none;
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: calc(12px * var(--rp-scale));
overflow: hidden;
}
.room-lobby-profile-archiv-item {
display: flex;
justify-content: flex-start;
align-items: center;
gap: calc(14px * var(--rp-scale));
min-height: 0;
}
.room-lobby-profile-archiv-icon-bg {
width: calc(131px * var(--rp-scale));
height: calc(123px * var(--rp-scale));
aspect-ratio: 131 / 123;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-main {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: calc(6px * var(--rp-scale));
}
.room-lobby-profile-archiv-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: clamp(8px, 0.8vw, 12px);
}
.room-lobby-profile-archiv-name {
width: calc(421px * var(--rp-scale));
height: calc(72px * var(--rp-scale));
max-width: 100%;
aspect-ratio: 421 / 72;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-count {
color: #ffe95a;
font-size: calc(38px * var(--rp-scale));
line-height: 1;
font-weight: 700;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
}
.room-lobby-profile-archiv-progress-wrap {
position: relative;
width: calc(571px * var(--rp-scale));
max-width: 100%;
aspect-ratio: 571 / 56;
height: calc(56px * var(--rp-scale));
}
.room-lobby-profile-archiv-progress-bg,
.room-lobby-profile-archiv-progress {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-progress {
clip-path: inset(0 calc(100% - var(--archiv-progress, 0%)) 0 0);
}
.room-lobby-profile-archiv-line {
width: 100%;
height: auto;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-scroll {
position: absolute;
top: 13%;
right: 1.6%;
bottom: 7%;
width: calc(15px * var(--rp-scale));
display: flex;
justify-content: center;
}
.room-lobby-profile-archiv-scroll-bg,
.room-lobby-profile-archiv-scroll-bar {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-archiv-scroll-bg {
top: 0;
bottom: 0;
height: 100%;
}
.room-lobby-profile-archiv-scroll-bar {
top: 8%;
height: 42%;
}
.room-lobby-profile-settings {
position: relative;
width: min(100%, calc(824px * var(--rp-scale)));
aspect-ratio: 824 / 169;
margin-top: auto;
}
.room-lobby-profile-settings-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.room-lobby-profile-settings-row {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 calc(70px * var(--rp-scale)) 0 calc(90px * var(--rp-scale));
box-sizing: border-box;
}
.room-lobby-profile-setting-item {
display: inline-flex;
align-items: center;
gap: calc(14px * var(--rp-scale));
}
.room-lobby-profile-setting-label {
width: calc(99px * var(--rp-scale));
height: calc(39px * var(--rp-scale));
aspect-ratio: 99 / 39;
object-fit: contain;
display: block;
}
.room-lobby-profile-toggle {
margin: 0;
width: calc(152px * var(--rp-scale));
height: calc(94px * var(--rp-scale));
aspect-ratio: 152 / 94;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.room-lobby-profile-toggle img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
/* Profile overlay — see profile-popup.css (linked from room-lobby.html) */
/* Host Console popup */
.room-lobby-host-console-overlay {
@@ -2166,21 +1706,29 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
bottom: max(0.65rem, env(safe-area-inset-bottom));
right: max(0.85rem, env(safe-area-inset-right));
display: flex;
flex-direction: column;
align-items: flex-end;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 1rem;
gap: 0.75rem;
}
/* แถวเดียว: อันดับ | ไมค์ | หลักฐาน (ตรง production) */
.room-lobby-br-fixed .lobby-b-extra-row:not(.is-hidden) {
display: contents;
}
.room-lobby-br-fixed .lobby-b-extra-row {
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: flex-end;
gap: 0.6rem;
order: 1;
position: static !important;
margin: 0 !important;
transform: none !important;
z-index: auto;
}
.room-lobby-br-fixed #lobby-b-btn-rank {
order: 1;
}
.room-lobby-br-fixed #btn-voice {
order: 2;
}
.room-lobby-br-fixed #lobby-b-btn-evidence {
order: 3;
}
.room-lobby-br-fixed .btn-voice-icon {
order: 2;
@@ -2188,13 +1736,20 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
margin: 0 !important;
transform: none !important;
}
.room-lobby-br-fixed .lobby-b-extra-row {
z-index: auto;
.room-lobby-br-fixed .lobby-b-sq-btn,
.room-lobby-br-fixed .btn-voice-icon {
width: var(--lobby-br-btn);
height: var(--lobby-br-btn);
min-width: var(--lobby-br-btn);
min-height: var(--lobby-br-btn);
max-width: var(--lobby-br-btn);
max-height: var(--lobby-br-btn);
}
.room-lobby-chat-peers.chat-panel-collapsed.chat-panel {
width: 122px;
height: 120px;
bottom: 12rem;
right: max(0.85rem, env(safe-area-inset-right));
}
.room-lobby-chat-peers.chat-panel-collapsed .chat-close-btn {
width: 122px;
@@ -2884,7 +2439,7 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
font-size: 0.8rem;
}
/* ซ่อนชั่วคราว: ปุ่มตั้งค่าโฮสต์ + ปรับแต่ง (มุมขวาบน) */
/* design 2026-05: แสดงปุ่มตั้งค่าโฮสต์ + ปรับแต่ง มุมขวาบน (ตาม TryChangeCSS/CSS ใหม่ — ไม่ใช้ JS เก่า) */
body.room-lobby-page .room-lobby-top-right-actions {
display: none !important;
}
@@ -0,0 +1,16 @@
LOCALHOST ONLY — does not change production pages.
1) Edit YOUR LOCAL file (not production server):
C:\AppServ\www\srv1361159.hstgr.cloud\Game\room-lobby.html
2) Find the first <script ...> near end of file (before app-base.js).
3) Insert this line ABOVE app-base.js:
<script src="https://srv1361159.hstgr.cloud/Game/dev-local/bootstrap.js"></script>
4) Save. Ctrl+Shift+R on room-lobby.
Production https://srv1361159.hstgr.cloud/ must NOT include this script tag.
Create Room (local): same bootstrap in Create Room\index.html if needed.
+46
View File
@@ -0,0 +1,46 @@
/**
* LOCALHOST ONLY — do NOT add this script to production room-lobby.html
* AppServ: add before other scripts in your LOCAL copy of room-lobby.html:
* <script src="https://srv1361159.hstgr.cloud/Game/dev-local/bootstrap.js"></script>
*/
(function (g) {
'use strict';
var h = (g.location && g.location.hostname || '').toLowerCase();
if (h !== 'localhost' && h !== '127.0.0.1') return;
var PROD = 'https://srv1361159.hstgr.cloud';
var PROD_GAME = PROD + '/Game';
g.GAME_NODE_ORIGIN = PROD;
g.GAME_SERVER = PROD;
g.GAME_API_FETCH_BASE = PROD_GAME;
g.__JD_GAME_API_BASE__ = PROD_GAME;
try {
if (g.localStorage.getItem('isLoggedIn') !== 'true') {
g.localStorage.setItem('isLoggedIn', 'true');
g.localStorage.setItem('loginType', 'guest');
}
if (!(g.localStorage.getItem('playerName') || '').trim()) {
g.localStorage.setItem('playerName', 'DevTester');
}
} catch (e) { /* ignore */ }
if (typeof g.fetch === 'function' && !g.__JD_DEV_LOCAL_FETCH__) {
g.__JD_DEV_LOCAL_FETCH__ = 1;
var nativeFetch = g.fetch.bind(g);
var bad = /^https?:\/\/(?:localhost|127\.0\.0\.1):(3004|3001|3010|13010)\/Game/i;
g.fetch = function (input, init) {
if (typeof input === 'string' && bad.test(input)) {
input = PROD_GAME + input.slice(input.indexOf('/Game') + 5);
}
return nativeFetch(input, init);
};
}
if (!g.io) {
document.write('<script src="' + PROD + '/Game/socket.io/socket.io.js"><\/script>');
}
g.jdDevLocalBootstrapLoaded = true;
})(typeof window !== 'undefined' ? window : this);
+2 -1
View File
@@ -103,6 +103,7 @@
<option value="cellImage">รูปบนกริด (อัปโหลด / เลือกจากคลัง)</option>
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
<option value="customizeSpot">จุดห้องแต่งตัว (เดินไปกด F — คลิกซ้ายวาง / ขวาลบ)</option>
<option value="hostConsoleSpot" id="draw-mode-option-host-console">จุดตั้งค่าโฮสต์ (Host กด F — คลิกซ้ายวาง / ขวาลบ)</option>
<option value="spawnArea" id="draw-mode-option-spawn-area">พื้นที่สุ่มจุดเกิด (ฟ้า — ผู้เล่นใหม่สุ่มในช่องนี้)</option>
<option value="lobbyPlayerSpawn" id="draw-mode-option-lobby-spawn" hidden>จุดเกิดผู้เล่น 1–6 (ลำดับเข้า P1…P6)</option>
<option value="startGame" id="draw-mode-option-start-game">พื้นที่เริ่มเกม (host ยืนแล้วกดเริ่มได้)</option>
@@ -399,7 +400,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0334"></script>
<script src="js/editor.js?v=0.0335"></script>
<div class="version-tag">v —</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+9 -2
View File
@@ -3,14 +3,21 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Game</title>
<title>Game — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>Game</h1>
<p><a href="character.html">เลือกตัวละคร</a> · <a href="lobby.html">ล็อบบี้ (สร้างห้อง/เข้าร่วม)</a> · <a href="game-timing-admin.html">ตั้งค่าเวลาเกม (Admin)</a></p>
<p>
<a href="/Login/">เข้าสู่ระบบ</a> ·
<a href="/Main-Lobby/">Main Lobby</a> ·
<a href="/Main-Menu/">เมนูหลัก (สร้าง/เข้าห้อง)</a> ·
<a href="character.html">เลือกตัวละคร</a> ·
<a href="game-timing-admin.html">ตั้งค่าเวลาเกม (Admin)</a>
</p>
</div>
<script src="/app-base.js?v=2"></script>
<script src="js/version.js?v=0.0104"></script>
<div class="version-tag">v —</div>
</body>
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
(function (global) {
'use strict';
var DISPLAY_NAME_KEY = 'roomLobbyDisplayName';
var PLAYER_NAME_KEY = 'playerName';
var MAX_LEN = 24;
function sanitizeDisplayName(raw) {
var s = String(raw == null ? '' : raw).trim();
if (!s) return '';
try {
s = s.normalize('NFKC');
} catch (e) { /* ignore */ }
s = s.replace(/[\x00-\x1f\x7f]/g, '');
if (s.length > MAX_LEN) s = s.slice(0, MAX_LEN);
return s;
}
function readDisplayName() {
try {
return (
localStorage.getItem(DISPLAY_NAME_KEY) ||
localStorage.getItem(PLAYER_NAME_KEY) ||
'MONE'
).trim() || 'MONE';
} catch (e) {
return 'MONE';
}
}
function syncDisplayNameDom(displayName) {
var text = String(displayName || '').trim();
if (!text) return;
var upper = text.toUpperCase();
var ids = [
'lobby-profile-name',
'room-lobby-profile-overlay-name',
'room-lobby-profile-name'
];
ids.forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = upper;
});
try {
document.querySelectorAll('[data-jd-display-name]').forEach(function (el) {
el.textContent = upper;
});
} catch (e2) { /* ignore */ }
}
function replaceNickQueryParam(displayName) {
try {
var u = new URL(global.location.href);
if (!u.searchParams.has('nick')) return;
u.searchParams.set('nick', displayName);
global.history.replaceState({}, '', u.pathname + u.search + u.hash);
} catch (e) { /* ignore */ }
}
/**
* @param {string} nextName
* @param {{ socket?: object, silent?: boolean }} [options]
* @returns {{ ok: boolean, displayName?: string, error?: string }}
*/
function applyDisplayNameChange(nextName, options) {
var safe = sanitizeDisplayName(nextName);
if (!safe) return { ok: false, error: 'empty' };
var opts = options || {};
try {
localStorage.setItem(DISPLAY_NAME_KEY, safe);
localStorage.setItem(PLAYER_NAME_KEY, safe);
} catch (e) { /* ignore */ }
syncDisplayNameDom(safe);
replaceNickQueryParam(safe);
try {
global.dispatchEvent(new CustomEvent('jd-display-name-changed', {
detail: { displayName: safe }
}));
} catch (e2) { /* ignore */ }
var sock = opts.socket || global.__jdGameSocket || null;
if (sock && typeof sock.emit === 'function' && sock.connected) {
sock.emit('update-display-name', { nickname: safe }, function (res) {
if (res && res.ok && res.nickname) {
try {
localStorage.setItem(DISPLAY_NAME_KEY, res.nickname);
localStorage.setItem(PLAYER_NAME_KEY, res.nickname);
} catch (e3) { /* ignore */ }
syncDisplayNameDom(res.nickname);
}
});
}
return { ok: true, displayName: safe };
}
var editorReady = false;
function ensureNameEditorDom() {
if (editorReady && document.getElementById('jd-display-name-editor-overlay')) return;
if (!document.getElementById('jd-display-name-editor-style')) {
var st = document.createElement('style');
st.id = 'jd-display-name-editor-style';
st.textContent = '' +
'.jd-dne-overlay{position:fixed;inset:0;z-index:10080;display:flex;align-items:center;justify-content:center;padding:16px;}' +
'.jd-dne-overlay.is-hidden{display:none!important;}' +
'.jd-dne-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.72);}' +
'.jd-dne-dialog{position:relative;z-index:1;width:min(420px,calc(100vw - 32px));padding:22px 20px 18px;border-radius:14px;' +
'background:linear-gradient(180deg,#0d2848 0%,#061528 100%);border:2px solid rgba(56,232,255,.85);box-shadow:0 0 28px rgba(56,232,255,.35);color:#eaf8ff;font-family:Kanit,Segoe UI,sans-serif;}' +
'.jd-dne-title{margin:0 0 14px;font-size:1.25rem;font-weight:700;text-align:center;letter-spacing:.02em;}' +
'.jd-dne-input{display:block;width:100%;box-sizing:border-box;padding:12px 14px;border-radius:10px;border:2px solid rgba(56,232,255,.55);' +
'background:rgba(0,20,40,.85);color:#fff;font-size:1.05rem;font-family:inherit;outline:none;}' +
'.jd-dne-input:focus{border-color:#5ce9ff;box-shadow:0 0 0 3px rgba(92,233,255,.25);}' +
'.jd-dne-hint{margin:8px 0 0;font-size:.82rem;opacity:.75;text-align:center;}' +
'.jd-dne-error{margin:8px 0 0;font-size:.85rem;color:#ff8a8a;text-align:center;min-height:1.1em;}' +
'.jd-dne-actions{display:flex;gap:10px;margin-top:18px;justify-content:center;}' +
'.jd-dne-btn{min-width:110px;padding:10px 16px;border-radius:10px;border:2px solid transparent;font-size:1rem;font-weight:600;font-family:inherit;cursor:pointer;}' +
'.jd-dne-btn--cancel{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.25);color:#e2e8f0;}' +
'.jd-dne-btn--confirm{background:linear-gradient(180deg,#2ee6ff,#0ea5c9);border-color:#7df4ff;color:#042033;}';
document.head.appendChild(st);
}
if (!document.getElementById('jd-display-name-editor-overlay')) {
var wrap = document.createElement('div');
wrap.id = 'jd-display-name-editor-overlay';
wrap.className = 'jd-dne-overlay is-hidden';
wrap.setAttribute('aria-hidden', 'true');
wrap.innerHTML = '' +
'<div class="jd-dne-backdrop" id="jd-dne-backdrop" aria-hidden="true"></div>' +
'<div class="jd-dne-dialog" role="dialog" aria-modal="true" aria-labelledby="jd-dne-title">' +
' <h2 class="jd-dne-title" id="jd-dne-title">แก้ไขชื่อผู้เล่น</h2>' +
' <input type="text" id="jd-dne-input" class="jd-dne-input" maxlength="24" autocomplete="nickname" aria-label="ชื่อผู้เล่น">' +
' <p class="jd-dne-hint">สูงสุด 24 ตัวอักษร — ชื่อนี้จะแสดงในเกม</p>' +
' <p class="jd-dne-error" id="jd-dne-error" role="alert"></p>' +
' <div class="jd-dne-actions">' +
' <button type="button" class="jd-dne-btn jd-dne-btn--cancel" id="jd-dne-cancel">ยกเลิก</button>' +
' <button type="button" class="jd-dne-btn jd-dne-btn--confirm" id="jd-dne-confirm">ยืนยัน</button>' +
' </div>' +
'</div>';
document.body.appendChild(wrap);
}
editorReady = true;
}
/**
* เปิดกล่องแก้ชื่อ (ใช้แทน window.prompt — ทำงานใน Cursor browser / mobile)
* @param {{ initialValue?: string, title?: string }} [options]
* @returns {Promise<string|null>} ชื่อใหม่ หรือ null ถ้ายกเลิก
*/
function openDisplayNameEditor(options) {
ensureNameEditorDom();
var opts = options || {};
var overlay = document.getElementById('jd-display-name-editor-overlay');
var input = document.getElementById('jd-dne-input');
var errEl = document.getElementById('jd-dne-error');
var btnOk = document.getElementById('jd-dne-confirm');
var btnCancel = document.getElementById('jd-dne-cancel');
var backdrop = document.getElementById('jd-dne-backdrop');
var titleEl = document.getElementById('jd-dne-title');
if (!overlay || !input || !btnOk || !btnCancel) {
return Promise.resolve(null);
}
return new Promise(function (resolve) {
var settled = false;
function finish(value) {
if (settled) return;
settled = true;
overlay.classList.add('is-hidden');
overlay.setAttribute('aria-hidden', 'true');
document.removeEventListener('keydown', onDocKey, true);
resolve(value);
}
function onDocKey(e) {
if (overlay.classList.contains('is-hidden')) return;
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
finish(null);
}
}
function submit() {
if (errEl) errEl.textContent = '';
var safe = sanitizeDisplayName(input.value);
if (!safe) {
if (errEl) errEl.textContent = 'กรุณากรอกชื่อผู้เล่น';
input.focus();
return;
}
finish(safe);
}
if (titleEl && opts.title) titleEl.textContent = opts.title;
input.value = opts.initialValue != null ? String(opts.initialValue) : readDisplayName();
if (errEl) errEl.textContent = '';
overlay.classList.remove('is-hidden');
overlay.setAttribute('aria-hidden', 'false');
document.addEventListener('keydown', onDocKey, true);
btnOk.onclick = function (e) {
e.preventDefault();
submit();
};
btnCancel.onclick = function (e) {
e.preventDefault();
finish(null);
};
backdrop.onclick = function () {
finish(null);
};
input.onkeydown = function (e) {
if (e.key === 'Enter') {
e.preventDefault();
submit();
}
};
requestAnimationFrame(function () {
input.focus();
input.select();
});
});
}
/**
* เปิด editor แล้ว apply ชื่อ (socket sync ถ้ามี)
* @param {{ socket?: object }} [options]
* @returns {Promise<{ ok: boolean, displayName?: string, cancelled?: boolean }>}
*/
function promptAndApplyDisplayName(options) {
var opts = options || {};
return openDisplayNameEditor({ initialValue: readDisplayName() }).then(function (draft) {
if (draft == null) return { ok: false, cancelled: true };
return applyDisplayNameChange(draft, opts);
});
}
global.jdDisplayName = {
key: DISPLAY_NAME_KEY,
read: readDisplayName,
sanitize: sanitizeDisplayName,
syncDom: syncDisplayNameDom,
apply: applyDisplayNameChange,
openEditor: openDisplayNameEditor,
promptAndApply: promptAndApplyDisplayName
};
global.jdApplyDisplayNameChange = applyDisplayNameChange;
global.jdReadDisplayName = readDisplayName;
global.jdOpenDisplayNameEditor = openDisplayNameEditor;
global.jdPromptAndApplyDisplayName = promptAndApplyDisplayName;
})(typeof window !== 'undefined' ? window : this);
+35
View File
@@ -131,6 +131,7 @@
let balloonBossPlayerSlots = [];
let balloonBossBossSpawn = null;
let customizeSpot = null; // จุด "ห้องแต่งตัว" ในฉาก (เดินไปกด F) — วางเองในeditor
let hostConsoleSpot = null; // จุด "ตั้งค่าโฮสต์" (Host Console) — วางใน editor
let showMapInGame = true;
let backgroundImage = null;
let backgroundImageImg = null;
@@ -2527,6 +2528,19 @@
ctx.fillText('แต่งตัว', tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (hostConsoleSpot && hostConsoleSpot.x === x && hostConsoleSpot.y === y) {
ctx.fillStyle = 'rgba(236, 72, 153, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 140, 200, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = '#fff0f8';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('โฮสต์', tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (gtDraw === 'quiz_carry') {
if (quizQuestionArea[y] && quizQuestionArea[y][x] === 1) {
ctx.fillStyle = 'rgba(255, 214, 102, 0.44)';
@@ -3097,6 +3111,19 @@
if (statusEl) statusEl.textContent = 'ลบจุดห้องแต่งตัว';
}
return;
} else if (drawModeEl.value === 'hostConsoleSpot') {
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดตั้งค่าโฮสต์ไม่ได้';
return;
}
if (left) {
hostConsoleSpot = { x: x, y: y };
if (statusEl) statusEl.textContent = 'จุดตั้งค่าโฮสต์ → ช่อง (' + x + ',' + y + ')';
} else {
hostConsoleSpot = null;
if (statusEl) statusEl.textContent = 'ลบจุดตั้งค่าโฮสต์';
}
return;
} else if (drawModeEl.value === 'quizBattlePath') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
ensureQuizBattlePathArea();
@@ -3730,6 +3757,9 @@
customizeSpot: customizeSpot && Number.isFinite(customizeSpot.x)
? { x: Math.floor(customizeSpot.x), y: Math.floor(customizeSpot.y) }
: null,
hostConsoleSpot: hostConsoleSpot && Number.isFinite(hostConsoleSpot.x)
? { x: Math.floor(hostConsoleSpot.x), y: Math.floor(hostConsoleSpot.y) }
: null,
gridImageLibrary: gridImageLibrary.map(serializeGridLibEntry).filter((x) => x != null),
gridImageSprites: gridImageSprites.map((s) => {
const o = { i: s.i, x: s.x, y: s.y, w: s.w, h: s.h };
@@ -4035,6 +4065,11 @@
} else {
customizeSpot = null;
}
if (m.hostConsoleSpot && Number.isFinite(Number(m.hostConsoleSpot.x)) && Number.isFinite(Number(m.hostConsoleSpot.y))) {
hostConsoleSpot = { x: Math.floor(Number(m.hostConsoleSpot.x)), y: Math.floor(Number(m.hostConsoleSpot.y)) };
} else {
hostConsoleSpot = null;
}
stackReleaseArea = m.stackReleaseArea && m.stackReleaseArea.length ? m.stackReleaseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
stackLandArea = m.stackLandArea && m.stackLandArea.length ? m.stackLandArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
cellColors = (m.cellColors && m.cellColors.length === height) ? m.cellColors.map(r => r && r.slice ? r.slice() : Array(width).fill(null)) : Array(height).fill(0).map(() => Array(width).fill(null));
+96
View File
@@ -0,0 +1,96 @@
/**
* Game API / Socket routing
* - Production: same-origin /Game/api (Nginx → Node)
* - localhost AppServ: production https://srv1361159.hstgr.cloud/Game (ไม่ต้องรัน node บน Windows)
*/
(function (g) {
'use strict';
var loc = g.location || {};
var pageBase = typeof g.appPath === 'function' ? g.appPath('/Game') : '/Game';
var PROD = 'https://srv1361159.hstgr.cloud';
var PROD_GAME = PROD + '/Game';
function isLocalDevHost() {
try {
var h = (loc.hostname || '').toLowerCase();
return h === 'localhost' || h === '127.0.0.1';
} catch (e) {
return false;
}
}
function useLocalNode() {
try { return localStorage.getItem('jdUseLocalGameApi') === '1'; } catch (e) { return false; }
}
function localApiBase() {
var port = '13010';
try {
var p = localStorage.getItem('jdGameNodePort');
if (p) port = String(p).replace(/\D/g, '') || port;
} catch (e) { /* ignore */ }
return 'http://127.0.0.1:' + port + '/Game';
}
var localDev = isLocalDevHost();
var apiBase = pageBase.replace(/\/$/, '');
var nodeOrigin = '';
if (localDev) {
if (useLocalNode()) {
apiBase = localApiBase().replace(/\/$/, '');
nodeOrigin = apiBase.replace(/\/Game\/?$/i, '');
} else {
apiBase = PROD_GAME;
nodeOrigin = PROD;
}
}
g.GAME_HTTP_BASE = pageBase;
g.GAME_API_FETCH_BASE = apiBase;
g.GAME_NODE_ORIGIN = localDev ? nodeOrigin : (String(g.GAME_SERVER || '').replace(/\/$/, '') || loc.origin || '');
if (localDev) g.GAME_SERVER = nodeOrigin;
g.ensureLocalDevLogin = function ensureLocalDevLogin() {
if (!localDev) return;
try {
if (localStorage.getItem('isLoggedIn') !== 'true') {
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('loginType', 'guest');
}
if (!(localStorage.getItem('playerName') || '').trim()) {
localStorage.setItem('playerName', 'DevTester');
}
if (!(localStorage.getItem('roomLobbyDisplayName') || '').trim()) {
localStorage.setItem('roomLobbyDisplayName', 'DevTester');
}
} catch (e) { /* ignore */ }
};
g.resolveGameApiBase = function resolveGameApiBase() {
if (!localDev) return pageBase.replace(/\/$/, '');
return String(g.GAME_API_FETCH_BASE || PROD_GAME).replace(/\/$/, '');
};
if (!localDev || g.__GAME_HTTP_FETCH_PATCHED__ || typeof g.fetch !== 'function') return;
var nativeFetch = g.fetch.bind(g);
var pageApiPrefix = pageBase.replace(/\/$/, '') + '/api';
var directApiBase = apiBase.replace(/\/$/, '');
var badPort = /https?:\/\/(?:localhost|127\.0\.0\.1):(3004|3001|3010|13010)\/Game/i;
g.fetch = function gameHttpFetch(input, init) {
if (typeof input === 'string') {
if (badPort.test(input)) {
input = PROD_GAME + input.slice(input.indexOf('/Game') + 5);
} else if (input.indexOf(pageApiPrefix) === 0) {
input = directApiBase + input.slice(pageBase.length);
} else if (input.indexOf('/Game/api') === 0) {
input = directApiBase + input.slice('/Game'.length);
}
}
return nativeFetch(input, init);
};
g.__GAME_HTTP_FETCH_PATCHED__ = true;
})(typeof window !== 'undefined' ? window : this);
+68 -7
View File
@@ -1,5 +1,21 @@
(function () {
const BASE = '/Game';
function playDesignMainMenuHref() {
try {
if (typeof globalThis !== 'undefined' && typeof globalThis.designMainMenuHref === 'function') {
return globalThis.designMainMenuHref();
}
} catch (e) { /* ignore */ }
return '/Main-Menu/';
}
function playDesignMainLobbyHref() {
try {
if (typeof globalThis !== 'undefined' && typeof globalThis.designMainLobbyHref === 'function') {
return globalThis.designMainLobbyHref();
}
} catch (e) { /* ignore */ }
return '/Main-Lobby/';
}
/** รูปเลข 3–2–1 — เสิร์ฟที่ /Game/img/QUESTION/ (nginx alias /Game/ → public; อย่าใช้ /Game/public/img/) */
const COUNTDOWN_321_IMG_BASE = BASE + '/img/QUESTION/';
/** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */
@@ -11,7 +27,12 @@
const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400;
const params = new URLSearchParams(window.location.search);
const spaceId = params.get('space');
const nick = params.get('nick') || 'ผู้เล่น';
function readStoredDisplayName() {
try {
return (localStorage.getItem('roomLobbyDisplayName') || localStorage.getItem('playerName') || '').trim();
} catch (e) { return ''; }
}
let nick = (params.get('nick') || '').trim() || readStoredDisplayName() || 'ผู้เล่น';
const previewMode = params.get('preview') === '1';
const forceDefaultCharacter = params.get('defaultChar') === '1';
const editorEmbedReturn = params.get('editorEmbed') === '1';
@@ -493,9 +514,10 @@
return false;
}
if (!spaceId) { window.location.replace(BASE + '/lobby.html'); return; }
if (!spaceId) { window.location.replace(playDesignMainMenuHref()); return; }
const socket = io({ path: BASE + '/socket.io' });
window.__jdGameSocket = socket;
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
let mapData = null, tileSize = 32, myId = null;
@@ -1613,8 +1635,8 @@
const PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
const PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' };
const PLAY_TINT_HEAD = ['#eaa78a', '#fbd5c4', '#fae9e1'];
const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#ef62b9'];
const PLAY_TINT_BODY = ['#fb4941', '#feaa11', '#fefe6d', '#adfd85', '#45fbfd', '#799afe', '#f87dff', '#fec1fe'];
const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe'];
const PLAY_TINT_BODY = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe'];
const playLayerMode = {};
/** คิว probe ครบ up/down/left/right แล้ว — กันทิศที่ไม่มีไฟล์ layer ไปตก fallback แถบทั้งทั้งที่ทิศอื่นมี */
const playLayerAllDirsQueued = {};
@@ -14425,6 +14447,10 @@
me.y = pathSnap.y;
}
me.direction = myPeer?.direction ?? 'down';
if (myPeer && myPeer.nickname) {
me.nickname = String(myPeer.nickname).trim() || me.nickname;
nick = me.nickname;
}
me.characterId = myPeer?.characterId || null;
meGauntletJumpTicks = typeof myPeer?.gauntletJumpTicks === 'number' ? myPeer.gauntletJumpTicks : 0;
meGauntletJumpVis = meGauntletJumpTicks;
@@ -14746,9 +14772,12 @@
socket.on('connect', () => {
firstCharacterDefaultPromise.then(() => {
const joinPlayMapId = (params.get('map') || '').trim();
const joinNick = (typeof window.jdReadDisplayName === 'function' ? window.jdReadDisplayName() : null)
|| readStoredDisplayName()
|| nick;
socket.emit('join-space', {
spaceId,
nickname: nick,
nickname: joinNick,
characterId: getPlayCharacterId(),
playMapId: joinPlayMapId || undefined,
}, (res) => {
@@ -14759,7 +14788,7 @@
if (caseLocked) {
window.location.replace(buildRoomLobbyReturnHref());
} else {
window.location.replace(BASE + '/lobby.html');
window.location.replace(playDesignMainMenuHref());
}
return;
}
@@ -14827,6 +14856,38 @@
});
const LERP = 0.2;
function applyPlayDisplayNameUpdate(data) {
if (!data || !data.id) return;
const next = String(data.nickname || '').trim();
if (!next) return;
if (data.id === myId) {
me.nickname = next;
nick = next;
try {
localStorage.setItem('roomLobbyDisplayName', next);
localStorage.setItem('playerName', next);
} catch (e) { /* ignore */ }
if (window.jdDisplayName && typeof window.jdDisplayName.syncDom === 'function') {
window.jdDisplayName.syncDom(next);
}
} else {
const o = others.get(data.id);
if (o) o.nickname = next;
}
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (typeof syncPlayCyberHud === 'function') syncPlayCyberHud();
}
socket.on('peer-display-name', applyPlayDisplayNameUpdate);
window.addEventListener('jd-display-name-changed', function (ev) {
const next = ev && ev.detail && ev.detail.displayName ? String(ev.detail.displayName).trim() : '';
if (!next || !myId) return;
applyPlayDisplayNameUpdate({ id: myId, nickname: next });
});
socket.on('user-joined', (data) => {
if (!data || isPreviewBotId(data.id)) return;
const x = Number(data.x);
@@ -18719,7 +18780,7 @@
var edQ = '?id=' + encodeURIComponent(mid) + (editorEmbedReturn ? '&embed=1' : '');
window.location.replace(BASE + '/editor.html' + edQ);
} else {
window.location.replace(BASE + '/lobby.html');
window.location.replace(playDesignMainMenuHref());
}
});
(function wireQuizBattleMcqModal() {
+309
View File
@@ -0,0 +1,309 @@
(function () {
'use strict';
var script = document.currentScript;
var triggerSelector = (script && script.getAttribute('data-profile-trigger')) || '#btn-myprofile,#btn-profile-set';
var assetBase = (script && script.getAttribute('data-profile-asset-base')) || 'img/03-6-Profile';
var DISPLAY_NAME_KEY = 'roomLobbyDisplayName';
var overlayId = 'room-lobby-profile-overlay';
if (typeof window !== 'undefined') window.__USE_SHARED_PROFILE_POPUP = true;
function resolveAssetBase(raw) {
var base = String(raw || 'img/03-6-Profile').replace(/\/$/, '');
if (/^\/(Game|img)\//i.test(base)) {
return typeof window.appPath === 'function' ? window.appPath(base) : base;
}
try {
return new URL(base + '/', window.location.href).pathname.replace(/\/$/, '');
} catch (e) {
return base;
}
}
var resolvedAssetBase = resolveAssetBase(assetBase);
function asset(name) {
return resolvedAssetBase + '/' + name;
}
function appRel(path) {
if (typeof window.appPath === 'function') return window.appPath(path);
return path;
}
function readName() {
try { return (localStorage.getItem(DISPLAY_NAME_KEY) || localStorage.getItem('playerName') || 'MONE').trim() || 'MONE'; } catch (e) { return 'MONE'; }
}
function readAgentId() {
try { return (localStorage.getItem('agentDisplayId') || '001998').trim() || '001998'; } catch (e) { return '001998'; }
}
function readCoins() {
try { return String(Math.max(0, parseInt(localStorage.getItem('jdCoins') || '0', 10) || 0)); } catch (e) { return '0'; }
}
var refs = {
overlay: null,
backdrop: null,
dialog: null,
closeBtn: null,
avatarEl: null,
nameEl: null,
agentEl: null,
coinsEl: null,
musicBtn: null,
sfxBtn: null,
groupBtns: [],
archivListEl: null,
archivScrollEl: null,
archivScrollBarEl: null,
editBtn: null,
logoutBtn: null
};
function createMarkupIfNeeded() {
if (document.getElementById(overlayId)) return;
var overlay = document.createElement('div');
overlay.id = overlayId;
overlay.className = 'room-lobby-profile-overlay is-hidden';
overlay.setAttribute('aria-hidden', 'true');
overlay.innerHTML = '' +
'<div class="room-lobby-profile-backdrop" id="room-lobby-profile-backdrop" aria-hidden="true"></div>' +
'<div class="room-lobby-profile-dialog" role="dialog" aria-modal="true" aria-label="โปรไฟล์ผู้เล่น">' +
' <button type="button" class="room-lobby-profile-close" id="room-lobby-profile-close" aria-label="ปิดโปรไฟล์">×</button>' +
' <div class="room-lobby-profile-content">' +
' <section class="room-lobby-profile-left-pane">' +
' <div class="room-lobby-profile-inner-frame">' +
' <p class="room-lobby-profile-coins">' +
' <img src="' + asset('profile-pop-coin.png') + '" alt="My Coins" class="room-lobby-profile-coins-label" decoding="async">' +
' <strong id="room-lobby-profile-coins-val">150</strong>' +
' </p>' +
' <div class="room-lobby-profile-char-card">' +
' <img id="room-lobby-profile-overlay-avatar" src="" alt="ตัวละครผู้เล่น" decoding="async">' +
' <div class="room-lobby-profile-overlay-name-row">' +
' <div class="room-lobby-profile-overlay-name" id="room-lobby-profile-overlay-name">PLAYER</div>' +
' <button type="button" class="room-lobby-profile-edit-btn" id="room-lobby-profile-edit-btn" title="แก้ไขชื่อผู้เล่น" aria-label="แก้ไขชื่อผู้เล่น">' +
' <img src="' + asset('btn-edit.png') + '" alt="" decoding="async">' +
' </button>' +
' </div>' +
' <img src="' + asset('profile-char-frame-2.png') + '" alt="" class="room-lobby-profile-name-sep" decoding="async">' +
' <div class="room-lobby-profile-overlay-agent" id="room-lobby-profile-overlay-agent">AGENT ID : 001998</div>' +
' </div>' +
' </div>' +
' <button type="button" class="room-lobby-profile-logout" id="room-lobby-profile-logout" aria-label="ออกจากระบบ">' +
' <img src="' + asset('btn-ex.png') + '" alt="ออกจากระบบ" decoding="async">' +
' </button>' +
' </section>' +
' <section class="room-lobby-profile-right-pane">' +
' <div class="room-lobby-profile-archiv-groups" id="room-lobby-profile-archiv-groups">' +
' <button type="button" class="room-lobby-profile-archiv-group-btn is-active" data-group="1"><img src="' + asset('archiv-group-01-a.png') + '" alt=""></button>' +
' <button type="button" class="room-lobby-profile-archiv-group-btn" data-group="2"><img src="' + asset('archiv-group-02.png') + '" alt=""></button>' +
' <button type="button" class="room-lobby-profile-archiv-group-btn" data-group="3"><img src="' + asset('archiv-group-03.png') + '" alt=""></button>' +
' <button type="button" class="room-lobby-profile-archiv-group-btn" data-group="4"><img src="' + asset('archiv-group-04.png') + '" alt=""></button>' +
' <button type="button" class="room-lobby-profile-archiv-group-btn" data-group="5"><img src="' + asset('archiv-group-05.png') + '" alt=""></button>' +
' </div>' +
' <div class="room-lobby-profile-archiv-frame">' +
' <ul class="room-lobby-profile-achievements-list" id="room-lobby-profile-achievements-list">' +
' <li class="room-lobby-profile-archiv-item"><img class="room-lobby-profile-archiv-icon-bg" src="' + asset('archiv-icon-bg.png') + '" alt=""><div class="room-lobby-profile-archiv-main"><div class="room-lobby-profile-archiv-title-row"><img class="room-lobby-profile-archiv-name" src="' + asset('archiv-name-1.png') + '" alt=""><strong class="room-lobby-profile-archiv-count">1/1</strong></div><div class="room-lobby-profile-archiv-progress-wrap"><img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt=""><img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:100%"></div><img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt=""></div></li>' +
' <li class="room-lobby-profile-archiv-item"><img class="room-lobby-profile-archiv-icon-bg" src="' + asset('archiv-icon-bg.png') + '" alt=""><div class="room-lobby-profile-archiv-main"><div class="room-lobby-profile-archiv-title-row"><img class="room-lobby-profile-archiv-name" src="' + asset('archiv-name-2.png') + '" alt=""><strong class="room-lobby-profile-archiv-count">2/5</strong></div><div class="room-lobby-profile-archiv-progress-wrap"><img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt=""><img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:40%"></div><img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt=""></div></li>' +
' <li class="room-lobby-profile-archiv-item"><img class="room-lobby-profile-archiv-icon-bg" src="' + asset('archiv-icon-bg.png') + '" alt=""><div class="room-lobby-profile-archiv-main"><div class="room-lobby-profile-archiv-title-row"><img class="room-lobby-profile-archiv-name" src="' + asset('archiv-name-3.png') + '" alt=""><strong class="room-lobby-profile-archiv-count">3/10</strong></div><div class="room-lobby-profile-archiv-progress-wrap"><img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt=""><img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:30%"></div><img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt=""></div></li>' +
' <li class="room-lobby-profile-archiv-item"><img class="room-lobby-profile-archiv-icon-bg" src="' + asset('archiv-icon-bg.png') + '" alt=""><div class="room-lobby-profile-archiv-main"><div class="room-lobby-profile-archiv-title-row"><img class="room-lobby-profile-archiv-name" src="' + asset('archiv-name-1.png') + '" alt=""><strong class="room-lobby-profile-archiv-count">4/10</strong></div><div class="room-lobby-profile-archiv-progress-wrap"><img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt=""><img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:40%"></div><img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt=""></div></li>' +
' <li class="room-lobby-profile-archiv-item"><img class="room-lobby-profile-archiv-icon-bg" src="' + asset('archiv-icon-bg.png') + '" alt=""><div class="room-lobby-profile-archiv-main"><div class="room-lobby-profile-archiv-title-row"><img class="room-lobby-profile-archiv-name" src="' + asset('archiv-name-2.png') + '" alt=""><strong class="room-lobby-profile-archiv-count">5/10</strong></div><div class="room-lobby-profile-archiv-progress-wrap"><img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt=""><img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:50%"></div><img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt=""></div></li>' +
' </ul>' +
' <div class="room-lobby-profile-archiv-scroll is-hidden"><div class="room-lobby-profile-archiv-scroll-bg"></div><div class="room-lobby-profile-archiv-scroll-bar"></div></div>' +
' </div>' +
' <div class="room-lobby-profile-settings"><img class="room-lobby-profile-settings-frame" src="' + asset('setting-frame.png') + '" alt=""><div class="room-lobby-profile-settings-row">' +
' <div class="room-lobby-profile-setting-item"><img class="room-lobby-profile-setting-label" src="' + asset('setting-txt-1.png') + '" alt="Music"><button type="button" class="room-lobby-profile-toggle" id="room-lobby-profile-music" data-on="true"><img src="' + asset('btn-on.png') + '" alt=""></button></div>' +
' <div class="room-lobby-profile-setting-item"><img class="room-lobby-profile-setting-label" src="' + asset('setting-txt-2.png') + '" alt="SFX"><button type="button" class="room-lobby-profile-toggle" id="room-lobby-profile-sfx" data-on="false"><img src="' + asset('btn-off.png') + '" alt=""></button></div>' +
' </div></div>' +
' </section>' +
' </div>' +
'</div>';
document.body.appendChild(overlay);
}
function bindRefs() {
refs.overlay = document.getElementById(overlayId);
refs.backdrop = document.getElementById('room-lobby-profile-backdrop');
refs.dialog = refs.overlay ? refs.overlay.querySelector('.room-lobby-profile-dialog') : null;
refs.closeBtn = document.getElementById('room-lobby-profile-close');
refs.avatarEl = document.getElementById('room-lobby-profile-overlay-avatar');
refs.nameEl = document.getElementById('room-lobby-profile-overlay-name');
refs.agentEl = document.getElementById('room-lobby-profile-overlay-agent');
refs.coinsEl = document.getElementById('room-lobby-profile-coins-val');
refs.musicBtn = document.getElementById('room-lobby-profile-music');
refs.sfxBtn = document.getElementById('room-lobby-profile-sfx');
refs.groupBtns = Array.prototype.slice.call(document.querySelectorAll('.room-lobby-profile-archiv-group-btn'));
refs.archivListEl = document.getElementById('room-lobby-profile-achievements-list');
refs.archivScrollEl = document.querySelector('.room-lobby-profile-archiv-scroll');
refs.archivScrollBarEl = document.querySelector('.room-lobby-profile-archiv-scroll-bar');
refs.editBtn = document.getElementById('room-lobby-profile-edit-btn');
refs.logoutBtn = document.getElementById('room-lobby-profile-logout');
}
function syncArchivScroll() {
if (!refs.archivListEl || !refs.archivScrollEl || !refs.archivScrollBarEl) return;
var total = refs.archivListEl.scrollHeight || 0;
var visible = refs.archivListEl.clientHeight || 0;
var overflow = total > visible + 1;
refs.archivScrollEl.classList.toggle('is-hidden', !overflow);
if (!overflow) return;
var max = Math.max(1, total - visible);
var ratio = Math.max(0, Math.min(1, refs.archivListEl.scrollTop / max));
var barPct = Math.max(18, Math.min(42, (visible / Math.max(1, total)) * 100));
refs.archivScrollBarEl.style.height = barPct.toFixed(2) + '%';
refs.archivScrollBarEl.style.top = ((100 - barPct) * ratio).toFixed(2) + '%';
}
function syncProfileScale() {
if (!refs.dialog) return;
var rect = refs.dialog.getBoundingClientRect();
if (!rect.width || !rect.height) return;
var scaleW = rect.width / 1701;
var scaleH = rect.height / 951;
var scale = Math.max(0.35, Math.min(1, Math.min(scaleW, scaleH)));
refs.dialog.style.setProperty('--rp-scale', String(scale.toFixed(4)));
}
function setGroup(index) {
refs.groupBtns.forEach(function (btn) {
var id = parseInt(btn.getAttribute('data-group'), 10) || 1;
var active = id === index;
btn.classList.toggle('is-active', active);
var img = btn.querySelector('img');
if (img) img.src = active ? asset('archiv-group-0' + id + '-a.png') : asset('archiv-group-0' + id + '.png');
});
syncArchivScroll();
}
function toggleChip(btn) {
if (!btn) return;
var on = String(btn.getAttribute('data-on') || '').toLowerCase() === 'true';
on = !on;
btn.setAttribute('data-on', on ? 'true' : 'false');
var img = btn.querySelector('img');
if (img) img.src = on ? asset('btn-on.png') : asset('btn-off.png');
}
function setProfileData(data) {
var payload = data || {};
var lobbyAvatar = document.getElementById('lobby-profile-avatar');
var roomAvatar = document.getElementById('room-lobby-profile-avatar');
var src = payload.avatarSrc || (lobbyAvatar && lobbyAvatar.getAttribute('src')) || (roomAvatar && roomAvatar.getAttribute('src')) || '';
if (refs.avatarEl && src) refs.avatarEl.src = src;
if (refs.nameEl) refs.nameEl.textContent = (payload.displayName || readName()).toUpperCase();
if (refs.agentEl) refs.agentEl.textContent = payload.agentIdLabel || ('AGENT ID : ' + readAgentId());
if (refs.coinsEl) refs.coinsEl.textContent = String(payload.coins != null ? payload.coins : readCoins());
}
function openPopup(data) {
if (!refs.overlay) return;
setProfileData(data);
refs.overlay.classList.remove('is-hidden');
refs.overlay.setAttribute('aria-hidden', 'false');
syncProfileScale();
syncArchivScroll();
if (refs.closeBtn) refs.closeBtn.focus();
}
function closePopup() {
if (!refs.overlay) return;
refs.overlay.classList.add('is-hidden');
refs.overlay.setAttribute('aria-hidden', 'true');
}
function bindEvents() {
if (refs.backdrop) refs.backdrop.addEventListener('click', closePopup);
if (refs.closeBtn) refs.closeBtn.addEventListener('click', closePopup);
if (refs.musicBtn) refs.musicBtn.addEventListener('click', function () { toggleChip(refs.musicBtn); });
if (refs.sfxBtn) refs.sfxBtn.addEventListener('click', function () { toggleChip(refs.sfxBtn); });
refs.groupBtns.forEach(function (btn) {
btn.addEventListener('click', function () { setGroup(parseInt(btn.getAttribute('data-group'), 10) || 1); });
});
if (refs.archivListEl) refs.archivListEl.addEventListener('scroll', syncArchivScroll, { passive: true });
if (refs.editBtn) refs.editBtn.addEventListener('click', function () {
var runApply = function (draft) {
if (draft == null) return;
var applyFn = typeof window.jdApplyDisplayNameChange === 'function'
? window.jdApplyDisplayNameChange
: null;
if (applyFn) {
var result = applyFn(draft, { socket: window.__jdGameSocket || null });
if (!result.ok) return;
if (refs.nameEl) refs.nameEl.textContent = result.displayName.toUpperCase();
return;
}
try {
localStorage.setItem(DISPLAY_NAME_KEY, draft);
localStorage.setItem('playerName', draft);
} catch (e) { /* ignore */ }
if (refs.nameEl) refs.nameEl.textContent = draft.toUpperCase();
var headerName = document.getElementById('lobby-profile-name');
if (headerName) headerName.textContent = draft.toUpperCase();
};
if (window.jdDisplayName && typeof window.jdDisplayName.openEditor === 'function') {
window.jdDisplayName.openEditor({
initialValue: typeof window.jdReadDisplayName === 'function' ? window.jdReadDisplayName() : readName()
}).then(runApply);
return;
}
if (typeof window.jdPromptAndApplyDisplayName === 'function') {
window.jdPromptAndApplyDisplayName({ socket: window.__jdGameSocket || null }).then(function (result) {
if (result && result.ok && refs.nameEl) refs.nameEl.textContent = result.displayName.toUpperCase();
});
return;
}
var draft = window.prompt('แก้ไขชื่อผู้เล่น', readName() || '');
runApply(draft == null ? null : String(draft).trim() || null);
});
if (refs.logoutBtn) refs.logoutBtn.addEventListener('click', function () {
if (document.getElementById('btn-back')) {
window.location.href = appRel('/Login/');
return;
}
window.location.href = appRel('/Create%20Room/');
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
var nameEditor = document.getElementById('jd-display-name-editor-overlay');
if (nameEditor && !nameEditor.classList.contains('is-hidden')) return;
if (refs.overlay && !refs.overlay.classList.contains('is-hidden')) closePopup();
});
window.addEventListener('resize', function () {
syncProfileScale();
syncArchivScroll();
});
document.querySelectorAll(triggerSelector).forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
openPopup();
});
});
}
function isRoomLobbyPage() {
return /room-lobby\.html$/i.test(String(window.location.pathname || ''));
}
function init() {
if (isRoomLobbyPage()) {
return;
}
createMarkupIfNeeded();
bindRefs();
bindEvents();
syncProfileScale();
setGroup(1);
window.openProfilePopup = openPopup;
window.closeProfilePopup = closePopup;
window.roomLobbyProfileOverlay = { open: openPopup, close: closePopup };
window.mainLobbyProfileOverlay = { open: openPopup, close: closePopup };
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
+266 -154
View File
@@ -1,7 +1,7 @@
(function () {
// Error "message channel closed" มาจาก extension ของเบราว์เซอร์ ไม่ใช่โค้ดเกม — ลองโหมดไม่ระบุตัวตนหรือปิด extension
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
const SERVER = BASE;
const MAIN_LOBBY_AI_BTN_ICON = typeof appPath === 'function'
? appPath('/Main-Lobby/IMAGE/BTN-AI-ChatBOT.png')
: '/Main-Lobby/IMAGE/BTN-AI-ChatBOT.png';
@@ -10,7 +10,8 @@
const nick = (params.get('nick') || '').trim() || 'ผู้เล่น';
const DISPLAY_NAME_STORAGE_KEY = 'roomLobbyDisplayName';
let profileDisplayName = nick;
if (!spaceId) { location.href = BASE + '/lobby.html'; return; }
const DESIGN_MENU = typeof designMainMenuHref === 'function' ? designMainMenuHref() : '/Main-Menu/';
if (!spaceId) { location.href = DESIGN_MENU; return; }
const roomIdValueEl = document.getElementById('room-id-value');
const displayRoomParam = (params.get('displayRoom') || '').trim();
if (displayRoomParam) {
@@ -102,7 +103,19 @@
}
};
const socket = io(typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : undefined, { path: '/Game/socket.io' });
const socketOrigin = (typeof GAME_NODE_ORIGIN !== 'undefined' && GAME_NODE_ORIGIN)
? GAME_NODE_ORIGIN
: (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : undefined);
if (typeof io !== 'function') {
var rlErr = document.createElement('div');
rlErr.setAttribute('role', 'alert');
rlErr.style.cssText = 'position:fixed;inset:0;z-index:999999;display:flex;align-items:center;justify-content:center;padding:24px;background:#0a0e1a;color:#e8f4ff;font-family:Kanit,system-ui,sans-serif;text-align:center;line-height:1.6';
rlErr.innerHTML = '<div><p style="font-size:1.15rem;margin:0 0 12px">โหลด Socket ไม่สำเร็จ (localhost)</p><p style="opacity:.85;margin:0 0 16px">เพิ่มในไฟล์ <code>room-lobby.html</code> บนเครื่องคุณเท่านั้น:<br><code>&lt;script src=&quot;https://srv1361159.hstgr.cloud/Game/dev-local/bootstrap.js&quot;&gt;&lt;/script&gt;</code><br>ดู Game/dev-local/INSTALL-WINDOWS.txt</p><a href="' + (typeof designMainMenuHref === 'function' ? designMainMenuHref() : '/Main-Menu/') + '" style="color:#67e8f9">กลับเมนูหลัก</a></div>';
(document.body || document.documentElement).appendChild(rlErr);
throw new Error('socket.io not loaded');
}
const socket = io(socketOrigin, { path: '/Game/socket.io' });
window.__jdGameSocket = socket;
const roomPlayersHud = document.getElementById('room-players-hud');
const peersList = document.getElementById('peers-list');
const readyCheck = document.getElementById('ready-check');
@@ -132,6 +145,8 @@
let spaceName = '';
let maxPlayers = 10;
let lobbyBotSlotCount = 0;
/** จำนวนช่องรวมในโถง (คน + บอท) — ตรง server LOBBY_SLOT_TOTAL */
const LOBBY_SLOT_TOTAL = 6;
const LOBBY_BOT_PREFIX = '__lobby_bot_';
const lobbyBots = new Map();
let suspectPickOverlayOpen = false;
@@ -305,9 +320,23 @@
img.src = src;
}
function rlHexToRgbCss(hex) {
var h = String(hex || '').replace('#', '').trim();
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
var n = parseInt(h, 16);
if (!Number.isFinite(n)) return null;
return 'rgb(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ')';
}
function rlSampleSwatch(group, idx, cb) {
var key = group + '-' + idx;
if (rlSwatchCache[key]) { cb(rlSwatchCache[key]); return; }
var hexArr = group === 'color' ? ROOM_CZ_THEME_HEX : ROOM_CZ_SKIN_HEX;
var hi = parseInt(idx, 10) - 1;
if (hexArr && hexArr[hi]) {
var fromHex = rlHexToRgbCss(hexArr[hi]);
if (fromHex) { rlSwatchCache[key] = fromHex; cb(fromHex); return; }
}
rlLoadImg(RL_CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png', function (img) {
if (!img || !img.naturalWidth) { cb(null); return; }
try {
@@ -501,17 +530,25 @@
}
/* ===== ห้องแต่งตัว (Customize) ในห้อง room-lobby — ปุ่มล่างซ้าย + popup + tint สด ===== */
var ROOM_CZ_ASSET = SERVER + '/img/03-5-Customize/';
var ROOM_CZ_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 }
/** ธีมสี 8 ช่อง — ตรงกับ customize-popup / play.js */
var ROOM_CZ_THEME_HEX = [
'#d72520', '#ef8508', '#efe237', '#5bb443',
'#2585cb', '#3f4ead', '#b53fd6', '#fec1fe'
];
/** สีผิว 3 ช่อง */
var ROOM_CZ_SKIN_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1'];
/** จุด "ห้องแต่งตัว" ในฉาก — ตั้งจาก mapData.customizeSpot (วางใน editor) ถ้าไม่มี = null = ไม่แสดง */
var ROOM_CZ_SPOT = null;
var roomCzIcon = new Image();
roomCzIcon.src = '/Main-Lobby/IMAGE/btn-cloth.png';
roomCzIcon.onload = function () { if (typeof mapData !== 'undefined' && mapData && canvas) drawLobbyMap(); };
/** จุด "ตั้งค่าโฮสต์" — mapData.hostConsoleSpot (วางใน editor) */
var ROOM_HC_SPOT = null;
var roomHcIcon = new Image();
roomHcIcon.src = SERVER + '/img/btn-setting-host.png';
roomHcIcon.onload = function () { if (typeof mapData !== 'undefined' && mapData && canvas) drawLobbyMap(); };
function markRoomCzInteractiveCell() {
if (!mapData) return;
if (mapData.customizeSpot && Number.isFinite(Number(mapData.customizeSpot.x)) && Number.isFinite(Number(mapData.customizeSpot.y))) {
@@ -527,6 +564,21 @@
mapData.interactive[ROOM_CZ_SPOT.y][ROOM_CZ_SPOT.x] = 1;
}
function markHostHcInteractiveCell() {
if (!mapData) return;
if (mapData.hostConsoleSpot && Number.isFinite(Number(mapData.hostConsoleSpot.x)) && Number.isFinite(Number(mapData.hostConsoleSpot.y))) {
ROOM_HC_SPOT = { x: Math.floor(Number(mapData.hostConsoleSpot.x)), y: Math.floor(Number(mapData.hostConsoleSpot.y)) };
} else {
ROOM_HC_SPOT = null;
return;
}
var w = mapData.width || 20, h = mapData.height || 15;
if (ROOM_HC_SPOT.x < 0 || ROOM_HC_SPOT.x >= w || ROOM_HC_SPOT.y < 0 || ROOM_HC_SPOT.y >= h) { ROOM_HC_SPOT = null; return; }
if (!Array.isArray(mapData.interactive)) mapData.interactive = [];
for (var y = 0; y < h; y++) { if (!Array.isArray(mapData.interactive[y])) mapData.interactive[y] = []; }
mapData.interactive[ROOM_HC_SPOT.y][ROOM_HC_SPOT.x] = 1;
}
function isRoomCzInteractTarget(t) {
return !!(t && ROOM_CZ_SPOT && t.x === ROOM_CZ_SPOT.x && t.y === ROOM_CZ_SPOT.y);
}
@@ -537,140 +589,49 @@
return (dx * dx + dy * dy) <= 2.25; // ภายในรัศมี ~1.5 ช่อง (ยืนใกล้ตู้ก็กด F ได้)
}
function roomCzInjectStyle() {
if (document.getElementById('room-cz-style')) return;
var st = document.createElement('style');
st.id = 'room-cz-style';
st.textContent = [
'.room-cz-open{position:fixed;left:clamp(10px,1.4vw,22px);bottom:clamp(96px,15vh,150px);z-index:60;width:clamp(54px,6vw,76px);padding:0;border:none;background:none;cursor:pointer}',
'.room-cz-open img{display:block;width:100%;height:auto;filter:drop-shadow(0 0 8px rgba(34,211,238,.5))}',
'.room-cz-overlay{position:fixed;inset:0;z-index:140;display:flex;align-items:center;justify-content:center;padding:16px;font-family:Kanit,Sarabun,system-ui,sans-serif}',
'.room-cz-overlay.hidden{display:none!important}',
'.room-cz-backdrop{position:absolute;inset:0;background:rgba(4,6,18,.78);backdrop-filter:blur(4px)}',
'.room-cz-dialog{position:relative;width:min(92vw,720px);max-height:90vh;overflow:hidden auto;display:flex;flex-direction:column;gap:clamp(.5rem,1.6vh,1rem);padding:clamp(1rem,3vw,1.6rem) clamp(1rem,3vw,1.8rem) clamp(1.2rem,3vw,1.8rem);border-radius:18px;background:linear-gradient(180deg,rgba(14,20,44,.97),rgba(9,13,30,.97));border:2px solid rgba(34,211,238,.7);box-shadow:0 0 22px rgba(34,211,238,.45),0 0 48px rgba(236,72,153,.28),inset 0 0 24px rgba(34,211,238,.1);color:#e8faff}',
'.room-cz-titlebar{display:flex;align-items:center;justify-content:center;position:relative}',
'.room-cz-titlebar h2{margin:0;font-size:clamp(1.1rem,3vw,1.6rem);font-weight:700;text-shadow:0 0 12px rgba(34,211,238,.6)}',
'.room-cz-close{position:absolute;right:0;top:0;width:38px;height:38px;border:2px solid rgba(236,72,153,.6);border-radius:10px;background:rgba(20,16,40,.6);color:#ff8fd0;font-size:1.4rem;line-height:1;cursor:pointer}',
'.room-cz-row{display:flex;align-items:center;gap:clamp(.5rem,2vw,1rem);flex-wrap:wrap}',
'.room-cz-rowlabel{height:clamp(20px,3.4vh,30px);width:auto;flex:0 0 auto}',
'.room-cz-swatches{display:flex;flex-wrap:wrap;gap:clamp(6px,1vw,10px)}',
'.room-cz-swatch{padding:0;border:2px solid transparent;border-radius:8px;background:none;cursor:pointer;line-height:0}',
'.room-cz-swatch img{display:block;width:clamp(28px,4vw,40px);height:auto;border-radius:6px}',
'.room-cz-swatch.sel{border-color:#22d3ee;box-shadow:0 0 10px rgba(34,211,238,.7)}',
'.room-cz-tabs{position:relative;width:100%;max-width:560px;margin:0 auto;aspect-ratio:776/118}',
'.room-cz-tabbar{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}',
'.room-cz-tabhit{position:absolute;top:0;height:100%;width:33.34%;padding:0;border:none;background:transparent;cursor:pointer}',
'.room-cz-tabhit[data-tab=face]{left:0}.room-cz-tabhit[data-tab=hair]{left:33.33%}.room-cz-tabhit[data-tab=cloth]{left:66.66%}',
'.room-cz-items{flex:1;min-height:clamp(140px,30vh,280px);max-height:42vh;overflow-y:auto;display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(8px,1.5vw,14px);padding:clamp(8px,1.5vw,14px);border-radius:12px;background:rgba(8,12,28,.6);border:1px solid rgba(34,211,238,.25)}',
'.room-cz-item{position:relative;padding:6px;border:2px solid rgba(34,211,238,.3);border-radius:12px;background:rgba(18,26,52,.7);cursor:pointer;aspect-ratio:1;display:flex;align-items:center;justify-content:center}',
'.room-cz-item.sel{border-color:#22d3ee;box-shadow:0 0 10px rgba(34,211,238,.6)}',
'.room-cz-item img{width:78%;height:auto;object-fit:contain}',
'.room-cz-item .pr{position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-size:.7rem;font-weight:700;color:#ffe066;background:rgba(0,0,0,.45);border-radius:8px;padding:1px 8px}',
'.room-cz-empty{grid-column:1/-1;align-self:center;text-align:center;color:rgba(255,255,255,.6);padding:2rem 1rem}',
'.room-cz-confirm{align-self:center;padding:0;border:none;background:none;cursor:pointer}',
'.room-cz-confirm img{display:block;width:min(60vw,240px);height:auto}',
'@media(max-width:560px){.room-cz-items{grid-template-columns:repeat(3,1fr)}}'
].join('');
document.head.appendChild(st);
function isHostHcInteractTarget(t) {
return !!(t && ROOM_HC_SPOT && t.x === ROOM_HC_SPOT.x && t.y === ROOM_HC_SPOT.y);
}
function roomCzBuildDom() {
if (document.getElementById('room-cz-overlay')) return;
var ov = document.createElement('div');
ov.id = 'room-cz-overlay'; ov.className = 'room-cz-overlay hidden';
ov.setAttribute('role', 'dialog'); ov.setAttribute('aria-modal', 'true'); ov.setAttribute('aria-label', 'ห้องแต่งตัว');
ov.innerHTML =
'<div class="room-cz-backdrop" id="room-cz-backdrop"></div>' +
'<div class="room-cz-dialog">' +
'<div class="room-cz-titlebar"><h2>ห้องแต่งตัว</h2><button type="button" class="room-cz-close" id="room-cz-close" aria-label="ปิด">&times;</button></div>' +
'<div class="room-cz-row"><img class="room-cz-rowlabel" src="' + ROOM_CZ_ASSET + 'theme-color-txt.png" alt="ธีมสี" decoding="async"><div class="room-cz-swatches" id="room-cz-colors"></div></div>' +
'<div class="room-cz-row"><img class="room-cz-rowlabel" src="' + ROOM_CZ_ASSET + 'theme-skin-txt.png" alt="สีผิว" decoding="async"><div class="room-cz-swatches" id="room-cz-skins"></div></div>' +
'<div class="room-cz-tabs"><img id="room-cz-tabbar" class="room-cz-tabbar" src="' + ROOM_CZ_ASSET + 'tab-1-face.png" alt="" decoding="async"><button type="button" class="room-cz-tabhit" data-tab="face" aria-label="ใบหน้า"></button><button type="button" class="room-cz-tabhit" data-tab="hair" aria-label="ทรงผม"></button><button type="button" class="room-cz-tabhit" data-tab="cloth" aria-label="ชุด"></button></div>' +
'<div class="room-cz-items" id="room-cz-items"></div>' +
'<button type="button" class="room-cz-confirm" id="room-cz-confirm" aria-label="ยืนยัน"><img src="' + ROOM_CZ_ASSET + 'btn-cf.png" alt="ยืนยัน" decoding="async"></button>' +
'</div>';
document.body.appendChild(ov);
function isNearHostHcSpot(me) {
if (!me || !ROOM_HC_SPOT || typeof me.x !== 'number' || typeof me.y !== 'number') return false;
var dx = me.x - ROOM_HC_SPOT.x, dy = me.y - ROOM_HC_SPOT.y;
return (dx * dx + dy * dy) <= 2.25;
}
function roomCzMarkSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'room-cz-colors' : 'room-cz-skins');
if (wrap) [].forEach.call(wrap.children, function (c) { c.classList.toggle('sel', c.getAttribute('data-idx') === String(idx)); });
}
function roomCzSelectSwatch(group, idx) {
roomCzMarkSwatch(group, idx);
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
rlResolveMyColors(function () {
persistLobbyPlayerTintJson();
updateRoomProfileAvatarTinted();
if (mapData && canvas) drawLobbyMap();
});
}
function roomCzMakeSwatch(group, idx) {
var b = document.createElement('button');
b.type = 'button'; b.className = 'room-cz-swatch'; b.setAttribute('data-idx', String(idx));
var im = document.createElement('img');
im.src = ROOM_CZ_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png'; im.alt = '';
b.appendChild(im);
b.addEventListener('click', function () { roomCzSelectSwatch(group, idx); });
return b;
}
function roomCzRenderItems(tab) {
var grid = document.getElementById('room-cz-items'); if (!grid) return;
grid.innerHTML = '';
if (tab !== 'face') { var e = document.createElement('div'); e.className = 'room-cz-empty'; e.textContent = 'ยังไม่เปิดให้บริการ'; grid.appendChild(e); return; }
var saved = ''; try { saved = localStorage.getItem('lobbyItem_face') || ''; } catch (e2) {}
ROOM_CZ_FACE.forEach(function (it) {
var cell = document.createElement('button'); cell.type = 'button';
cell.className = 'room-cz-item' + (String(it.idx) === saved ? ' sel' : ''); cell.setAttribute('data-idx', String(it.idx));
var im = document.createElement('img'); im.src = ROOM_CZ_ASSET + 'face-' + it.idx + '.png'; im.alt = ''; cell.appendChild(im);
if (it.price > 0) { var p = document.createElement('span'); p.className = 'pr'; p.textContent = String(it.price); cell.appendChild(p); }
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('sel'); });
cell.classList.add('sel');
try { localStorage.setItem('lobbyItem_face', String(it.idx)); } catch (e3) {}
});
grid.appendChild(cell);
});
}
function roomCzSetTab(tab) {
var bar = document.getElementById('room-cz-tabbar');
if (bar) { var m = { face: 'tab-1-face.png', hair: 'tab-2-hair.png', cloth: 'tab-3-cloth.png' }; bar.src = ROOM_CZ_ASSET + (m[tab] || m.face); }
roomCzRenderItems(tab);
function openHostConsoleIfHost() {
if (hostId !== socket.id) {
appendLobbySystemChat('— เฉพาะ Host เท่านั้น');
return;
}
if (hostConsoleOverlay && typeof hostConsoleOverlay.open === 'function') hostConsoleOverlay.open();
}
/** เปิดห้องแต่งตัว — production ใช้ #room-cz-overlay + CSS จาก customize-popup.css */
function openRoomCustomize() {
var ov = document.getElementById('room-cz-overlay'); if (!ov) return;
var cw = document.getElementById('room-cz-colors'), sw = document.getElementById('room-cz-skins');
if (cw && !cw.childElementCount) { for (var i = 1; i <= 8; i++) cw.appendChild(roomCzMakeSwatch('color', i)); }
if (sw && !sw.childElementCount) { for (var j = 1; j <= 3; j++) sw.appendChild(roomCzMakeSwatch('skin', j)); }
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) roomCzMarkSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) roomCzMarkSwatch('skin', s);
} catch (e) {}
roomCzSetTab('face');
ov.classList.remove('hidden');
if (typeof window.openCustomizePopup === 'function') {
window.openCustomizePopup();
}
}
function closeRoomCustomize() { var ov = document.getElementById('room-cz-overlay'); if (ov) ov.classList.add('hidden'); }
function closeRoomCustomize() {
if (typeof window.closeCustomizePopup === 'function') {
window.closeCustomizePopup();
}
}
function setupRoomCustomize() {
roomCzInjectStyle();
roomCzBuildDom();
var ob = document.getElementById('room-cz-open');
if (ob) ob.addEventListener('click', openRoomCustomize);
var cb = document.getElementById('room-cz-close');
if (cb) cb.addEventListener('click', closeRoomCustomize);
var bd = document.getElementById('room-cz-backdrop');
if (bd) bd.addEventListener('click', closeRoomCustomize);
var cf = document.getElementById('room-cz-confirm');
if (cf) cf.addEventListener('click', closeRoomCustomize);
[].forEach.call(document.querySelectorAll('.room-cz-tabhit'), function (t) {
t.addEventListener('click', function () { roomCzSetTab(t.getAttribute('data-tab')); });
window.openRoomCustomize = openRoomCustomize;
window.closeRoomCustomize = closeRoomCustomize;
window.addEventListener('customize-popup:applied', function () {
rlResolveMyColors(function () {
persistLobbyPlayerTintJson();
updateRoomProfileAvatarTinted();
if (mapData && canvas) drawLobbyMap();
renderPeers();
});
});
try { document.dispatchEvent(new CustomEvent('room-lobby-ready')); } catch (e) { /* ignore */ }
}
function rlTintMask(img, color) {
@@ -1121,6 +1082,20 @@
ctx.drawImage(roomCzIcon, _czx + tileSize / 2 - _czS / 2, _czy + tileSize - _czS, _czS, _czS);
}
if (ROOM_HC_SPOT && roomHcIcon && hostId === socket.id && roomHcIcon.complete && roomHcIcon.naturalWidth) {
const _hcx = ROOM_HC_SPOT.x * tileSize;
const _hcy = ROOM_HC_SPOT.y * tileSize;
const _hcS = tileSize * 1.5;
ctx.save();
ctx.globalAlpha = 0.5;
ctx.fillStyle = 'rgba(236,72,153,0.35)';
ctx.beginPath();
ctx.ellipse(_hcx + tileSize / 2, _hcy + tileSize - 3, tileSize * 0.55, tileSize * 0.22, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.drawImage(roomHcIcon, _hcx + tileSize / 2 - _hcS / 2, _hcy + tileSize - _hcS, _hcS, _hcS);
}
const peerList = [...peers.entries(), ...lobbyBots.entries()].sort(function (a, b) {
const pa = a[1], pb = b[1];
const ya = pa.y != null ? pa.y : 1, yb = pb.y != null ? pb.y : 1;
@@ -1625,9 +1600,26 @@
if (preplayCaseDetailOverlay) preplayCaseDetailOverlay.classList.add('is-hidden');
}
function syncPreplayCaseStepScale() {
if (!getPreplayEls() || !preplayStepCase || !preplayOverlay) return;
if (preplayOverlay.classList.contains('is-hidden')) return;
if (preplayStepCase.classList.contains('is-hidden')) return;
var rect = preplayStepCase.getBoundingClientRect();
if (!rect.width || !rect.height) return;
var baseWidth = 1700;
var baseHeight = 900;
var byWidth = rect.width / baseWidth;
var byHeight = rect.height / baseHeight;
var targetScale = 1.08;
var scale = Math.max(0.56, Math.min(targetScale, Math.min(byWidth, byHeight) * targetScale));
preplayStepCase.style.setProperty('--lobby-preplay-case-scale', scale.toFixed(4));
}
function showPreplayCaseStep() {
if (preplayStepLevel) preplayStepLevel.classList.add('is-hidden');
if (preplayStepCase) preplayStepCase.classList.remove('is-hidden');
syncPreplayCaseStepScale();
requestAnimationFrame(syncPreplayCaseStepScale);
}
function showPreplayLevelStep() {
@@ -1716,6 +1708,11 @@
var o = document.getElementById('lobby-preplay-case-detail-overlay');
if (o) o.classList.add('is-hidden');
});
window.addEventListener('resize', syncPreplayCaseStepScale);
window.addEventListener('orientationchange', function () {
setTimeout(syncPreplayCaseStepScale, 180);
});
}
const PATH_ARRIVE_THRESH = 0.15;
@@ -2104,6 +2101,7 @@
if (serverName) return serverName;
return profileDisplayName || nick || 'PLAYER';
}
window.getProfileDisplayName = getProfileDisplayName;
loadProfileDisplayName();
@@ -2172,6 +2170,9 @@
this.musicBtn = document.getElementById('room-lobby-profile-music');
this.sfxBtn = document.getElementById('room-lobby-profile-sfx');
this.archivGroupBtns = Array.from(document.querySelectorAll('.room-lobby-profile-archiv-group-btn'));
this.archivListEl = document.getElementById('room-lobby-profile-achievements-list');
this.archivScrollEl = document.querySelector('.room-lobby-profile-archiv-scroll');
this.archivScrollBarEl = document.querySelector('.room-lobby-profile-archiv-scroll-bar');
this.editBtn = document.getElementById('room-lobby-profile-edit-btn');
this.activeArchivGroup = 1;
this._boundSyncProfileFrameScale = () => this._syncProfileFrameScale();
@@ -2197,9 +2198,25 @@
});
});
this.editBtn?.addEventListener('click', () => this._editDisplayName());
this.archivListEl?.addEventListener('scroll', () => this._syncArchivScroll(), { passive: true });
window.addEventListener('resize', this._boundSyncProfileFrameScale);
}
_syncArchivScroll() {
if (!this.archivListEl || !this.archivScrollEl || !this.archivScrollBarEl) return;
const total = this.archivListEl.scrollHeight || 0;
const visible = this.archivListEl.clientHeight || 0;
const overflow = total > visible + 1;
this.archivScrollEl.classList.toggle('is-hidden', !overflow);
this.archivScrollEl.setAttribute('aria-hidden', overflow ? 'false' : 'true');
if (!overflow) return;
const max = Math.max(1, total - visible);
const ratio = Math.max(0, Math.min(1, this.archivListEl.scrollTop / max));
const barPct = Math.max(18, Math.min(42, (visible / Math.max(1, total)) * 100));
this.archivScrollBarEl.style.height = barPct.toFixed(2) + '%';
this.archivScrollBarEl.style.top = ((100 - barPct) * ratio).toFixed(2) + '%';
}
_initProfileFrameScaleObserver() {
if (!this.innerFrameEl) return;
if (typeof ResizeObserver === 'undefined') return;
@@ -2210,15 +2227,17 @@
_syncProfileFrameScale() {
let dialogScale = 1;
if (this.dialogEl) {
const dialogWidth = this.dialogEl.clientWidth || 0;
if (dialogWidth > 0) {
const rawDialogScale = dialogWidth / 1701;
dialogScale = Math.max(0.35, Math.min(1, rawDialogScale));
const rect = this.dialogEl.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const scaleW = rect.width / 1701;
const scaleH = rect.height / 951;
dialogScale = Math.max(0.35, Math.min(1, Math.min(scaleW, scaleH)));
this.dialogEl.style.setProperty('--rp-scale', String(dialogScale.toFixed(4)));
}
}
if (!this.innerFrameEl) return;
this.innerFrameEl.style.setProperty('--pf-scale', String(dialogScale.toFixed(4)));
this._syncArchivScroll();
}
_toggleChip(btn) {
@@ -2253,6 +2272,7 @@
? 'img/03-6-Profile/archiv-group-0' + id + '-a.png'
: 'img/03-6-Profile/archiv-group-0' + id + '.png';
});
this._syncArchivScroll();
}
setProfileData(data) {
@@ -2282,7 +2302,32 @@
}
_editDisplayName() {
const currentName = this.nameEl ? String(this.nameEl.textContent || '').trim() : getProfileDisplayName();
const self = this;
const currentName = getProfileDisplayName();
function afterRename(nextName) {
if (!nextName) return;
saveProfileDisplayName(nextName);
self.setProfileData({ displayName: nextName });
updateLobbyProfileAvatar();
renderPeers();
updatePlayersHud();
refreshHostConsoleOverlayIfOpen();
}
if (window.jdDisplayName && typeof window.jdDisplayName.openEditor === 'function') {
window.jdDisplayName.openEditor({ initialValue: currentName }).then(function (draft) {
if (draft == null) return;
if (typeof window.jdApplyDisplayNameChange === 'function') {
const result = window.jdApplyDisplayNameChange(draft, { socket });
if (result.ok) afterRename(result.displayName);
return;
}
afterRename(draft);
});
return;
}
const draft = window.prompt('แก้ไขชื่อผู้เล่น', currentName || '');
if (draft == null) return;
const nextName = String(draft).trim();
@@ -2290,9 +2335,13 @@
alert('กรุณากรอกชื่อผู้เล่น');
return;
}
saveProfileDisplayName(nextName);
this.setProfileData({ displayName: nextName });
updateLobbyProfileAvatar();
if (typeof window.jdApplyDisplayNameChange === 'function') {
const result = window.jdApplyDisplayNameChange(nextName, { socket });
if (!result.ok) return;
afterRename(result.displayName);
return;
}
afterRename(nextName);
}
open(data) {
@@ -2304,6 +2353,7 @@
this._syncProfileFrameScale();
requestAnimationFrame(() => this._syncProfileFrameScale());
requestAnimationFrame(() => requestAnimationFrame(() => this._syncProfileFrameScale()));
this._syncArchivScroll();
this.closeBtn?.focus();
}
@@ -2382,11 +2432,10 @@
_changeMax(delta) {
if (!this._isHost()) return;
const currentPlayers = this._getPlayersCount();
const minAllowed = Math.max(2, currentPlayers);
const maxAllowed = 10;
const humans = this._getPlayersCount();
const minAllowed = Math.max(humans, 2);
const next = this.pendingMaxPlayers + delta;
this.pendingMaxPlayers = Math.max(minAllowed, Math.min(maxAllowed, next));
this.pendingMaxPlayers = Math.max(minAllowed, Math.min(LOBBY_SLOT_TOTAL, next));
this._render();
}
@@ -2460,14 +2509,42 @@
_confirm() {
if (!this._isHost()) return;
maxPlayers = this.pendingMaxPlayers;
const humans = this._getPlayersCount();
const total = Math.min(LOBBY_SLOT_TOTAL, Math.max(humans, this.pendingMaxPlayers));
const bots = total - humans;
lobbyBotSlotCount = bots;
maxPlayers = Math.max(humans, total - bots);
this.pendingMaxPlayers = total;
syncLobbyCaseBots();
updatePlayersHud();
this.close();
if (mapData && canvas) drawLobbyMap();
const self = this;
socket.emit('host-console-apply', {
totalSlots: total,
botSlotCount: bots,
maxPlayers: maxPlayers
}, function (res) {
if (!res || !res.ok) {
var msg = (res && res.error) ? res.error : 'บันทึกการตั้งค่าไม่สำเร็จ';
try { alert(msg); } catch (e) { /* ignore */ }
return;
}
if (res.maxPlayers != null) maxPlayers = res.maxPlayers;
if (res.botSlotCount != null) {
lobbyBotSlotCount = Math.max(0, parseInt(res.botSlotCount, 10) || 0);
syncLobbyCaseBots();
updatePlayersHud();
if (mapData && canvas) drawLobbyMap();
}
self.close();
});
}
open() {
if (!this.overlay) return;
this.pendingMaxPlayers = Math.max(2, maxPlayers || 10);
if (!this._isHost()) return;
const totalNow = Math.min(LOBBY_SLOT_TOTAL, Math.max(peers.size, lobbyOccupantCount()));
this.pendingMaxPlayers = totalNow > 0 ? totalNow : Math.min(LOBBY_SLOT_TOTAL, Math.max(2, (maxPlayers || 2) + (lobbyBotSlotCount || 0)));
this._render();
this.overlay.classList.remove('is-hidden');
this.overlay.setAttribute('aria-hidden', 'false');
@@ -2491,8 +2568,26 @@
hostConsoleOverlay._render();
}
function applyPeerDisplayNameUpdate(data) {
if (!data || !data.id) return;
const p = peers.get(data.id);
if (p) p.nickname = data.nickname;
if (data.id === socket.id) {
saveProfileDisplayName(data.nickname);
try { localStorage.setItem('playerName', data.nickname); } catch (e) { /* ignore */ }
if (window.jdDisplayName && typeof window.jdDisplayName.syncDom === 'function') {
window.jdDisplayName.syncDom(data.nickname);
}
}
renderPeers();
updatePlayersHud();
refreshHostConsoleOverlayIfOpen();
}
socket.on('peer-display-name', applyPeerDisplayNameUpdate);
socket.on('connect', () => {
socket.emit('join-space', { spaceId, nickname: nick, characterId: getStoredCharacterId() }, (res) => {
socket.emit('join-space', { spaceId, nickname: getProfileDisplayName(), characterId: getStoredCharacterId() }, (res) => {
if (!res || !res.ok) {
var joinErr = (res && res.error) || 'เข้าไม่ได้';
if (/เริ่มคดี|ไม่รับผู้เล่น/.test(joinErr)) {
@@ -2500,14 +2595,16 @@
} else {
alert(joinErr);
}
location.href = BASE + '/lobby.html';
location.href = DESIGN_MENU;
return;
}
mapData = res.mapData;
roomJoinReady = true;
maybeHideRoomLoading();
markRoomCzInteractiveCell();
markHostHcInteractiveCell();
if (ROOM_CZ_SPOT) appendLobbySystemChat('— เดินไปช่องเขียว แล้วกด F เพื่อเปิดห้องแต่งตัว');
if (ROOM_HC_SPOT && hostId === socket.id) appendLobbySystemChat('— เดินไปจุดโฮสต์ (ไอคอนชมพู) แล้วกด F เพื่อเปิด Host Console');
clientLobbyMapId = res.mapId != null ? res.mapId : null;
hostId = res.hostId || null;
spaceName = (res.spaceName || '').trim() || spaceId;
@@ -2625,6 +2722,18 @@
window.location.href = CREATE_ROOM_URL;
});
socket.on('host-console-settings', (data) => {
if (!data || typeof data !== 'object') return;
if (data.maxPlayers != null) maxPlayers = Math.max(peers.size, parseInt(data.maxPlayers, 10) || peers.size);
if (data.botSlotCount != null) {
lobbyBotSlotCount = Math.max(0, parseInt(data.botSlotCount, 10) || 0);
syncLobbyCaseBots();
}
updatePlayersHud();
refreshHostConsoleOverlayIfOpen();
if (mapData && canvas) drawLobbyMap();
});
socket.on('peer-ready', (data) => {
const p = peers.get(data.id);
if (p) { p.ready = data.ready; renderPeers(); redrawLobbyMap(); }
@@ -3745,14 +3854,10 @@
roomLobbyProfileOverlay.open();
});
document.getElementById('btn-setting-host')?.addEventListener('click', () => {
if (hostId !== socket.id) {
alert('เฉพาะ Host เท่านั้น');
return;
}
hostConsoleOverlay.open();
openHostConsoleIfHost();
});
document.getElementById('btn-customize')?.addEventListener('click', () => {
roomLobbyProfileOverlay.open();
openRoomCustomize();
});
document.getElementById('room-lobby-profile-logout')?.addEventListener('click', () => {
window.location.href = CREATE_ROOM_URL;
@@ -4741,8 +4846,10 @@
if (!mapData.quizTrueArea) mapData.quizTrueArea = [];
if (!mapData.quizFalseArea) mapData.quizFalseArea = [];
if (!mapData.quizQuestionArea) mapData.quizQuestionArea = [];
ROOM_CZ_SPOT = null; // รีเซ็ตจุดแต่งตัวก่อน แล้วคำนวณใหม่ตาม map ใหม่ (LobbyB ไม่มี customizeSpot → ไม่แสดง icon)
ROOM_CZ_SPOT = null;
ROOM_HC_SPOT = null;
markRoomCzInteractiveCell();
markHostHcInteractiveCell();
(data.peersSnap || []).forEach(function (row) {
if (!row || !row.id) return;
const p = peers.get(row.id);
@@ -4927,7 +5034,7 @@
sessionStorage.setItem('justicePlayLobbyAvatar:' + cidPlay, avEl.src);
}
} catch (eAv) { /* ignore */ }
let q = 'play.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
let q = 'play.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(getProfileDisplayName());
if (mid) q += '&map=' + encodeURIComponent(mid);
if (lobbyLevelStr) q += '&lobbyLevel=' + encodeURIComponent(lobbyLevelStr);
if (caseIdStr) q += '&case=' + encodeURIComponent(caseIdStr);
@@ -4998,6 +5105,11 @@
openRoomCustomize();
return;
}
if (isHostHcInteractTarget(target) || isNearHostHcSpot(me)) {
e.preventDefault();
openHostConsoleIfHost();
return;
}
/* F อื่นในโถง (Host เลือกระดับ/คดี และช่องเขียว) — ต้องกดพร้อมก่อน */
if (!me.ready) {
appendLobbySystemChat('— กดพร้อมก่อน แล้วค่อยกด F');
+9 -50
View File
@@ -4,57 +4,16 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ล็อบบี้ — Game</title>
<link rel="stylesheet" href="css/style.css">
<meta http-equiv="refresh" content="0; url=/Main-Menu/">
<script src="/app-base.js?v=2"></script>
<script>
(function () {
var dest = typeof designMainMenuHref === 'function' ? designMainMenuHref() : '/Main-Menu/';
window.location.replace(dest);
})();
</script>
</head>
<body>
<div class="container">
<h1>ล็อบบี้</h1>
<p><a href="index.html">กลับ</a> · <a href="character.html">เลือกตัวละคร</a></p>
<p><label>ชื่อคุณ <input type="text" id="nick" placeholder="ชื่อผู้เล่น" value="ผู้เล่น"></label></p>
<section class="lobby-section">
<h2>ห้องสาธารณะ (Public) — เลือกเข้าร่วมได้เลย</h2>
<div id="room-list-wrap">
<p id="room-list-status">โหลดรายการห้อง...</p>
<ul id="room-list"></ul>
</div>
</section>
<section class="lobby-section">
<h2>สร้างห้องใหม่</h2>
<p>เลือกประเภทห้อง แล้วกดสร้าง</p>
<p>
<label><input type="radio" name="room-type" value="public" checked> สาธารณะ (Public)</label> — โชว์ในรายการด้านบน คนอื่น join ได้เลย
<br>
<label><input type="radio" name="room-type" value="private"> ส่วนตัว (Private)</label> — สร้างรหัสอัตโนมัติ แชร์รหัสเพื่อให้คนอื่นเข้า
</p>
<p>
<label>จำนวนผู้เล่นสูงสุด <select id="max-players">
<option value="2">2 คน</option>
<option value="3">3 คน</option>
<option value="4" selected>4 คน</option>
<option value="5">5 คน</option>
<option value="6">6 คน</option>
<option value="7">7 คน</option>
<option value="8">8 คน</option>
<option value="9">9 คน</option>
<option value="10">10 คน</option>
</select></label>
</p>
<button type="button" id="btn-create-lobby">สร้างห้อง</button>
</section>
<section class="lobby-section">
<h2>เข้าร่วมห้องส่วนตัว (Private)</h2>
<p>กรอกรหัสห้องที่ได้จาก host เพื่อเข้า</p>
<form id="form-join">
<label>รหัสห้อง <input type="text" id="space-id" placeholder="กรอกรหัส 6 หลัก" required></label>
<button type="submit">เข้าร่วม</button>
</form>
</section>
</div>
<script src="js/version.js?v=0.0104"></script>
<script src="js/lobby.js?v=0.0105"></script>
<div class="version-tag">v —</div>
<p>กำลังไปเมนูหลัก… <a href="/Main-Menu/">คลิกที่นี่</a> ถ้าไม่ redirect อัตโนมัติ</p>
</body>
</html>
+2 -1
View File
@@ -3211,9 +3211,10 @@
</div>
</div>
</div>
<script src="/app-base.js?v=2"></script>
<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.0457"></script>
<script src="js/play.js?v=0.0459"></script>
<div class="version-tag">v —</div>
</body>
</html>
+36 -23
View File
@@ -7,7 +7,9 @@
<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="css/room-lobby.css?v=0.0168">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0172">
<link rel="stylesheet" href="css/profile-popup.css?v=6">
<link rel="stylesheet" href="css/customize-popup.css?v=40">
<style>
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
#mic-permission-overlay {
@@ -630,14 +632,23 @@
}
.lobby-preplay-back-wrap img { display: block; width: min(367px, 20vw); height: auto; }
#lobby-preplay-step-case {
--lobby-preplay-case-scale: 1;
--lobby-preplay-case-gap: calc(20px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-head-width: calc(1120px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-row-width: calc(1400px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-wrap-width: calc(1260px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-row-gap: calc(24px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-wrap-gap: calc(16px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-arrow-size: calc(130px * var(--lobby-preplay-case-scale));
--lobby-preplay-case-card-size: calc(439px * var(--lobby-preplay-case-scale));
justify-content: center;
align-items: center;
gap: clamp(10px, 1.6vh, 20px);
gap: max(calc(10px * var(--lobby-preplay-case-scale)), min(1.6vh, var(--lobby-preplay-case-gap)));
}
.lobby-preplay-case-head {
flex-shrink: 0;
text-align: center;
width: min(96vw, 1120px);
width: min(96vw, var(--lobby-preplay-case-head-width));
padding-top: 0;
padding-left: 0;
padding-right: 0;
@@ -649,8 +660,8 @@
flex-wrap: nowrap;
justify-content: center;
align-items: flex-start;
gap: clamp(10px, 1.5vw, 24px);
width: min(96vw, 1400px);
gap: max(calc(10px * var(--lobby-preplay-case-scale, 1)), min(1.5vw, var(--lobby-preplay-case-row-gap, 24px)));
width: min(96vw, var(--lobby-preplay-case-row-width));
min-height: auto;
padding: 0;
overflow-x: auto;
@@ -659,17 +670,17 @@
}
.lobby-preplay-case-row-wrap {
flex: 0 0 auto;
width: min(98vw, 1260px);
width: min(98vw, var(--lobby-preplay-case-wrap-width));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: clamp(6px, 1vw, 16px);
gap: max(calc(6px * var(--lobby-preplay-case-scale, 1)), min(1vw, var(--lobby-preplay-case-wrap-gap, 16px)));
}
.lobby-preplay-case-arrow {
flex: 0 0 auto;
width: clamp(44px, 8vw, 130px);
height: clamp(44px, 8vw, 130px);
width: max(calc(44px * var(--lobby-preplay-case-scale, 1)), min(8vw, var(--lobby-preplay-case-arrow-size, 130px)));
height: max(calc(44px * var(--lobby-preplay-case-scale, 1)), min(8vw, var(--lobby-preplay-case-arrow-size, 130px)));
padding: 0;
margin: 0;
border: none;
@@ -709,15 +720,15 @@
}
}
.lobby-case-card-wrap {
flex: 0 0 clamp(220px, 26vw, 439px);
width: clamp(220px, 26vw, 439px);
min-width: clamp(220px, 26vw, 439px);
max-width: clamp(220px, 26vw, 439px);
flex: 0 0 max(calc(220px * var(--lobby-preplay-case-scale, 1)), min(26vw, var(--lobby-preplay-case-card-size, 439px)));
width: max(calc(220px * var(--lobby-preplay-case-scale, 1)), min(26vw, var(--lobby-preplay-case-card-size, 439px)));
min-width: max(calc(220px * var(--lobby-preplay-case-scale, 1)), min(26vw, var(--lobby-preplay-case-card-size, 439px)));
max-width: max(calc(220px * var(--lobby-preplay-case-scale, 1)), min(26vw, var(--lobby-preplay-case-card-size, 439px)));
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(6px, 1vh, 12px);
gap: max(calc(6px * var(--lobby-preplay-case-scale, 1)), min(1vh, calc(12px * var(--lobby-preplay-case-scale, 1))));
border: none;
border-radius: 0;
padding: 0;
@@ -1241,7 +1252,7 @@
<span>เลือกผู้ต้องสงสัย</span>
</button>
<div class="room-lobby-top-right-cluster" role="group" aria-label="โปรไฟล์ตัวละคร">
<div class="room-lobby-top-right-actions" role="group" aria-label="ปุ่มตั้งค่าโฮสต์และปรับแต่ง">
<div class="room-lobby-top-right-actions is-hidden" hidden role="group" aria-label="ปุ่มตั้งค่าโฮสต์และปรับแต่ง" aria-hidden="true">
<button type="button" id="btn-setting-host" class="room-lobby-top-right-action-btn" title="ตั้งค่าโฮสต์" aria-label="ตั้งค่าโฮสต์">
<img src="img/btn-setting-host.png" alt="" width="161" height="176" decoding="async">
</button>
@@ -1351,9 +1362,9 @@
</div>
</li>
</ul>
<div class="room-lobby-profile-archiv-scroll">
<img class="room-lobby-profile-archiv-scroll-bg" src="img/03-6-Profile/archiv-scroll-bg.png" alt="" decoding="async">
<img class="room-lobby-profile-archiv-scroll-bar" src="img/03-6-Profile/archiv-scroll-bar.png" alt="" decoding="async">
<div class="room-lobby-profile-archiv-scroll is-hidden" aria-hidden="true">
<div class="room-lobby-profile-archiv-scroll-bg"></div>
<div class="room-lobby-profile-archiv-scroll-bar"></div>
</div>
</div>
<div class="room-lobby-profile-settings">
@@ -1472,12 +1483,12 @@
</div>
<div class="room-lobby-br-fixed" id="room-lobby-br-fixed">
<div id="lobby-b-extra-row" class="lobby-b-extra-row is-hidden" aria-hidden="true">
<button type="button" class="lobby-b-sq-btn" id="lobby-b-btn-evidence" title="สืบสวน / หลักฐาน" aria-label="สืบสวน หลักฐาน">
<img src="img/lobby-b/icon-evidence.png?v=1" alt="" width="56" height="56" decoding="async">
</button>
<button type="button" class="lobby-b-sq-btn" id="lobby-b-btn-rank" title="อันดับ / รางวัล" aria-label="อันดับ รางวัล">
<img src="img/lobby-b/icon-rank.png?v=1" alt="" width="56" height="56" decoding="async">
</button>
<button type="button" class="lobby-b-sq-btn" id="lobby-b-btn-evidence" title="สืบสวน / หลักฐาน" aria-label="สืบสวน หลักฐาน">
<img src="img/lobby-b/icon-evidence.png?v=1" alt="" width="56" height="56" decoding="async">
</button>
</div>
<button type="button" id="btn-voice" class="btn-voice-icon" title="เปิด/ปิดเสียงพูด" aria-label="เปิด/ปิดเสียงพูด">
<img src="img/btn-mic-mute.png" alt="เปิดเสียง" id="btn-voice-icon-img">
@@ -1530,10 +1541,12 @@
</div>
</div>
</div>
<script src="/app-base.js?v=1"></script>
<script src="../app-base.js"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/display-name.js?v=2"></script>
<script src="js/version.js?v=0.0122"></script>
<script src="js/room-lobby.js?v=0.0205"></script>
<script src="js/customize-popup.js?v=29" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/room-lobby.js?v=0.0218"></script>
<div class="version-tag">v —</div>
</body>
</html>
+110 -2
View File
@@ -4,7 +4,9 @@ const fs = require('fs');
const crypto = require('crypto');
const { Server } = require('socket.io');
const PORT = process.env.PORT || 3001;
const PORT = parseInt(process.env.PORT, 10) || 3001;
/** Windows dev: bind 127.0.0.1 (0.0.0.0 มัก EACCES ถ้าพอร์ตอยู่ใน excluded range ของ Hyper-V) */
const HOST = process.env.HOST || process.env.GAME_HOST || '0.0.0.0';
const BASE_PATH = '/Game';
const AI_SETTINGS_PATH = path.join(__dirname, 'data', 'ai-settings.json');
const ADMIN_PASSWORD = process.env.GAME_AI_ADMIN_PASSWORD || 'game-ai-admin';
@@ -2440,7 +2442,23 @@ function isMapNameTaken(name, excludeId) {
return false;
}
function applyDevCors(req, res) {
const origin = req.headers.origin;
if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Vary', 'Origin');
}
}
const server = http.createServer((req, res) => {
applyDevCors(req, res);
if (req.method === 'OPTIONS' && req.url && req.url.indexOf(BASE_PATH + '/api') === 0) {
res.writeHead(204);
res.end();
return;
}
let url = req.url;
if (url === BASE_PATH || url === BASE_PATH + '/') url = BASE_PATH + '/index.html';
if (url.startsWith(BASE_PATH + '/api/maps')) {
@@ -4409,6 +4427,33 @@ io.on('connection', (socket) => {
}
});
/** เปลี่ยนชื่อที่แสดงใน lobby / เกม (ซิงก์ทุก client ในห้อง) */
socket.on('update-display-name', ({ nickname }, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
const p = space.peers.get(socket.id);
if (!p) return reply({ ok: false, error: 'ไม่พบผู้เล่น' });
let safe = String(nickname || '').trim();
if (!safe) return reply({ ok: false, error: 'ชื่อว่าง' });
try {
safe = safe.normalize('NFKC');
} catch (e) { /* ignore */ }
if (safe.length > 32) safe = safe.slice(0, 32);
const oldNorm = normalizeLobbyNickname(p.nickname);
const newNorm = normalizeLobbyNickname(safe);
p.nickname = safe;
if (space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0) {
if (space.caseParticipantNicknames.has(oldNorm)) {
space.caseParticipantNicknames.delete(oldNorm);
space.caseParticipantNicknames.add(newNorm);
}
}
io.to(sid).emit('peer-display-name', { id: socket.id, nickname: safe });
reply({ ok: true, nickname: safe });
});
/** ห้องทดสอบจากเอดิเตอร์ (ชื่อ preview) — สลับโฮสต์ให้เครื่องนี้กดเริ่มเกมได้ */
socket.on('preview-claim-host', (cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
@@ -4605,6 +4650,55 @@ io.on('connection', (socket) => {
reply({ ok: true });
});
/** Host Console — ตั้งจำนวนรวม (คน+บอท) ไม่เกิน LOBBY_SLOT_TOTAL */
socket.on('host-console-apply', (data, cb) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id) {
return cb && cb({ ok: false, error: 'เฉพาะ Host เท่านั้น' });
}
const humans = space.peers.size;
let total = parseInt(data && data.totalSlots, 10);
if (!Number.isFinite(total)) total = humans + effectiveBotSlotCount(space);
total = Math.min(LOBBY_SLOT_TOTAL, Math.max(humans, total));
let bots = parseInt(data && data.botSlotCount, 10);
if (!Number.isFinite(bots)) bots = total - humans;
bots = Math.max(0, Math.min(LOBBY_SLOT_TOTAL - humans, bots));
total = humans + bots;
space.botSlotCount = bots;
space.maxPlayers = Math.max(humans, total - bots);
io.to(sid).emit('host-console-settings', {
maxPlayers: space.maxPlayers,
botSlotCount: space.botSlotCount,
totalSlots: total
});
cb && cb({ ok: true, maxPlayers: space.maxPlayers, botSlotCount: space.botSlotCount, totalSlots: total });
});
socket.on('host-console-kick-peer', ({ targetId }, cb) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id) {
return cb && cb({ ok: false, error: 'เฉพาะ Host เท่านั้น' });
}
const tid = targetId != null ? String(targetId) : '';
if (!tid || tid === socket.id || tid === space.hostId) {
return cb && cb({ ok: false, error: 'ไม่สามารถลบผู้เล่นนี้ได้' });
}
if (!space.peers.has(tid)) {
return cb && cb({ ok: false, error: 'ไม่พบผู้เล่นในห้อง' });
}
const targetSock = io.sockets.sockets.get(tid);
if (targetSock) {
targetSock.emit('host-console-kicked', { message: 'คุณถูก Host นำออกจากห้อง' });
targetSock.leave(sid);
delete targetSock.data.spaceId;
}
space.peers.delete(tid);
io.to(sid).emit('user-left', { id: tid });
cb && cb({ ok: true });
});
/* พร้อมแล้ว — ไม่บังคับจำนวนคนในห้อง */
socket.on('set-ready', ({ ready }) => {
for (const [sid, space] of spaces) {
@@ -5183,4 +5277,18 @@ io.on('connection', (socket) => {
});
loadMaps();
server.listen(PORT, () => console.log('Game', PORT));
server.on('error', (err) => {
if (err && (err.code === 'EACCES' || err.code === 'EADDRINUSE')) {
console.error('');
console.error('[Game] Cannot listen on', HOST + ':' + PORT, '(' + err.code + ').');
console.error(' Windows AppServ:');
console.error(' set HOST=127.0.0.1');
console.error(' set PORT=13010');
console.error(' node server.js');
console.error(' Or double-click start-dev.cmd');
console.error('');
process.exit(1);
}
throw err;
});
server.listen(PORT, HOST, () => console.log('Game server listening on http://' + HOST + ':' + PORT + BASE_PATH));
+17
View File
@@ -0,0 +1,17 @@
@echo off
REM Windows AppServ — Hyper-V มักจองพอร์ตต่ำ (3001, 3010 = EACCES)
REM ใช้ 127.0.0.1:13010 แทน
cd /d "%~dp0"
set HOST=127.0.0.1
set PORT=13010
echo.
echo Game dev server: http://localhost:%PORT%/Game
echo (Create Room / Join จะเรียก API ที่พอร์ตนี้อัตโนมัติ)
echo.
node server.js
if errorlevel 1 (
echo.
echo ถ้ายัง error ลองรัน CMD แบบ Run as administrator
echo หรือเปลี่ยนพอร์ต: set PORT=18080
pause
)
+19 -15
View File
@@ -4,42 +4,44 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>เข้าร่วมเกม — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css?v=0.004">
</head>
<body>
<div class="join-bg" role="img" aria-label="พื้นหลัง">
<img src="/Main-Menu/bg.png" alt="" class="join-bg-img" role="presentation">
<img src="../Main-Menu/bg.png" alt="" class="join-bg-img" role="presentation">
</div>
<div class="join-char-layer" aria-hidden="true" role="img">
<img src="/Main-Menu/char-main.png" alt="" class="join-char-img" role="presentation">
<img src="../Main-Menu/char-main.png" alt="" class="join-char-img" role="presentation">
</div>
<div class="join-vignette" aria-hidden="true"></div>
<div class="join-ui">
<div class="join-back-section">
<button type="button" class="join-btn-back" id="btn-back" aria-label="ย้อนกลับ">
<img src="/Main-Menu/IMAGE/btn-back.png" alt="" class="join-btn-back-img" role="presentation">
<img src="../Main-Menu/IMAGE/btn-back.png" alt="" class="join-btn-back-img" role="presentation">
</button>
</div>
<div class="join-content">
<header class="join-header">
<img src="IMAGE/txt-joinroom.png" alt="เข้าร่วมเกม" class="join-title-img" role="presentation">
<img src="IMAGE/line.png" alt="" class="join-title-line" aria-hidden="true" role="presentation">
</header>
<div class="join-panel">
<div class="join-tabs" role="tablist" aria-label="เลือกประเภทห้อง">
<button type="button" class="join-tab" id="tab-public" role="tab" aria-selected="true" aria-controls="join-public-view">
<img src="IMAGE/btn-publish-room-h.png" alt="ห้องสาธารณะ" class="join-tab-img" id="tab-img-public" role="presentation">
</button>
<button type="button" class="join-tab" id="tab-private" role="tab" aria-selected="false" aria-controls="join-private-view">
<img src="IMAGE/btn-private-room.png" alt="ห้องส่วนตัว" class="join-tab-img" id="tab-img-private" role="presentation">
</button>
</div>
<div class="join-tabs" role="tablist" aria-label="เลือกประเภทห้อง">
<button type="button" class="join-tab" id="tab-public" role="tab" aria-selected="true" aria-controls="join-public-view">
<img src="IMAGE/btn-publish-room-h.png" alt="ห้องสาธารณะ" class="join-tab-img" id="tab-img-public" role="presentation">
</button>
<button type="button" class="join-tab" id="tab-private" role="tab" aria-selected="false" aria-controls="join-private-view">
<img src="IMAGE/btn-private-room.png" alt="ห้องส่วนตัว" class="join-tab-img" id="tab-img-private" role="presentation">
</button>
</div>
<div class="join-panel">
<div id="join-public-view" class="join-view" role="tabpanel">
<div class="join-list-scroll">
<div class="join-list" id="room-list-public" aria-label="รายการห้องสาธารณะ"></div>
<div class="join-scrollbar-thumb" id="join-scrollbar-thumb" aria-hidden="true"></div>
</div>
</div>
@@ -58,7 +60,7 @@
</div>
</div>
<button type="button" class="join-private-submit" id="btn-private-join" aria-label="เข้าร่วมห้องส่วนตัว">
<img src="IMAGE/btn-join-room.png" alt="เข้าร่วม" class="join-private-submit-img" role="presentation">
<img src="IMAGE/btn-join-room-2.png" alt="เข้าร่วม" class="join-private-submit-img" role="presentation">
</button>
</div>
</div>
@@ -78,6 +80,8 @@
</div>
</template>
<script src="join.js?v=0.003"></script>
<script src="../app-base.js?v=2"></script>
<script src="../Game/js/display-name.js?v=2"></script>
<script src="join.js?v=0.004"></script>
</body>
</html>
+100 -39
View File
@@ -4,7 +4,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;
@@ -14,7 +14,8 @@
return;
}
const BASE = '/Game';
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + (typeof appPath === 'function' ? appPath('/Game') : '/Game');
const IMG = {
publishOn: 'IMAGE/btn-publish-room-h.png',
publishOff: 'IMAGE/btn-publish-room.png',
@@ -23,9 +24,26 @@
joinOn: 'IMAGE/btn-join-room.png',
joinOff: 'IMAGE/btn-join-room-2.png',
};
const USE_PUBLIC_ROOM_MOCK = false;
const MOCK_PUBLIC_ROOMS = [
{ spaceId: 'JD-001', spaceName: '[JUSTICE DIVERS #01]', mapName: 'Neo City', peerCount: 2, maxPlayers: 4 },
{ spaceId: 'JD-002', spaceName: '[JUSTICE DIVERS #02]', mapName: 'Cyber Lab', peerCount: 5, maxPlayers: 6 },
{ spaceId: 'JD-003', spaceName: '[JUSTICE DIVERS #03]', mapName: 'Sky Deck', peerCount: 5, maxPlayers: 5 },
{ spaceId: 'JD-004', spaceName: '[JUSTICE DIVERS #04]', mapName: 'Dockyard', peerCount: 2, maxPlayers: 4 },
{ spaceId: 'JD-005', spaceName: '[JUSTICE DIVERS #05]', mapName: 'Temple', peerCount: 3, maxPlayers: 4 },
{ spaceId: 'JD-006', spaceName: '[JUSTICE DIVERS #06]', mapName: 'Arena', peerCount: 6, maxPlayers: 6 },
{ spaceId: 'JD-007', spaceName: '[JUSTICE DIVERS #07]', mapName: 'Outpost', peerCount: 1, maxPlayers: 4 }
];
function getNick() {
return (localStorage.getItem('playerName') || '').trim() || 'ผู้เล่น';
if (typeof window.jdReadDisplayName === 'function') {
return window.jdReadDisplayName();
}
try {
return (localStorage.getItem('roomLobbyDisplayName') || localStorage.getItem('playerName') || '').trim() || 'ผู้เล่น';
} catch (e) {
return 'ผู้เล่น';
}
}
function escapeHtml(s) {
@@ -42,13 +60,14 @@
var viewPublic = document.getElementById('join-public-view');
var viewPrivate = document.getElementById('join-private-view');
var listPublic = document.getElementById('room-list-public');
var publicScrollThumb = document.getElementById('join-scrollbar-thumb');
var tplRow = document.getElementById('tpl-room-row');
var passPrivate = document.getElementById('join-private-password');
var idPrivate = document.getElementById('join-private-room-id');
var btnPrivateJoin = document.getElementById('btn-private-join');
btnBack?.addEventListener('click', function () {
window.location.href = '/Main-Menu/';
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
passPrivate?.addEventListener('input', function (e) {
@@ -59,49 +78,85 @@
window.location.href = BASE + '/room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(getNick());
}
function renderPublicRooms(list) {
if (!listPublic || !tplRow) return;
listPublic.innerHTML = '';
if (!Array.isArray(list) || list.length === 0) {
listPublic.innerHTML = '<div class="join-list-status">ยังไม่มีห้อง</div>';
return;
}
list.forEach(function (room) {
var code = room.spaceId || ('room-' + Math.random().toString(36).slice(2, 8));
var name = room.spaceName || code;
var mapName = room.mapName || '—';
var count = room.peerCount != null ? room.peerCount : 0;
var max = room.maxPlayers != null ? room.maxPlayers : 10;
var full = count >= max;
var node = tplRow.content.cloneNode(true);
var nameEl = node.querySelector('.join-room-name');
var playersEl = node.querySelector('.join-room-players');
var btn = node.querySelector('.join-room-btn');
var img = node.querySelector('.join-room-btn-img');
nameEl.textContent = 'ห้อง : ' + name + ' [' + mapName + ']';
playersEl.textContent = 'ผู้เล่น : ' + count + '/' + max;
img.src = IMG.joinOn;
img.alt = full ? 'เต็ม' : 'เข้าร่วม';
if (full) {
btn.disabled = true;
} else {
btn.addEventListener('click', function () {
goToRoom(room.spaceId);
});
}
listPublic.appendChild(node);
});
syncPublicScrollbarThumb();
}
function syncPublicScrollbarThumb() {
if (!listPublic || !publicScrollThumb) return;
var total = listPublic.scrollHeight;
var visible = listPublic.clientHeight;
var track = listPublic.parentElement ? listPublic.parentElement.clientHeight : visible;
var styles = window.getComputedStyle(publicScrollThumb);
var topInset = parseFloat(styles.top) || 0;
var bottomInset = topInset;
var minThumb = parseFloat(styles.minHeight) || 40;
var innerTrack = Math.max(0, track - topInset - bottomInset);
if (!total || total <= visible || track <= 0) {
publicScrollThumb.style.opacity = '0';
return;
}
var thumbH = Math.max(minThumb, (visible / total) * innerTrack);
var maxY = Math.max(0, innerTrack - thumbH);
var ratio = listPublic.scrollTop / Math.max(1, total - visible);
var y = topInset + (maxY * ratio);
var clampedY = Math.min(topInset + maxY, Math.max(topInset, y));
publicScrollThumb.style.height = thumbH + 'px';
publicScrollThumb.style.transform = 'translateY(' + clampedY + 'px)';
publicScrollThumb.style.opacity = '1';
}
function refreshPublicRooms() {
if (!listPublic || !tplRow) return;
if (USE_PUBLIC_ROOM_MOCK) {
renderPublicRooms(MOCK_PUBLIC_ROOMS);
return;
}
listPublic.innerHTML = '<div class="join-list-status" id="room-list-status">โหลดรายการห้อง...</div>';
var statusEl = document.getElementById('room-list-status');
fetch(BASE + '/api/spaces', { cache: 'no-store' })
fetch(SERVER + '/api/spaces?_=' + Date.now())
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list)) {
if (statusEl) statusEl.textContent = 'โหลดรายการห้องไม่ได้';
return;
}
listPublic.innerHTML = '';
if (list.length === 0) {
listPublic.innerHTML = '<div class="join-list-status">ยังไม่มีห้อง</div>';
return;
}
list.forEach(function (room) {
var code = room.spaceId || ('room-' + Math.random().toString(36).slice(2, 8));
var name = room.spaceName || code;
var mapName = room.mapName || '—';
var count = room.peerCount != null ? room.peerCount : 0;
var max = room.maxPlayers != null ? room.maxPlayers : 10;
var full = count >= max;
var node = tplRow.content.cloneNode(true);
var nameEl = node.querySelector('.join-room-name');
var playersEl = node.querySelector('.join-room-players');
var btn = node.querySelector('.join-room-btn');
var img = node.querySelector('.join-room-btn-img');
nameEl.textContent = escapeHtml(name) + (mapName !== '—' ? ' (ฉาก: ' + escapeHtml(mapName) + ')' : '');
playersEl.textContent = count + '/' + max + ' คน' + (full ? ' (เต็ม)' : '');
img.src = full ? IMG.joinOff : IMG.joinOn;
img.alt = full ? 'เต็ม' : 'เข้าร่วม';
if (full) {
btn.disabled = true;
} else {
btn.addEventListener('click', function () {
goToRoom(room.spaceId);
});
}
listPublic.appendChild(node);
});
renderPublicRooms(list);
})
.catch(function () {
if (statusEl) statusEl.textContent = 'โหลดรายการห้องไม่ได้';
@@ -142,7 +197,13 @@
goToRoom(code);
});
refreshPublicRooms();
setInterval(refreshPublicRooms, 8000);
if (USE_PUBLIC_ROOM_MOCK) {
renderPublicRooms(MOCK_PUBLIC_ROOMS);
} else {
refreshPublicRooms();
setInterval(refreshPublicRooms, 8000);
}
listPublic?.addEventListener('scroll', syncPublicScrollbarThumb);
window.addEventListener('resize', syncPublicScrollbarThumb);
setTab('public');
})();
+297 -74
View File
@@ -15,6 +15,14 @@ body {
background: #0a0b1e;
}
@font-face {
font-family: 'NotoSansThai';
src: url('../FONTS/NotoSansThai-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
.join-bg {
position: fixed;
inset: 0;
@@ -34,7 +42,7 @@ body {
.join-char-layer {
position: fixed;
inset: 0;
z-index: 1;
z-index: 0;
pointer-events: none;
display: flex;
justify-content: center;
@@ -47,6 +55,7 @@ body {
max-height: 72vh;
object-fit: contain;
object-position: center;
filter: brightness(0.62);
}
.join-vignette {
@@ -68,17 +77,18 @@ body {
}
.join-back-section {
position: fixed;
top: max(12px, env(safe-area-inset-top));
left: max(12px, env(safe-area-inset-left));
z-index: 10;
position: static;
}
.join-btn-back {
position: fixed;
top: max(clamp(0.5rem, 2vw, 2rem), env(safe-area-inset-top));
left: max(clamp(0.5rem, 2vw, 2rem), env(safe-area-inset-left));
padding: 0;
border: none;
background: transparent;
cursor: pointer;
z-index: 20;
-webkit-tap-highlight-color: transparent;
}
@@ -86,8 +96,10 @@ body {
display: block;
width: clamp(40px, 10vw, 60px);
height: auto;
object-fit: contain;
}
.join-content {
width: 100%;
max-width: min(96vw, 720px);
@@ -104,25 +116,40 @@ body {
}
.join-title-img {
--join-title-height: 14cqh; /* ปรับความสูง txt-joinroom ที่นี่ */
display: block;
width: min(88vw, 420px);
height: auto;
height: var(--join-title-height);
width: calc(var(--join-title-height) * 535 / 137);
max-width: 88vw;
margin-inline: auto;
aspect-ratio: 535 / 137;
object-fit: contain;
image-rendering: -webkit-optimize-contrast;
filter: drop-shadow(0 0 16px rgba(191, 0, 255, 0.55));
filter: drop-shadow(0 0 16px rgba(191, 0, 255, 0.55));
margin-top: 0cqh;
}
.join-title-line {
--line-height: 5cqh; /* ปรับความสูง line.png ที่นี่ */
display: block;
height: var(--line-height);
width: min(calc(var(--line-height) * 1018 / 6), 100%);
max-width: none;
aspect-ratio: 1018 / 6;
object-fit: contain;
margin: -2cqh auto -3.5cqh;
}
/* กล่องหลักตาม mock — bg-pop */
.join-panel {
width: 100%;
padding: clamp(14px, 3vw, 22px) clamp(12px, 2.5vw, 20px) clamp(16px, 3vw, 24px);
padding: 0px;
border-radius: 10px;
background-image: url('IMAGE/bg-pop.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
box-shadow:
0 0 20px rgba(0, 255, 255, 0.25),
0 0 40px rgba(191, 0, 255, 0.15);
container-type: inline-size;
}
.join-tabs {
@@ -130,8 +157,14 @@ body {
flex-direction: row;
justify-content: center;
align-items: flex-end;
--join-tab-overlap: 0px;
gap: clamp(0.5rem, 2vw, 1rem);
margin-bottom: clamp(12px, 2.5vw, 18px);
margin-bottom: clamp(8px, 1.2cqh, 14px);
width: 100%;
}
.join-tab + .join-tab {
margin-left: var(--join-tab-overlap);
}
.join-tab {
@@ -159,64 +192,105 @@ body {
display: none !important;
}
#join-public-view {
display: flex;
width: 100%;
height: 100%;
}
#join-private-view {
height: 100%;
min-height: min(48vh, 420px);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: clamp(8px, 2vh, 16px) 0;
container-type: inline-size;
}
/* พื้นที่สกรอล + แถบเลื่อนตาม asset */
.join-list-scroll {
--join-scrollbar-width: clamp(12px, 3.8cqw, 25px); /* ปรับความอ้วน scrollbar ที่นี่ */
--join-scrollbar-thumb-inset: clamp(2px, 0.8cqw, 6px);
--join-scrollbar-right-offset: clamp(4px, 2.2cqw, 17px); /* ขยับราง+แท่ง scrollbar ไปซ้าย/ขวา */
position: relative;
padding-right: clamp(10px, 2.5vw, 16px);
margin-right: -4px;
width: 100%;
height: 95%;
min-height: 0;
padding-right: calc(var(--join-scrollbar-width) + var(--join-scrollbar-thumb-inset) + 2px);
margin-top: 1cqh;
margin-right: var(--join-scrollbar-right-offset);
}
.join-list-scroll::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 14px;
right: var(--join-scrollbar-right-offset);
width: var(--join-scrollbar-width);
height: 100%;
background: url('IMAGE/join-room-scrollbar-bg.png') center top / 100% 100% no-repeat;
pointer-events: none;
opacity: 0.95;
border-radius: 4px;
border-radius: 6px;
}
.join-scrollbar-thumb {
position: absolute;
right: calc(var(--join-scrollbar-right-offset) + var(--join-scrollbar-thumb-inset));
top: var(--join-scrollbar-thumb-inset);
width: calc(var(--join-scrollbar-width) - (var(--join-scrollbar-thumb-inset) * 2));
min-height: clamp(18px, 4.2cqh, 40px);
border-radius: 999px;
pointer-events: none;
background:
linear-gradient(180deg, rgba(80, 255, 255, 0.95) 0%, rgba(0, 210, 255, 0.95) 100%);
border: clamp(1px, 0.25cqw, 2px) solid rgba(120, 255, 255, 0.9);
box-shadow:
0 0 10px rgba(0, 255, 255, 0.65),
inset 0 0 6px rgba(255, 255, 255, 0.35);
transform: translateY(0);
opacity: 0;
transition: opacity 120ms ease;
}
.join-list {
min-height: min(48vh, 420px);
max-height: min(48vh, 420px);
--join-list-shift-x: 2cqw; /* ขยับรายการห้องไปทางขวา */
--join-room-row-stack-offset: -3cqh; /* ปรับระยะห่างแนวตั้งระหว่างห้อง (ติดลบได้) */
height: 100%;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding-right: 6px;
padding-right: 4px;
margin-left: var(--join-list-shift-x);
display: flex;
flex-direction: column;
/* gap: clamp(0.25rem, 1vw, 0.45rem); */
}
.join-room-row + .join-room-row {
margin-top: var(--join-room-row-stack-offset);
}
.join-list::-webkit-scrollbar {
width: 10px;
}
.join-list::-webkit-scrollbar-track {
background: rgba(0, 40, 60, 0.4);
border-radius: 4px;
}
.join-list::-webkit-scrollbar-thumb {
background: url('IMAGE/join-room-scrollbar.png') center / contain no-repeat;
background-color: rgba(0, 255, 255, 0.35);
border-radius: 4px;
min-height: 40px;
width: 0;
height: 0;
}
.join-list {
scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 255, 0.5) rgba(0, 40, 60, 0.4);
scrollbar-width: none;
scrollbar-color: rgba(0, 255, 255, 1) rgba(0, 40, 60, 0.4);
}
.join-list-status {
margin: auto;
--join-list-status-font-size: 3.5cqw; /* ปรับขนาดฟอนต์สถานะห้องสาธารณะ */
padding: 1rem;
font-size: clamp(0.8rem, 2vw, 0.92rem);
font-size: var(--join-list-status-font-size);
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', sans-serif;
color: rgba(255, 255, 255, 0.85);
text-align: center;
margin-top: 2cqh;
}
/* แถวห้อง — bg-room-name */
@@ -225,13 +299,19 @@ body {
}
.join-room-row-inner {
--join-room-row-height: 16.5cqh; /* ปรับความสูง bg-room-name ได้ที่นี่ */
--join-room-card-padding-y: 1.6cqh; /* ปรับ padding บน/ล่างของกล่องห้อง */
--join-room-card-padding-x: 4cqw; /* ปรับ padding ซ้าย/ขวาของกล่องห้อง */
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: clamp(0.35rem, 1.2vw, 0.75rem);
height: clamp(64px, 14vw, 90px);
padding: clamp(8px, 2vw, 12px) clamp(10px, 2.5vw, 16px);
height: var(--join-room-row-height);
width: calc(var(--join-room-row-height) * (910 / 160));
max-width: 100%;
aspect-ratio: 910 / 160;
padding: var(--join-room-card-padding-y) var(--join-room-card-padding-x);
background-image: url('IMAGE/bg-room-name.png');
background-size: 100% 100%;
background-repeat: no-repeat;
@@ -239,10 +319,12 @@ body {
}
.join-room-name {
--join-room-name-font-size: 2.5cqw; /* ปรับขนาดฟอนต์ชื่อห้อง */
flex: 1 1 auto;
min-width: 0;
font-size: clamp(0.72rem, 1.9vw, 0.88rem);
font-size: var(--join-room-name-font-size);
font-weight: 700;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', sans-serif;
color: #fff;
text-shadow: 0 0 8px rgba(0, 255, 255, 0.25);
overflow: hidden;
@@ -253,9 +335,11 @@ body {
}
.join-room-players {
--join-room-players-font-size: 2.5cqw; /* ปรับขนาดฟอนต์จำนวนผู้เล่น */
flex: 0 0 auto;
font-size: clamp(0.72rem, 1.9vw, 0.88rem);
font-size: var(--join-room-players-font-size);
font-weight: 600;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', sans-serif;
color: #fff;
white-space: nowrap;
line-height: 1.3;
@@ -272,58 +356,90 @@ body {
.join-room-btn:disabled {
cursor: not-allowed;
opacity: 0.85;
filter: grayscale(0.35) brightness(0.65);
opacity: 0.5;
filter: none;
}
.join-room-btn-img {
--join-room-btn-height: 12cqh; /* ปรับความสูงปุ่มเข้าร่วมในรายการห้อง */
display: block;
width: clamp(72px, 20vw, 110px);
height: auto;
height: var(--join-room-btn-height);
width: calc(var(--join-room-btn-height) * (198 / 119));
aspect-ratio: 198 / 119;
max-width: 100%;
image-rendering: -webkit-optimize-contrast;
transform: translateX(1.5cqw);
}
/* แท็บห้องส่วนตัว — ฟอร์ม */
.join-private-form {
--join-private-row-width: 100%;
--join-private-label-width: 25%; /* ปรับความกว้างคอลัมน์ label */
--join-private-pass-scale: 1.15; /* ทำให้ PASSWORD ใหญ่กว่า ROOM ID */
--join-private-input-width: 70%; /* ปรับความกว้าง textfield */
--join-private-row-gap: 0;
--join-private-shift-x: -2cqw; /* ขยับฟอร์มไปทางขวา */
display: flex;
flex-direction: column;
gap: clamp(0.75rem, 2vw, 1.1rem);
padding-top: clamp(4px, 1vw, 8px);
width: min(96%, 94cqw);
max-width: 100%;
gap: clamp(0.6rem, 2.2cqw, 2.2cqw);
padding-top: 0;
margin: auto;
transform: translateX(var(--join-private-shift-x));
}
.join-private-row + .join-private-row {
margin-top: -4cqh; /* ค่าติดลบได้ */
}
.join-private-row {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: clamp(0.6rem, 2.2vw, 1rem);
justify-content: flex-end;
width: var(--join-private-row-width);
margin-inline: auto;
gap: var(--join-private-row-gap);
}
.join-private-label-img {
--join-private-label-shift-x: 2.5cqw; /* ขยับ label ไปทางขวา */
display: block;
height: clamp(28px, 6.5vw, 40px);
width: auto;
width: 100%;
height: auto;
aspect-ratio: 211 / 83;
object-fit: contain;
image-rendering: -webkit-optimize-contrast;
flex: 0 0 auto;
flex: 0 0 var(--join-private-label-width);
max-width: var(--join-private-label-width);
margin-left: auto;
transform: translateX(var(--join-private-label-shift-x));
}
.join-private-label-img--pass {
height: clamp(26px, 6vw, 36px);
--join-private-label-shift-x: 2.5cqw;
width: calc(100% * var(--join-private-pass-scale));
height: auto;
aspect-ratio: 228 / 84;
flex: 0 0 calc(var(--join-private-label-width) * var(--join-private-pass-scale));
max-width: calc(var(--join-private-label-width) * var(--join-private-pass-scale));
margin-left: auto;
}
.join-textfield-wrap {
position: relative;
flex: 1 1 auto;
min-width: 200px;
max-width: min(100%, 75%);
height: clamp(48px, 11vw, 60px);
flex: 0 0 var(--join-private-input-width);
width: 100%;
min-width: 0;
aspect-ratio: 613 / 141;
height: auto;
max-width: var(--join-private-input-width);
}
.join-textfield-wrap--narrow {
flex: 0 1 auto;
min-width: 160px;
max-width: 260px;
flex: 0 0 var(--join-private-input-width);
width: 100%;
max-width: var(--join-private-input-width);
}
.join-textfield-wrap::before {
@@ -331,7 +447,7 @@ body {
position: absolute;
inset: 0;
background-image: url('IMAGE/textfield.png');
background-size: cover;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
@@ -345,16 +461,16 @@ input.join-textfield-input,
z-index: 2;
width: 100% !important;
height: 100% !important;
padding-top: clamp(0.35rem, 1vw, 0.55rem) !important;
padding-bottom: clamp(0.35rem, 1vw, 0.55rem) !important;
padding-left: clamp(0.85rem, 2.2vw, 1.15rem) !important;
padding-right: clamp(0.85rem, 2.2vw, 1.15rem) !important;
padding-top: clamp(0.28rem, 0.9cqw, 0.55rem) !important;
padding-bottom: clamp(0.28rem, 0.9cqw, 0.55rem) !important;
padding-left: clamp(0.65rem, 1.8cqw, 1.15rem) !important;
padding-right: clamp(0.65rem, 1.8cqw, 1.15rem) !important;
border: none !important;
background: transparent !important;
color: #fff;
font-size: clamp(0.88rem, 2.2vw, 1.02rem);
font-size: clamp(0.5rem, 2.4cqw, 3cqw);
font-weight: 600;
font-family: inherit;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', sans-serif;
line-height: 1.5;
outline: none;
box-sizing: border-box;
@@ -372,9 +488,9 @@ input.join-textfield-input,
.join-textfield-input--code {
text-align: center;
font-family: 'Courier New', monospace;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', sans-serif;
letter-spacing: 0.18em;
font-size: clamp(0.95rem, 2.5vw, 1.15rem);
font-size: clamp(0.5rem, 2.7cqw, 3cqw);
}
.join-textfield-input::placeholder {
@@ -384,7 +500,7 @@ input.join-textfield-input,
.join-private-submit {
align-self: center;
padding: 0;
margin-top: 0.25rem;
margin-top: clamp(0.4rem, 1.5cqh, 0.9rem);
border: none;
background: transparent;
cursor: pointer;
@@ -397,13 +513,40 @@ input.join-textfield-input,
}
.join-private-submit-img {
--join-private-submit-height: 16cqh; /* ปรับความสูงปุ่มเข้าร่วมได้ที่นี่ */
display: block;
width: min(72vw, 280px);
height: auto;
height: var(--join-private-submit-height);
width: calc(var(--join-private-submit-height) * (404 / 141));
aspect-ratio: 404 / 141;
max-width: 100%;
image-rendering: -webkit-optimize-contrast;
transform: translateX(4cqw);
}
@media (max-width: 520px) {
.join-private-form {
--join-private-label-width: 100%;
--join-private-input-width: 100%;
}
.join-private-row {
justify-content: center;
width: 100%;
gap: 0.35rem;
flex-wrap: wrap;
}
.join-private-label-img,
.join-private-label-img--pass {
margin-left: 0;
max-width: 100%;
}
.join-textfield-wrap {
width: min(100%, 86vw);
max-width: 100%;
}
.join-room-row-inner {
flex-wrap: wrap;
justify-content: center;
@@ -420,3 +563,83 @@ input.join-textfield-input,
margin-top: 0.25rem;
}
}
@media (min-width: 1025px) {
html,
body {
height: 100%;
overflow: hidden;
}
body {
min-height: 100dvh;
max-height: 100dvh;
}
.join-btn-back-img {
width: 112px;
height: 139px;
}
.join-ui {
justify-content: center;
align-items: center;
height: 100dvh;
min-height: 100dvh;
max-height: 100dvh;
overflow: hidden;
}
.join-content {
position: relative;
z-index: 40;
isolation: isolate;
width: min(calc(95dvh * 1), 90vw);
max-width: min(calc(95dvh * 1), 90vw);
height: auto;
min-height: 0;
max-height: none;
aspect-ratio: 1 / 1;
margin: 0 auto;
padding: 2.1dvh 0 1.4dvh;
justify-content: flex-start;
container-type: size;
overflow: hidden;
gap: 1.2cqh;
}
.join-panel {
--join-panel-height: 70cqh; /* ปรับความสูง bg-pop ที่นี่ */
height: var(--join-panel-height);
width: calc(var(--join-panel-height) * 1008 / 681);
max-width: 100%;
aspect-ratio: 1008 / 681;
padding: 2.4cqh 2.2cqw 2.2cqh;
background-size: 100% 100%;
}
#join-private-view {
min-height: 0;
height: 100%;
}
.join-tabs {
--join-tab-height: 14.5cqh; /* ปรับความสูงปุ่มห้องสาธารณะ/ส่วนตัวที่นี่ */
--join-tab-overlap: -3cqh; /* ติดลบได้: ปุ่มจะซ้อนกันมากขึ้น */
margin-bottom: -4cqh;
}
.join-tab {
flex: 0 0 auto;
height: var(--join-tab-height);
width: calc(var(--join-tab-height) * 499 / 143);
aspect-ratio: 499 / 143;
}
.join-tab-img {
width: 100%;
height: 100%;
aspect-ratio: 499 / 143;
object-fit: contain;
}
}
+1 -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>เข้าสู่ระบบ — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css?v=20260526">
</head>
<body>
<div class="login-bg" role="img" aria-label="พื้นหลัง">
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+235
View File
@@ -0,0 +1,235 @@
@font-face {
font-family: 'NotoSansThai';
src: url('../FONTS/NotoSansThai-ExtraBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
.daily-popup-overlay {
position: fixed;
inset: 0;
z-index: 10120;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
}
.daily-popup-overlay.is-hidden {
display: none !important;
}
.daily-popup-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.72);
}
.daily-popup-dialog {
position: relative;
z-index: 1;
width: min(1181px, calc(100vw - 24px), calc((100dvh - 24px) * (1181 / 960)));
aspect-ratio: 1181 / 960;
max-height: calc(100dvh - 24px);
--dr-scale: 1;
font-family: 'NotoSansThai', 'Kanit', system-ui, sans-serif;
}
.daily-popup-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
z-index: 1;
}
.daily-popup-close {
position: absolute;
top: calc(85px * var(--dr-scale));
right: calc(75px * var(--dr-scale));
width: calc(78px * var(--dr-scale));
height: calc(78px * var(--dr-scale));
border: none;
background: #fff;
opacity: 0;
color: transparent;
font-size: 0;
cursor: pointer;
z-index: 6;
}
.daily-popup-content {
position: absolute;
inset: calc(122px * var(--dr-scale)) calc(62px * var(--dr-scale)) calc(80px * var(--dr-scale));
display: flex;
flex-direction: column;
align-items: center;
z-index: 5;
}
.daily-popup-reset {
margin: calc(50px * var(--dr-scale)) 0 calc(25px * var(--dr-scale));
text-align: center;
color: #d9e5ff;
font-size: calc(30px * var(--dr-scale));
}
.daily-popup-grid {
--daily-popup-grid-gap-x: calc(-10px * var(--dr-scale));
--daily-popup-grid-gap-y: calc(-10px * var(--dr-scale));
margin: calc(var(--daily-popup-grid-gap-y) / -2) calc(var(--daily-popup-grid-gap-x) / -2) 0;
width: calc(1052px * var(--dr-scale));
max-width: 100%;
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
}
.daily-popup-card {
position: relative;
width: calc(254px * var(--dr-scale));
height: calc(311px * var(--dr-scale));
isolation: isolate;
flex: 0 0 auto;
margin: calc(var(--daily-popup-grid-gap-y) / 2) calc(var(--daily-popup-grid-gap-x) / 2);
}
.daily-popup-card.day-7 {
width: calc(505px * var(--dr-scale));
height: calc(319px * var(--dr-scale));
}
.daily-popup-card-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 0;
}
.daily-popup-received-top-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 4;
pointer-events: none;
}
.daily-popup-card-inner {
--daily-day-offset-y: calc(20px * var(--dr-scale));
--daily-claim-offset-y: calc(-11px * var(--dr-scale));
position: absolute;
inset: calc(12px * var(--dr-scale)) calc(10px * var(--dr-scale)) calc(14px * var(--dr-scale));
display: flex;
flex-direction: column;
align-items: center;
}
.daily-popup-day-img {
width: calc(101px * var(--dr-scale));
aspect-ratio: 101 / 41;
height: auto;
object-fit: contain;
margin-top: var(--daily-day-offset-y);
}
.daily-popup-icon-wrap {
position: relative;
margin-top: calc(-10px * var(--dr-scale));
width: calc(161px * var(--dr-scale));
aspect-ratio: 1 / 1;
height: auto;
}
.daily-popup-icon-bg,
.daily-popup-reward,
.daily-popup-icon-frame,
.daily-popup-icon-lock-overlay,
.daily-popup-redeemed {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.daily-popup-reward {
width: calc(151px * 1 * var(--dr-scale));
aspect-ratio: 151 / 101;
height: auto;
left: 50%;
top: 50%;
transform: translate(4%, 27%);
inset: auto;
object-fit: contain;
}
.daily-popup-redeemed {
width: calc(89px * var(--dr-scale));
aspect-ratio: 89 / 77;
height: auto;
left: 50%;
top: 50%;
transform: translate(50%, 50%);
inset: auto;
z-index: 5;
}
.daily-popup-redeemed.is-claim-animate {
animation: dailyRedeemedPopIn 360ms cubic-bezier(0.2, 0.9, 1, 1) both;
}
@keyframes dailyRedeemedPopIn {
0% {
transform: translate(50%, 50%) scale(1.55);
opacity: 0;
}
100% {
transform: translate(50%, 50%) scale(1);
opacity: 1;
}
}
.daily-popup-status-img {
margin-top: calc(5px * var(--dr-scale));
width: calc(151px * var(--dr-scale));
aspect-ratio: 151 / 41;
height: auto;
object-fit: contain;
}
.daily-popup-claim-btn {
margin-top: var(--daily-claim-offset-y);
width: calc(208px * 1 * var(--dr-scale));
aspect-ratio: 208 / 76;
margin-left: calc(6px * var(--dr-scale));
border: none;
background: transparent;
padding: 0;
cursor: pointer;
}
.daily-popup-claim-btn-img {
display: block;
width: 100%;
height: auto;
object-fit: contain;
}
.daily-popup-preload {
position: absolute;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
overflow: hidden;
}
+578
View File
@@ -0,0 +1,578 @@
(function () {
'use strict';
function appPath(path) {
if (typeof window.appPath === 'function') return window.appPath(path);
if (typeof window.projectPath === 'function') return window.projectPath(path);
var base = '';
try {
base = window.APP_BASE || sessionStorage.getItem('APP_BASE') || localStorage.getItem('APP_BASE') || '';
} catch (e) {
base = window.APP_BASE || '';
}
var normalized = String(path || '/').replace(/\\/g, '/');
normalized = normalized.startsWith('/') ? normalized : '/' + normalized;
return (String(base || '').replace(/\/$/, '')) + normalized;
}
var DEFAULT_ASSET_BASE = 'IMAGE/Daily';
function makeAssetResolver(base) {
var root = String(base || DEFAULT_ASSET_BASE).replace(/\\/g, '/').replace(/\/+$/, '');
return function asset(fileName) {
var cleanFile = String(fileName || '').replace(/^\/+/, '');
if (/^(?:https?:)?\/\//i.test(root)) {
return root + '/' + cleanFile;
}
/* ใช้ path เทียบกับหน้า Main-Lobby เสมอ — ไม่ผูก APP_BASE (กัน localhost ดึงรูปจาก production) */
var relRoot = root.replace(/^\/+/, '');
if (/^Main-Lobby\//i.test(relRoot)) {
relRoot = relRoot.slice('Main-Lobby/'.length);
}
try {
return new URL(relRoot + '/' + cleanFile, window.location.href).href;
} catch (e) {
return relRoot + '/' + cleanFile;
}
};
}
function findDatasetScript() {
var scripts = Array.prototype.slice.call(document.querySelectorAll('script[src]'));
for (var i = 0; i < scripts.length; i += 1) {
var src = scripts[i].getAttribute('src') || '';
if (src.indexOf('daily-popup.js') !== -1) return scripts[i];
}
return null;
}
var scriptEl = findDatasetScript();
var config = (scriptEl && scriptEl.dataset) || {};
var asset = makeAssetResolver(config.dailyAssetBase || DEFAULT_ASSET_BASE);
var triggerSelector = config.dailyTrigger || '#btn-daily';
var TOTAL_DAYS = 7;
var testResetSeconds = Math.max(0, Number(config.dailyTestResetSeconds || 0));
var isFastTestMode = testResetSeconds > 0;
var testResetMs = testResetSeconds * 1000;
var normalResetMs = 24 * 60 * 60 * 1000;
var activeResetMs = isFastTestMode ? testResetMs : normalResetMs;
var DAILY_STORAGE_KEY = 'jdDailyProgressV3:shared';
var state = {
isOpen: false,
claimedAnimDay: null,
currentDay: 3,
msUntilReset: 0,
progress: null
};
var refs = {
overlay: null,
dialog: null,
cardsEl: null,
resetEl: null
};
var resetTimerId = null;
var dailyDotTimerId = null;
function pad2(value) {
return String(value).padStart(2, '0');
}
function createDefaultProgress(nowMs) {
var anchorMs = nowMs;
if (!isFastTestMode) {
var midnight = new Date(nowMs);
midnight.setHours(0, 0, 0, 0);
anchorMs = midnight.getTime();
}
return {
anchorMs: anchorMs,
claimedDays: [false, false, false, false, false, false, false],
lockUntilMs: 0,
timingKey: isFastTestMode ? ('test-' + String(testResetSeconds)) : 'normal'
};
}
function getFirstUnclaimedDay(claimedDays) {
for (var i = 0; i < TOTAL_DAYS; i += 1) {
if (!claimedDays[i]) return i + 1;
}
return 0;
}
function normalizeProgressTiming(progress, nowMs) {
var currentTimingKey = isFastTestMode ? ('test-' + String(testResetSeconds)) : 'normal';
if (progress.timingKey === currentTimingKey) return;
var firstUnclaimed = getFirstUnclaimedDay(progress.claimedDays);
if (firstUnclaimed > 0) {
// ย้ายโหมดเวลาแล้วให้วันที่รอเคลม "ขึ้นกดได้" ทันทีในโหมดใหม่
if (isFastTestMode) {
progress.anchorMs = nowMs - Math.max(0, firstUnclaimed - 1) * activeResetMs;
} else {
var nowMidnight = new Date(nowMs);
nowMidnight.setHours(0, 0, 0, 0);
progress.anchorMs = nowMidnight.getTime() - Math.max(0, firstUnclaimed - 1) * normalResetMs;
}
progress.lockUntilMs = 0;
} else {
// เคลมครบ 7 แล้ว ให้รออีก 1 รอบของโหมดใหม่ก่อนรีเซ็ต
if (isFastTestMode) {
progress.anchorMs = nowMs;
progress.lockUntilMs = nowMs + activeResetMs;
} else {
var anchorMidnight = new Date(nowMs);
anchorMidnight.setHours(0, 0, 0, 0);
progress.anchorMs = anchorMidnight.getTime();
progress.lockUntilMs = nowMs + getMsUntilNextMidnight(nowMs);
}
}
progress.timingKey = currentTimingKey;
}
function loadProgress() {
var nowMs = Date.now();
try {
var raw = localStorage.getItem(DAILY_STORAGE_KEY);
if (!raw) {
var legacyKeys = [
'jdDailyProgressV3:normal',
'jdDailyProgressV3:test-' + String(testResetSeconds),
'jdDailyTestStateV2:' + String(testResetSeconds || 'default')
];
for (var i = 0; i < legacyKeys.length; i += 1) {
raw = localStorage.getItem(legacyKeys[i]);
if (raw) {
localStorage.setItem(DAILY_STORAGE_KEY, raw);
break;
}
}
}
if (!raw) {
var fresh = createDefaultProgress(nowMs);
localStorage.setItem(DAILY_STORAGE_KEY, JSON.stringify(fresh));
return fresh;
}
var parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') throw new Error('invalid');
if (!Array.isArray(parsed.claimedDays) || parsed.claimedDays.length !== TOTAL_DAYS) throw new Error('invalid');
if (typeof parsed.anchorMs !== 'number' || !isFinite(parsed.anchorMs)) throw new Error('invalid');
if (typeof parsed.lockUntilMs !== 'number' || !isFinite(parsed.lockUntilMs)) parsed.lockUntilMs = 0;
normalizeProgressTiming(parsed, nowMs);
return parsed;
} catch (e) {
var fallback = createDefaultProgress(nowMs);
try { localStorage.setItem(DAILY_STORAGE_KEY, JSON.stringify(fallback)); } catch (e2) { /* ignore */ }
return fallback;
}
}
function saveProgress() {
if (!state.progress) return;
try {
localStorage.setItem(DAILY_STORAGE_KEY, JSON.stringify(state.progress));
} catch (e) { /* ignore */ }
}
function syncProgress() {
if (!state.progress) return false;
var progress = state.progress;
var nowMs = Date.now();
var elapsedMs = Math.max(0, nowMs - progress.anchorMs);
var slotMs = elapsedMs % activeResetMs;
var dayIndex = Math.floor(elapsedMs / activeResetMs);
var unlockedByTimeDay = Math.min(TOTAL_DAYS, dayIndex + 1);
var remainMs = activeResetMs - slotMs;
if (remainMs <= 0 || remainMs > activeResetMs) remainMs = activeResetMs;
if (!isFastTestMode) {
var anchorMidnight = new Date(progress.anchorMs);
anchorMidnight.setHours(0, 0, 0, 0);
var nowMidnight = new Date(nowMs);
nowMidnight.setHours(0, 0, 0, 0);
dayIndex = Math.max(0, Math.floor((nowMidnight.getTime() - anchorMidnight.getTime()) / normalResetMs));
unlockedByTimeDay = Math.min(TOTAL_DAYS, dayIndex + 1);
remainMs = getMsUntilNextMidnight(nowMs);
}
var firstUnclaimedDay = 0;
for (var i = 0; i < TOTAL_DAYS; i += 1) {
if (!progress.claimedDays[i]) {
firstUnclaimedDay = i + 1;
break;
}
}
var changed = false;
// Self-heal: ถ้าข้อมูลเก่าค้างในสถานะเคลมครบแต่ไม่มี lockUntil ให้ตั้ง lock ถึง boundary ถัดไป
if (!firstUnclaimedDay && Number(progress.lockUntilMs || 0) <= 0) {
progress.lockUntilMs = nowMs + Math.max(1, remainMs);
changed = true;
}
// รีเซ็ตรอบใหม่เฉพาะตอนเคลมครบ 7 วันแล้ว และถึง boundary ถัดไปเท่านั้น
if (!firstUnclaimedDay && Number(progress.lockUntilMs || 0) > 0 && nowMs >= progress.lockUntilMs) {
if (isFastTestMode) {
progress.anchorMs = progress.lockUntilMs;
} else {
var resetMidnight = new Date(nowMs);
resetMidnight.setHours(0, 0, 0, 0);
progress.anchorMs = resetMidnight.getTime();
}
progress.claimedDays = [false, false, false, false, false, false, false];
progress.lockUntilMs = 0;
elapsedMs = Math.max(0, nowMs - progress.anchorMs);
slotMs = elapsedMs % activeResetMs;
dayIndex = Math.floor(elapsedMs / activeResetMs);
unlockedByTimeDay = Math.min(TOTAL_DAYS, dayIndex + 1);
remainMs = activeResetMs - slotMs;
if (remainMs <= 0 || remainMs > activeResetMs) remainMs = activeResetMs;
if (!isFastTestMode) {
remainMs = getMsUntilNextMidnight(nowMs);
}
firstUnclaimedDay = 1;
changed = true;
}
var currentDay = 0;
if (firstUnclaimedDay > 0 && firstUnclaimedDay <= unlockedByTimeDay) {
currentDay = firstUnclaimedDay;
}
var canUnlockByTime = firstUnclaimedDay > 0 && firstUnclaimedDay <= unlockedByTimeDay;
var lockActive = Number(progress.lockUntilMs || 0) > nowMs;
var gatedDay = canUnlockByTime && !lockActive ? firstUnclaimedDay : 0;
if (!lockActive && progress.lockUntilMs) {
progress.lockUntilMs = 0;
changed = true;
}
if (state.currentDay !== gatedDay) changed = true;
state.currentDay = gatedDay;
state.msUntilReset = remainMs;
if (changed) saveProgress();
return changed;
}
function hasPendingClaim() {
for (var day = 1; day <= TOTAL_DAYS; day += 1) {
if (getDayStatus(day) === 'claim') return true;
}
return false;
}
function syncDailyDotIndicator() {
var dailyBtn = document.getElementById('btn-daily');
if (!dailyBtn) return;
var dot = dailyBtn.querySelector('.lobby-daily-dot');
if (!dot) return;
dot.style.display = hasPendingClaim() ? '' : 'none';
}
function startDailyDotWatcher() {
syncDailyDotIndicator();
if (dailyDotTimerId != null) return;
dailyDotTimerId = window.setInterval(function () {
var changed = syncProgress();
if (changed && refs.cardsEl && state.isOpen) renderCards();
syncDailyDotIndicator();
}, 1000);
}
var ALL_DAILY_ASSETS = [
'btn-claim-reward.png',
'daily-bg.png',
'Day-1-h.png',
'Day-1.png',
'Day-2-h.png',
'Day-2.png',
'Day-3-h.png',
'Day-3.png',
'Day-4-h.png',
'Day-4.png',
'Day-5-h.png',
'Day-5.png',
'Day-6-h.png',
'Day-6.png',
'Day-7-h.png',
'Day-7.png',
'icon-bg.png',
'icon-frame-h.png',
'icon-frame-lock.png',
'icon-frame.png',
'reward-01.png',
'reward-02.png',
'reward-03.png',
'reward-04.png',
'reward-05.png',
'reward-06.png',
'reward-07.png',
'reward-redeemed.png',
'rewards-frame-d7.png',
'rewards-frame-hilight-d7.png',
'rewards-frame-hilight.png',
'rewards-frame-received-d7.png',
'rewards-frame-d7-received-top.png',
'rewards-frame-received-top.png',
'rewards-frame-received.png',
'rewards-frame.png',
'txt-locked.png',
'txt-recieved.png'
];
function getDayStatus(day) {
if (state.progress) {
if (state.progress.claimedDays[day - 1]) return 'received';
if (day === state.currentDay) return 'claim';
return 'locked';
}
return day === 1 ? 'claim' : 'locked';
}
function getCardFrame(day, status) {
var isDay7 = day === 7;
if (status === 'received') {
if (isDay7) return 'rewards-frame-received-d7.png';
return 'rewards-frame-received.png';
}
if (status === 'claim') return isDay7 ? 'rewards-frame-hilight-d7.png' : 'rewards-frame-hilight.png';
return isDay7 ? 'rewards-frame-d7.png' : 'rewards-frame.png';
}
function getIconFrame(status) {
if (status === 'claim') return 'icon-frame-h.png';
return 'icon-frame.png';
}
function getDayTitle(day, status) {
if (status === 'claim') return 'Day-' + day + '-h.png';
return 'Day-' + day + '.png';
}
function makeImg(className, src, alt) {
var img = document.createElement('img');
img.className = className;
img.src = src;
img.alt = alt || '';
img.decoding = 'async';
return img;
}
function appendPreloadContainer(parent) {
var preload = document.createElement('div');
preload.className = 'daily-popup-preload';
preload.setAttribute('aria-hidden', 'true');
ALL_DAILY_ASSETS.forEach(function (fileName) {
preload.appendChild(makeImg('', asset(fileName), ''));
});
parent.appendChild(preload);
}
function createCard(day) {
var status = getDayStatus(day);
var li = document.createElement('li');
li.className = 'daily-popup-card ' + status + (day === 7 ? ' day-7' : '');
li.appendChild(makeImg('daily-popup-card-frame', asset(getCardFrame(day, status)), ''));
if (status === 'received') {
var topOverlayFile = day === 7 ? 'rewards-frame-d7-received-top.png' : 'rewards-frame-received-top.png';
li.appendChild(makeImg('daily-popup-received-top-overlay', asset(topOverlayFile), ''));
}
var inner = document.createElement('div');
inner.className = 'daily-popup-card-inner';
li.appendChild(inner);
inner.appendChild(makeImg('daily-popup-day-img', asset(getDayTitle(day, status)), 'Day ' + day));
var iconWrap = document.createElement('div');
iconWrap.className = 'daily-popup-icon-wrap';
iconWrap.appendChild(makeImg('daily-popup-icon-bg', asset('icon-bg.png'), ''));
iconWrap.appendChild(makeImg('daily-popup-reward', asset('reward-0' + day + '.png'), 'Reward Day ' + day));
iconWrap.appendChild(makeImg('daily-popup-icon-frame', asset(getIconFrame(status)), ''));
if (status === 'locked') {
iconWrap.appendChild(makeImg('daily-popup-icon-lock-overlay', asset('icon-frame-lock.png'), ''));
}
if (status === 'received') {
var redeemedClassName = 'daily-popup-redeemed';
if (state.claimedAnimDay === day) redeemedClassName += ' is-claim-animate';
var redeemedImg = makeImg(redeemedClassName, asset('reward-redeemed.png'), 'Received');
if (state.claimedAnimDay === day) {
redeemedImg.addEventListener('animationend', function () {
redeemedImg.classList.remove('is-claim-animate');
}, { once: true });
}
iconWrap.appendChild(redeemedImg);
}
inner.appendChild(iconWrap);
if (status === 'claim') {
var claimBtn = document.createElement('button');
claimBtn.type = 'button';
claimBtn.className = 'daily-popup-claim-btn';
claimBtn.appendChild(makeImg('daily-popup-claim-btn-img', asset('btn-claim-reward.png'), 'Claim'));
claimBtn.addEventListener('click', function () {
if (!state.progress) return;
if (day !== state.currentDay) return;
if (state.progress.claimedDays[day - 1]) return;
state.progress.claimedDays[day - 1] = true;
state.progress.lockUntilMs = Date.now() + Math.max(1, state.msUntilReset || activeResetMs);
saveProgress();
syncProgress();
state.claimedAnimDay = day;
renderCards();
state.claimedAnimDay = null;
syncDailyDotIndicator();
});
inner.appendChild(claimBtn);
} else {
var statusFile = status === 'received' ? 'txt-recieved.png' : 'txt-locked.png';
inner.appendChild(makeImg('daily-popup-status-img', asset(statusFile), status === 'received' ? 'Received' : 'Locked'));
}
return li;
}
function renderCards() {
if (!refs.cardsEl) return;
refs.cardsEl.innerHTML = '';
for (var day = 1; day <= TOTAL_DAYS; day += 1) refs.cardsEl.appendChild(createCard(day));
syncDailyDotIndicator();
}
function getMsUntilNextMidnight(nowMs) {
var now = nowMs ? new Date(nowMs) : new Date();
var nextMidnight = new Date(now);
nextMidnight.setHours(24, 0, 0, 0);
return Math.max(0, nextMidnight.getTime() - now.getTime());
}
function renderResetCountdown() {
if (!refs.resetEl) return;
var changed = syncProgress();
if (changed) renderCards();
var remainMs = state.msUntilReset || (isFastTestMode ? testResetMs : getMsUntilNextMidnight(Date.now()));
var totalSeconds = Math.floor(remainMs / 1000);
var hours = Math.floor(totalSeconds / 3600);
var minutes = Math.floor((totalSeconds % 3600) / 60);
var seconds = totalSeconds % 60;
refs.resetEl.textContent = 'รีเซ็ตในอีก ' + pad2(hours) + ':' + pad2(minutes) + ':' + pad2(seconds);
syncDailyDotIndicator();
}
function startResetCountdown() {
renderResetCountdown();
if (resetTimerId != null) return;
resetTimerId = window.setInterval(renderResetCountdown, 1000);
}
function stopResetCountdown() {
if (resetTimerId == null) return;
window.clearInterval(resetTimerId);
resetTimerId = null;
}
function buildPopup() {
var overlay = document.createElement('div');
overlay.id = 'daily-popup-overlay';
overlay.className = 'daily-popup-overlay is-hidden';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'รางวัลประจำวัน');
var backdrop = document.createElement('button');
backdrop.type = 'button';
backdrop.className = 'daily-popup-backdrop';
backdrop.setAttribute('aria-label', 'ปิดหน้าต่างรางวัลประจำวัน');
overlay.appendChild(backdrop);
var dialog = document.createElement('div');
dialog.className = 'daily-popup-dialog';
dialog.appendChild(makeImg('daily-popup-bg', asset('daily-bg.png'), ''));
appendPreloadContainer(dialog);
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'daily-popup-close';
closeBtn.setAttribute('aria-label', 'ปิด');
dialog.appendChild(closeBtn);
var content = document.createElement('div');
content.className = 'daily-popup-content';
var reset = document.createElement('p');
reset.className = 'daily-popup-reset';
reset.textContent = 'รีเซ็ตในอีก 00:00:00';
content.appendChild(reset);
var list = document.createElement('ol');
list.className = 'daily-popup-grid';
content.appendChild(list);
dialog.appendChild(content);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
refs.overlay = overlay;
refs.dialog = dialog;
refs.cardsEl = list;
refs.resetEl = reset;
renderCards();
backdrop.addEventListener('click', closePopup);
closeBtn.addEventListener('click', closePopup);
overlay.addEventListener('click', function (event) {
if (event.target === overlay) closePopup();
});
}
function syncScale() {
if (!refs.dialog) return;
var width = refs.dialog.getBoundingClientRect().width;
if (!width) return;
refs.dialog.style.setProperty('--dr-scale', String(width / 1181));
}
function openPopup() {
if (!refs.overlay) buildPopup();
syncProgress();
renderCards();
refs.overlay.classList.remove('is-hidden');
state.isOpen = true;
syncScale();
startResetCountdown();
}
function closePopup() {
if (!refs.overlay) return;
refs.overlay.classList.add('is-hidden');
state.isOpen = false;
stopResetCountdown();
}
function bindTriggers() {
var triggers = Array.prototype.slice.call(document.querySelectorAll(triggerSelector));
triggers.forEach(function (btn) {
btn.addEventListener('click', function (event) {
event.preventDefault();
openPopup();
});
});
}
function bindGlobalEvents() {
window.addEventListener('resize', syncScale);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && state.isOpen) closePopup();
});
}
window.openDailyPopup = openPopup;
window.closeDailyPopup = closePopup;
state.progress = loadProgress();
syncProgress();
startDailyDotWatcher();
bindTriggers();
bindGlobalEvents();
})();
+12 -33
View File
@@ -7,7 +7,10 @@
<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.0175">
<link rel="stylesheet" href="style.css?v=0.0189">
<link rel="stylesheet" href="daily-popup.css?v=8">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=39">
<link rel="stylesheet" href="../Game/css/profile-popup.css?v=5">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
@@ -146,8 +149,9 @@
<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 class="lobby-leaderboard-scroll-track" id="lobby-leaderboard-scroll-track" aria-hidden="true"></div>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</aside>
@@ -219,38 +223,13 @@
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<img id="lobby-customize-tabbar" class="lobby-customize-tabbar-img" src="../Game/img/03-5-Customize/tab-1-face.png" alt="" decoding="async">
<button type="button" class="lobby-customize-tabhit" data-tab="face" role="tab" aria-selected="true" aria-label="ใบหน้า"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="hair" role="tab" aria-selected="false" aria-label="ทรงผม"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="cloth" role="tab" aria-selected="false" aria-label="ชุด"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0170"></script>
<script src="../app-base.js?v=2"></script>
<script src="../Game/js/display-name.js?v=2"></script>
<script src="daily-popup.js?v=25" data-daily-trigger="#btn-daily" data-daily-asset-base="IMAGE/Daily" data-daily-test-reset-seconds="0"></script>
<script src="../Game/js/customize-popup.js?v=26" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
<script src="../Game/js/profile-popup.js?v=6" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
<script src="lobby.js?v=0.0190"></script>
</body>
</html>
+178 -255
View File
@@ -84,7 +84,6 @@
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;
@@ -210,7 +209,6 @@
}
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 () {
@@ -219,6 +217,7 @@
}
var PLAYER_KEY = 'jdPlayerKey';
var DISPLAY_NAME_STORAGE_KEY = 'roomLobbyDisplayName';
function ensurePlayerKey() {
var k;
@@ -258,11 +257,10 @@
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
lobbyBakedCharUrl = urlOrNull;
applyLobbyCharacterAppearance();
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
lobbyResolvedCharId = id || '';
if (!id) {
applyCharAssets(null);
return;
@@ -276,8 +274,21 @@
});
}
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 = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var name = getProfileDisplayName();
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
@@ -368,6 +379,7 @@
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);
@@ -376,11 +388,13 @@
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 = scrollEl.clientHeight;
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);
@@ -443,6 +457,140 @@
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');
@@ -616,14 +764,26 @@
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
openLobbyCustomize();
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/';
});
@@ -778,6 +938,9 @@
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
scheduleLeaderboardPlacement();
initGuide();
@@ -789,6 +952,9 @@
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
@@ -799,6 +965,9 @@
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
bindLeaderboardWheelSupport();
bindLeaderboardDragScroll();
bindLeaderboardThumbDrag();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
@@ -809,7 +978,7 @@
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName' || e.key === DISPLAY_NAME_STORAGE_KEY) {
syncMainLobbyCharacterUi();
return;
}
@@ -817,250 +986,4 @@
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();
})();
+57 -262
View File
@@ -7,7 +7,13 @@ html {
-webkit-text-size-adjust: 100%;
}
/* Noto local file removed — use Google Kanit in index.html (avoids 404 on /Create%20Room/FONTS/...) */
@font-face {
font-family: 'NotoSansThai';
src: url('../FONTS/NotoSansThai-ExtraBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
:root {
--lobby-character-height: 62cqh; /* ความสูงตัวละครแบบพิกเซลคงที่ */
@@ -36,7 +42,7 @@ body.lobby-page {
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
font-family: 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
color: #fff;
background: #060818;
-webkit-tap-highlight-color: transparent;
@@ -51,7 +57,7 @@ body.lobby-page select {
body.lobby-page,
body.lobby-page * {
font-family: 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
font-family: 'NotoSansThai', 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
}
/* ปุ่มที่กดง่ายบนมือถือ */
@@ -406,13 +412,14 @@ body.lobby-page * {
--lobby-daily-leaderboard-gap: 0cqh; /* ระยะห่างจากปุ่มรางวัลประจำวัน (ติดลบได้) */
--lobby-leaderboard-row-height: 7cqh; /* ปรับความสูงแต่ละรายชื่อ */
--lobby-leaderboard-rank-icon-size: 2.3em; /* ปรับขนาดไอคอนอันดับ 1-3 */
--lobby-leaderboard-scrollbar-width: 1cqh; /* ความอ้วน scrollbar */
--lobby-leaderboard-scrollbar-thumb-inset: 1cqh;
--lobby-leaderboard-scrollbar-offset-x: 1cqh; /* + = ขยับ scrollbar ไปขวา */
--lobby-leaderboard-scrollbar-width: max(8px, 1.1cqh); /* ความอ้วน scrollbar */
--lobby-leaderboard-scrollbar-safe-gap: 10px; /* เว้นช่องกันทับตัวหนังสือ */
--lobby-leaderboard-scrollbar-thumb-inset: 1px;
--lobby-leaderboard-scrollbar-offset-x: 0px; /* ตำแหน่งแถบ scrollbar */
--lobby-leaderboard-font-size: 2.1cqh;
--lobby-leaderboard-content-inset-top: 37%;
--lobby-leaderboard-content-inset-right: 4%;
--lobby-leaderboard-content-inset-bottom: 4%;
--lobby-leaderboard-content-inset-right: 8.5%;
--lobby-leaderboard-content-inset-bottom: 8%;
--lobby-leaderboard-content-inset-left: 6%;
position: absolute;
top: 19em;
@@ -445,12 +452,6 @@ body.lobby-page * {
z-index: 1;
overflow: hidden;
border-radius: 10px;
-webkit-mask-image: url('IMAGE/leaderboard-bg.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-image: url('IMAGE/leaderboard-bg.png');
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
.lobby-leaderboard-inner {
@@ -476,41 +477,66 @@ body.lobby-page * {
margin-right: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: calc(var(--lobby-leaderboard-scrollbar-width) + var(--lobby-leaderboard-scrollbar-thumb-inset) + 2px);
padding-right: calc(var(--lobby-leaderboard-scrollbar-width) + var(--lobby-leaderboard-scrollbar-safe-gap));
scrollbar-width: none;
cursor: grab;
}
.lobby-leaderboard-scroll::after {
content: '';
.lobby-leaderboard-scroll.is-dragging {
cursor: grabbing;
}
body.lobby-page.lobby-leaderboard-dragging,
body.lobby-page.lobby-leaderboard-dragging * {
user-select: none !important;
}
.lobby-leaderboard-scroll-track {
position: absolute;
top: 0;
right: calc(var(--lobby-leaderboard-scrollbar-offset-x) * -1);
right: 0;
width: var(--lobby-leaderboard-scrollbar-width);
height: 100%;
pointer-events: none;
opacity: 0.95;
opacity: 1;
border-radius: 999px;
background:
linear-gradient(180deg, rgba(127, 225, 255, 0.25) 0%, rgba(54, 126, 188, 0.2) 100%),
linear-gradient(180deg, rgba(20, 37, 72, 0.85) 0%, rgba(19, 34, 63, 0.78) 100%);
box-shadow:
inset 0 0 0 1px rgba(115, 220, 255, 0.26),
0 0 0 1px rgba(43, 88, 149, 0.35);
background: #1b2740;
border: 1px solid #22324f;
z-index: 1;
}
.lobby-leaderboard-scroll-thumb {
position: absolute;
right: calc((var(--lobby-leaderboard-scrollbar-offset-x) * -1) + var(--lobby-leaderboard-scrollbar-thumb-inset));
right: var(--lobby-leaderboard-scrollbar-thumb-inset);
top: var(--lobby-leaderboard-scrollbar-thumb-inset);
width: calc(var(--lobby-leaderboard-scrollbar-width) - (var(--lobby-leaderboard-scrollbar-thumb-inset) * 2));
min-height: 18px;
border-radius: 999px;
pointer-events: none;
background:
linear-gradient(180deg, #ff9adf 0%, #ff4fb5 42%, #e02b92 100%);
pointer-events: auto;
z-index: 2;
background: #f04cdc;
box-shadow:
inset 0 0 0 1px rgba(255, 210, 240, 0.52),
0 0 8px rgba(255, 83, 181, 0.45);
display: block;
cursor: grab;
}
.lobby-leaderboard-scroll-thumb.is-dragging {
cursor: grabbing;
}
.lobby-leaderboard-scroll::-webkit-scrollbar {
width: 0;
height: 0;
}
.lobby-leaderboard-scroll::-webkit-scrollbar-track {
background: transparent;
border: 0;
}
.lobby-leaderboard-scroll::-webkit-scrollbar-thumb {
background: transparent;
border: 0;
}
.lobby-leaderboard-list {
@@ -520,6 +546,7 @@ body.lobby-page * {
padding: 0;
width: 100%;
max-width: 100%;
padding-right: 2px;
min-height: 0;
overflow: visible;
display: flex;
@@ -1852,6 +1879,7 @@ body.lobby-guide-active:not(.lobby-guide-top-step) .lobby-footer {
max-width: 100% !important;
aspect-ratio: 517 / 213;
transform: translateY(var(--lobby-news-wrap-shift-y));
margin-right: -1%;
}
.lobby-news-bg {
@@ -1944,236 +1972,3 @@ body.lobby-guide-active:not(.lobby-guide-top-step) .lobby-footer {
width: var(--lobby-profile-coin-size) !important;
height: var(--lobby-profile-coin-size) !important;
}
/* ---- News panel: ซ่อนกล่องขาว (news-banner-bg.png) ใช้พื้นมืดเข้าธีมแทน ---- */
.lobby-news-bg {
display: none !important;
}
.lobby-news-wrap {
background: linear-gradient(180deg, rgba(12, 18, 40, 0.92), rgba(10, 16, 34, 0.92));
border: 2px solid rgba(34, 211, 238, 0.65);
border-radius: 12px;
box-shadow:
0 0 14px rgba(34, 211, 238, 0.35),
inset 0 0 18px rgba(34, 211, 238, 0.12);
}
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1 ===== */
.lobby-customize-overlay {
position: fixed;
inset: 0;
z-index: 150;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.lobby-customize-overlay.hidden { display: none !important; }
.lobby-customize-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 6, 18, 0.78);
backdrop-filter: blur(4px);
}
.lobby-customize-dialog {
position: relative;
width: min(92vw, 720px);
max-height: 90vh;
overflow: hidden auto;
display: flex;
flex-direction: column;
gap: clamp(0.5rem, 1.6vh, 1rem);
padding: clamp(1rem, 3vw, 1.6rem) clamp(1rem, 3vw, 1.8rem) clamp(1.2rem, 3vw, 1.8rem);
border-radius: 18px;
background: linear-gradient(180deg, rgba(14, 20, 44, 0.97), rgba(9, 13, 30, 0.97));
border: 2px solid rgba(34, 211, 238, 0.7);
box-shadow:
0 0 22px rgba(34, 211, 238, 0.45),
0 0 48px rgba(236, 72, 153, 0.28),
inset 0 0 24px rgba(34, 211, 238, 0.1);
}
.lobby-customize-titlebar {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.lobby-customize-title {
margin: 0;
font-size: clamp(1.1rem, 3vw, 1.6rem);
font-weight: 700;
color: #e8faff;
text-shadow: 0 0 12px rgba(34, 211, 238, 0.6);
}
.lobby-customize-close {
position: absolute;
right: 0;
top: 0;
width: 38px;
height: 38px;
border: 2px solid rgba(236, 72, 153, 0.6);
border-radius: 10px;
background: rgba(20, 16, 40, 0.6);
color: #ff8fd0;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
}
.lobby-customize-close:hover { background: rgba(236, 72, 153, 0.25); }
.lobby-customize-row {
display: flex;
align-items: center;
gap: clamp(0.5rem, 2vw, 1rem);
flex-wrap: wrap;
}
.lobby-customize-row-label {
height: clamp(20px, 3.4vh, 30px);
width: auto;
flex: 0 0 auto;
}
.lobby-customize-swatches {
display: flex;
flex-wrap: wrap;
gap: clamp(6px, 1vw, 10px);
}
.lobby-customize-swatch {
padding: 0;
border: 2px solid transparent;
border-radius: 8px;
background: none;
cursor: pointer;
line-height: 0;
}
.lobby-customize-swatch img {
display: block;
width: clamp(28px, 4vw, 40px);
height: auto;
border-radius: 6px;
}
.lobby-customize-swatch.is-selected {
border-color: #22d3ee;
box-shadow: 0 0 10px rgba(34, 211, 238, 0.7);
}
.lobby-customize-tabs {
position: relative;
width: 100%;
max-width: 560px;
margin: 0 auto;
aspect-ratio: 776 / 118;
}
.lobby-customize-tabbar-img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.lobby-customize-tabhit {
position: absolute;
top: 0;
height: 100%;
width: 33.34%;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.lobby-customize-tabhit[data-tab="face"] { left: 0; }
.lobby-customize-tabhit[data-tab="hair"] { left: 33.33%; }
.lobby-customize-tabhit[data-tab="cloth"] { left: 66.66%; }
.lobby-customize-items {
flex: 1;
min-height: clamp(140px, 30vh, 280px);
max-height: 42vh;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: clamp(8px, 1.5vw, 14px);
padding: clamp(8px, 1.5vw, 14px);
border-radius: 12px;
background: rgba(8, 12, 28, 0.6);
border: 1px solid rgba(34, 211, 238, 0.25);
}
.lobby-customize-item {
position: relative;
padding: 6px;
border: 2px solid rgba(34, 211, 238, 0.3);
border-radius: 12px;
background: rgba(18, 26, 52, 0.7);
cursor: pointer;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
}
.lobby-customize-item.is-selected {
border-color: #22d3ee;
box-shadow: 0 0 10px rgba(34, 211, 238, 0.6);
}
.lobby-customize-item-img {
width: 78%;
height: auto;
object-fit: contain;
}
.lobby-customize-item-price {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
font-weight: 700;
color: #ffe066;
background: rgba(0, 0, 0, 0.45);
border-radius: 8px;
padding: 1px 8px;
}
.lobby-customize-empty {
grid-column: 1 / -1;
align-self: center;
text-align: center;
color: rgba(255, 255, 255, 0.6);
padding: 2rem 1rem;
font-size: 0.95rem;
}
.lobby-customize-confirm {
align-self: center;
padding: 0;
border: none;
background: none;
cursor: pointer;
}
.lobby-customize-confirm img {
display: block;
width: min(60vw, 240px);
height: auto;
}
@media (max-width: 560px) {
.lobby-customize-items { grid-template-columns: repeat(3, 1fr); }
}
+3 -3
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?v=1.2">
<link rel="stylesheet" href="style.css?v=1.3">
</head>
<body>
<!-- พื้นหลังจาก Login -->
@@ -43,7 +43,7 @@
</div>
</div>
<script src="../app-base.js"></script>
<script src="menu.js"></script>
<script src="../app-base.js?v=2"></script>
<script src="menu.js?v=1.1"></script>
</body>
</html>
+15
View File
@@ -17,6 +17,15 @@
}
// ปุ่มต่างๆ
function syncMainMenuScale() {
var ui = document.querySelector('.menu-ui');
if (!ui) return;
var vw = Math.max(1, window.innerWidth || 0);
var vh = Math.max(1, window.innerHeight || 0);
var scale = Math.min(1, vw / 1920, vh / 1080);
ui.style.setProperty('--mm-scale', String(scale.toFixed(4)));
}
document.getElementById('btn-back')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/';
});
@@ -36,4 +45,10 @@
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
}
});
syncMainMenuScale();
window.addEventListener('resize', syncMainMenuScale);
window.addEventListener('orientationchange', function () {
setTimeout(syncMainMenuScale, 150);
});
})();
+659 -1136
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -7,24 +7,24 @@
<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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0204">
<link rel="stylesheet" href="style.css?v=0.0221">
</head>
<body>
<div class="qb-bg" aria-hidden="true">
<img src="/Quiz-Battle/IMAGE/Quiz-Battle-bg.png" alt="" class="qb-bg-img" decoding="async">
<img src="IMAGE/Quiz-Battle-bg.png" alt="" class="qb-bg-img" decoding="async">
</div>
<div class="qb-vignette" aria-hidden="true"></div>
<div class="qb-ui">
<header class="qb-top">
<button type="button" class="qb-icon-btn qb-icon-btn--back" id="btn-back" aria-label="กลับล็อบบี้">
<img src="/Main-Menu/IMAGE/btn-back.png" alt="" decoding="async">
<button type="button" class="qb-join-back" id="btn-back" aria-label="กลับล็อบบี้">
<img src="IMAGE/btn-exit-room.png" alt="" class="qb-join-back-img" decoding="async">
</button>
<div class="qb-top-fill" aria-hidden="true"></div>
<div class="qb-profile-panel">
<div class="qb-avatar-frame">
<div class="qb-avatar-wrap">
<img src="/Main-Menu/char-main.png" alt="" class="lobby-avatar-mask qb-avatar-img" id="lobby-profile-avatar" width="64" height="64" decoding="async">
<div class="qb-room-profile-wrap" aria-hidden="true">
<div class="qb-room-profile-frame">
<div class="qb-room-profile-avatar-viewport">
<img src="../Main-Menu/char-main.png" alt="" class="qb-room-profile-avatar" id="lobby-profile-avatar" width="64" height="64" decoding="async">
</div>
</div>
</div>
@@ -32,19 +32,19 @@
<main class="qb-main">
<div class="qb-room-banner" aria-hidden="true">
<img src="/Quiz-Battle/IMAGE/quiz-room.png" alt="" decoding="async">
<img src="IMAGE/quiz-room.png" alt="" decoding="async">
</div>
<div class="qb-grid" id="qb-grid" role="list" aria-label="เลือกห้องควิซ"></div>
<p class="qb-toast" id="qb-toast" role="status"></p>
</main>
<footer class="qb-bottom">
<button type="button" class="qb-start-btn" id="btn-start-quiz" aria-label="เริ่มควิซ">
<img src="/Quiz-Battle/IMAGE/btn-start-quiz.png" alt="" decoding="async">
</button>
</footer>
</div>
<div class="room-lobby-br-fixed qb-room-lobby-br-fixed" id="qb-room-lobby-br-fixed">
<button type="button" id="btn-voice" class="btn-voice-icon" title="เปิด/ปิดเสียงพูด" aria-label="เปิด/ปิดเสียงพูด">
<img src="../Game/img/btn-mic-mute.png" alt="เปิดเสียง" id="btn-voice-icon-img" decoding="async">
</button>
</div>
<script src="quiz-battle.js?v=0.0204"></script>
<script src="../app-base.js?v=2"></script>
<script src="quiz-battle.js?v=0.0220"></script>
</body>
</html>
+121 -35
View File
@@ -1,18 +1,19 @@
(function () {
'use strict';
var QB_IMAGE = '/Quiz-Battle/IMAGE';
var BASE = '/Game';
var QB_IMAGE = typeof appPath === 'function' ? appPath('/Quiz-Battle/IMAGE') : '/Quiz-Battle/IMAGE';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var CHAR_KEY = 'gameCharacterId';
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
var VOICE_STATE_KEY = 'qbVoiceMicOn';
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = '/Login/';
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return;
}
var grid = document.getElementById('qb-grid');
var toast = document.getElementById('qb-toast');
var selectedRoom = 1;
function pad2(n) {
return n < 10 ? '0' + n : String(n);
@@ -41,13 +42,28 @@
function applyProfileAvatar() {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fallbackAvatar = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
var cid = getSelectedCharacterId();
if (!cid) {
av.onerror = null;
av.src = '/Main-Menu/char-main.png';
av.src = fallbackAvatar;
return;
}
// ใช้รูปเดียวกับหน้า Main-Lobby ก่อน (composited idle ที่บันทึกไว้)
try {
var savedLobbyAvatar = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + cid) || '';
if (savedLobbyAvatar && savedLobbyAvatar.indexOf('data:image/') === 0) {
av.onerror = function () {
av.onerror = null;
av.src = fallbackAvatar;
};
av.src = savedLobbyAvatar;
return;
}
} catch (e0) { /* ignore */ }
var urls = characterDownUrls(cid);
var avStep = 0;
av.onerror = function () {
@@ -55,7 +71,7 @@
if (avStep === 1) av.src = urls[1];
else {
av.onerror = null;
av.src = '/Main-Menu/char-main.png';
av.src = fallbackAvatar;
}
};
av.src = urls[0];
@@ -65,56 +81,126 @@
try {
localStorage.setItem('quizBattleRoom', String(n));
} catch (e) { /* ignore */ }
setToast('เริ่มควิซห้อง ' + n + ' (ต่อระบบจริงในเวอร์ชันถัดไป)');
setToast('เริ่มเกมห้อง ' + n + ' (ต่อระบบจริงในเวอร์ชันถัดไป)');
}
function selectRoom(n) {
selectedRoom = n;
grid?.querySelectorAll('.qb-card').forEach(function (btn) {
var r = parseInt(btn.getAttribute('data-room'), 10);
btn.classList.toggle('qb-card--selected', r === n);
function getVoiceIconPath(micOn) {
var imgPath = micOn ? '/Game/img/btn-mic-on.png' : '/Game/img/btn-mic-mute.png';
return typeof appPath === 'function' ? appPath(imgPath) : imgPath;
}
function setVoiceButtonIcon(btn, micOn) {
if (!btn) return;
var img = btn.querySelector('#btn-voice-icon-img') || btn.querySelector('img');
if (img) {
img.src = getVoiceIconPath(micOn);
img.alt = micOn ? 'ปิดเสียง' : 'เปิดเสียง';
}
btn.title = micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด';
btn.setAttribute('aria-label', micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด');
}
function bindVoiceButton() {
var btnVoice = document.getElementById('btn-voice');
if (!btnVoice) return;
var micOn = false;
try { micOn = localStorage.getItem(VOICE_STATE_KEY) === '1'; } catch (e) { /* ignore */ }
setVoiceButtonIcon(btnVoice, micOn);
btnVoice.addEventListener('click', function () {
micOn = !micOn;
try { localStorage.setItem(VOICE_STATE_KEY, micOn ? '1' : '0'); } catch (e2) { /* ignore */ }
setVoiceButtonIcon(btnVoice, micOn);
});
setToast('ห้อง ' + n + ' — กด Start ด้านล่างเพื่อเริ่ม');
}
function buildGrid() {
if (!grid) return;
grid.innerHTML = '';
for (var i = 1; i <= 10; i++) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'qb-card' + (i === 1 ? ' qb-card--selected' : '');
b.setAttribute('data-room', String(i));
b.setAttribute('role', 'listitem');
b.setAttribute('aria-label', 'ห้อง ' + i + ' — แตะเพื่อเลือก');
b.addEventListener('click', function () {
selectRoom(parseInt(this.getAttribute('data-room'), 10));
});
var card = document.createElement('div');
card.className = 'qb-card';
card.setAttribute('data-room', String(i));
card.setAttribute('role', 'listitem');
card.setAttribute('aria-label', 'ห้อง ' + i);
var roomImg = document.createElement('img');
roomImg.className = 'qb-card-room';
roomImg.src = QB_IMAGE + '/Room-' + pad2(i) + '.png';
roomImg.alt = '';
roomImg.decoding = 'async';
var tagImg = document.createElement('img');
tagImg.className = 'qb-card-tag';
tagImg.src = QB_IMAGE + '/tag-' + pad2(i) + '.png';
tagImg.alt = '';
tagImg.decoding = 'async';
b.appendChild(roomImg);
b.appendChild(tagImg);
grid.appendChild(b);
var startBtn = document.createElement('button');
startBtn.type = 'button';
startBtn.className = 'qb-card-start';
startBtn.setAttribute('aria-label', 'เริ่มเกมห้อง ' + i);
startBtn.addEventListener('click', (function (roomNo) {
return function (ev) {
ev.preventDefault();
ev.stopPropagation();
startQuizForRoom(roomNo);
};
})(i));
var startImg = document.createElement('img');
startImg.src = QB_IMAGE + '/btn-start-quiz.png';
startImg.alt = 'เริ่มเกม';
startImg.decoding = 'async';
startBtn.appendChild(startImg);
var countEl = document.createElement('span');
countEl.className = 'qb-card-count';
countEl.textContent = '0/50';
card.appendChild(roomImg);
card.appendChild(startBtn);
card.appendChild(countEl);
grid.appendChild(card);
}
setToast('เลือกห้อง แล้วกด Start ด้านล่าง');
setToast('กดปุ่มเริ่มเกมในแต่ละห้องเพื่อเล่น');
}
document.getElementById('btn-start-quiz')?.addEventListener('click', function () {
startQuizForRoom(selectedRoom);
});
function syncMainScale() {
var mainEl = document.querySelector('.qb-main');
if (!mainEl) return;
var availableW = Math.max(1, mainEl.clientWidth);
var availableH = Math.max(1, mainEl.clientHeight);
// Base layout from image dimensions:
// banner 419x151, banner top offset, card 271x351, 5 columns, 2 rows, gaps + toast spacing.
var baseWidth = (271 * 5) + (18 * 4);
var baseHeight = 60 + 151 + 12 + (351 * 2) + 14 + 8 + 24;
var scaleW = availableW / baseWidth;
var scaleH = availableH / baseHeight;
var scale = Math.max(0.22, Math.min(1, Math.min(scaleW, scaleH)));
mainEl.style.setProperty('--qb-main-scale', scale.toFixed(4));
}
function syncUiScale() {
var uiEl = document.querySelector('.qb-ui');
if (!uiEl) return;
var vw = Math.max(1, window.innerWidth || 0);
var vh = Math.max(1, window.innerHeight || 0);
// Base stage from mock-normal viewport.
var scale = Math.max(0.34, Math.min(1, Math.min(vw / 1920, vh / 1080)));
uiEl.style.setProperty('--qb-ui-scale', scale.toFixed(4));
}
document.getElementById('btn-back')?.addEventListener('click', function () {
window.location.href = '/Main-Lobby/';
window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/';
});
applyProfileAvatar();
buildGrid();
bindVoiceButton();
syncUiScale();
syncMainScale();
window.addEventListener('resize', function () {
syncUiScale();
syncMainScale();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
syncUiScale();
syncMainScale();
}, 120);
});
})();
+386 -315
View File
@@ -1,315 +1,386 @@
* { box-sizing: border-box; }
html {
min-height: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
font-family: 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
color: #fff;
background: #030712;
}
.qb-bg {
position: fixed;
inset: 0;
z-index: 0;
}
.qb-bg-img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.qb-vignette {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
background: radial-gradient(ellipse at 50% 25%, transparent 15%, rgba(0, 0, 0, 0.4) 100%);
}
.qb-ui {
position: relative;
z-index: 2;
min-height: 100vh;
min-height: 100dvh;
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));
display: flex;
flex-direction: column;
}
.qb-top {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin-bottom: 12px;
}
.qb-top-fill {
flex: 1;
min-width: 8px;
pointer-events: none;
}
.qb-icon-btn {
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.qb-icon-btn img {
display: block;
height: clamp(38px, 10vw, 52px);
width: auto;
image-rendering: -webkit-optimize-contrast;
}
/* ย้อนกลับ — สเกลตามจอใหญ่ (ใกล้เคียง Main-Menu .menu-btn-back-img) */
.qb-icon-btn--back img {
width: clamp(48px, 6.5vw, 76px);
height: auto;
max-height: clamp(56px, 8.5vw, 92px);
object-fit: contain;
}
.qb-profile-panel {
flex-shrink: 0;
}
.qb-avatar-frame {
width: clamp(60px, 14vw, 92px);
height: clamp(60px, 14vw, 92px);
padding: 3px;
box-sizing: border-box;
border: 2px solid rgba(34, 211, 238, 0.95);
border-radius: 4px;
background: rgba(3, 7, 18, 0.65);
box-shadow:
0 0 16px rgba(34, 211, 238, 0.55),
0 0 28px rgba(168, 85, 247, 0.22),
inset 0 0 12px rgba(34, 211, 238, 0.12);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.qb-avatar-wrap {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 2px;
}
.qb-avatar-img.lobby-avatar-mask {
width: 151%;
height: 212%;
/* object-fit / object-position: match Main-Lobby .lobby-avatar-mask */
transform: translate(0, 16px);
display: block;
}
.qb-main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(34, 211, 238, 0.45) rgba(0, 20, 40, 0.5);
}
.qb-main::-webkit-scrollbar {
width: 8px;
}
.qb-main::-webkit-scrollbar-track {
background: url(/Quiz-Battle/IMAGE/q-scroll-bg.png) center / 100% 100% no-repeat;
border-radius: 4px;
}
.qb-main::-webkit-scrollbar-thumb {
background: url(/Quiz-Battle/IMAGE/q-scroll-bar.png) center / 100% 100% no-repeat;
border-radius: 4px;
}
.qb-room-banner {
width: 100%;
max-width: min(94vw, 640px);
margin: 0 auto clamp(10px, 1.5vh, 18px);
flex-shrink: 0;
}
@media (min-width: 1200px) {
.qb-room-banner {
max-width: min(92vw, 760px);
}
}
@media (min-width: 1600px) {
.qb-room-banner {
max-width: min(88vw, 880px);
}
}
.qb-room-banner img {
display: block;
width: 100%;
height: auto;
object-fit: contain;
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.35));
}
.qb-grid {
width: 100%;
max-width: min(96vw, 1040px);
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: clamp(8px, 1.2vw, 18px);
padding: clamp(8px, 1.2vw, 14px) clamp(6px, 1vw, 12px) clamp(14px, 2vh, 24px);
}
@media (min-width: 1200px) {
.qb-grid {
max-width: min(94vw, 1180px);
gap: clamp(10px, 1vw, 20px);
}
}
@media (min-width: 1600px) {
.qb-grid {
max-width: min(92vw, 1320px);
gap: clamp(12px, 0.9vw, 22px);
}
}
.qb-card {
position: relative;
padding: 0;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
transition: box-shadow 0.2s, transform 0.15s;
}
.qb-card:hover,
.qb-card:focus-visible {
transform: translateY(-2px);
}
.qb-card:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 2px;
}
.qb-card--selected {
box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.9), 0 0 18px rgba(34, 211, 238, 0.45);
}
.qb-card-room {
display: block;
width: 100%;
height: auto;
vertical-align: middle;
}
.qb-card-tag {
position: absolute;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: auto;
pointer-events: none;
}
.qb-toast {
text-align: center;
font-size: clamp(0.88rem, 1.5vw, 1.05rem);
color: rgba(248, 250, 252, 0.88);
min-height: 1.2em;
margin-top: 4px;
flex-shrink: 0;
padding: 0 8px 4px;
}
.qb-bottom {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 10px 12px max(12px, env(safe-area-inset-bottom));
z-index: 4;
}
.qb-start-btn {
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
filter: drop-shadow(0 4px 20px rgba(34, 211, 238, 0.35));
transition: transform 0.15s, filter 0.2s;
}
.qb-start-btn:hover,
.qb-start-btn:focus-visible {
transform: scale(1.04);
filter: drop-shadow(0 6px 28px rgba(168, 85, 247, 0.45));
}
.qb-start-btn:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 4px;
}
.qb-start-btn:active {
transform: scale(0.98);
}
.qb-start-btn img {
display: block;
width: auto;
height: clamp(54px, 9vw, 96px);
max-height: min(22vh, 120px);
max-width: min(94vw, 440px);
object-fit: contain;
}
@media (min-width: 1200px) {
.qb-start-btn img {
height: clamp(64px, 7.5vw, 104px);
max-height: 128px;
max-width: min(90vw, 520px);
}
}
@media (max-width: 720px) {
.qb-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 400px) {
.qb-grid {
grid-template-columns: repeat(2, 1fr);
}
}
* { box-sizing: border-box; }
@font-face {
font-family: 'NotoSansThai';
src: url('../FONTS/NotoSansThai-ExtraBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'NotoSansThaiMedium';
src: url('../FONTS/NotoSansThai-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
html {
min-height: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
font-family: 'Kanit', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
color: #fff;
background: #030712;
}
.qb-bg {
position: fixed;
inset: 0;
z-index: 0;
}
.qb-bg-img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.qb-vignette {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
background: radial-gradient(ellipse at 50% 25%, transparent 15%, rgba(0, 0, 0, 0.4) 100%);
}
.qb-ui {
--qb-ui-scale: 1;
position: relative;
z-index: 2;
min-height: 100vh;
min-height: 100dvh;
padding:
max(calc(10px * var(--qb-ui-scale)), env(safe-area-inset-top))
max(calc(12px * var(--qb-ui-scale)), env(safe-area-inset-right))
max(calc(14px * var(--qb-ui-scale)), env(safe-area-inset-bottom))
max(calc(12px * var(--qb-ui-scale)), env(safe-area-inset-left));
overflow: hidden;
}
.qb-top {
position: absolute;
top: max(calc(10px * var(--qb-ui-scale)), env(safe-area-inset-top));
left: max(calc(12px * var(--qb-ui-scale)), env(safe-area-inset-left));
right: max(calc(12px * var(--qb-ui-scale)), env(safe-area-inset-right));
display: flex;
align-items: flex-start;
justify-content: flex-end;
gap: calc(8px * var(--qb-ui-scale));
min-height: calc(157px * var(--qb-ui-scale));
z-index: 6;
pointer-events: none;
}
.qb-top-fill {
flex: 1;
min-width: calc(8px * var(--qb-ui-scale));
pointer-events: none;
}
/* กรอบแดง 1: ใช้ layout แบบ Join Room back button */
.qb-join-back {
position: fixed;
top: max(2px, env(safe-area-inset-top));
left: max(2px, env(safe-area-inset-left));
padding: 0;
border: none;
background: transparent;
cursor: pointer;
z-index: 20;
pointer-events: auto;
-webkit-tap-highlight-color: transparent;
}
.qb-join-back-img {
--qb-exit-btn-height: 142px;
display: block;
height: var(--qb-exit-btn-height);
width: calc(var(--qb-exit-btn-height) * (122 / 142));
object-fit: contain;
}
/* กรอบแดง 2: ใช้ layout แบบ room-lobby-profile-avatar-viewport */
.qb-room-profile-wrap {
flex-shrink: 0;
pointer-events: none;
}
.qb-room-profile-frame {
position: relative;
width: 159px;
height: 157px;
min-width: 159px;
min-height: 157px;
max-width: 159px;
max-height: 157px;
aspect-ratio: 159 / 157;
border-radius: 0;
border: none;
overflow: hidden;
background: url(../Game/img/btn-profile-bg.png) center/100% 100% no-repeat;
box-shadow: none;
}
.qb-room-profile-avatar-viewport {
position: absolute;
left: 18px;
top: 10px;
width: 124px;
height: 118px;
overflow: hidden;
border-radius: 12px;
}
.qb-room-profile-avatar {
position: absolute;
left: 50%;
top: 5px;
display: block;
width: 120%;
height: auto;
max-width: none;
object-fit: contain;
object-position: center top;
transform: translateX(-50%);
transform-origin: center center;
image-rendering: pixelated;
image-rendering: crisp-edges;
pointer-events: none;
}
.qb-main {
--qb-main-scale: 1;
--qb-banner-width: calc(419px * var(--qb-main-scale));
--qb-banner-height: calc(151px * var(--qb-main-scale));
--qb-card-width: calc(271px * var(--qb-main-scale));
--qb-card-height: calc(351px * var(--qb-main-scale));
--qb-card-start-height: calc(83px * var(--qb-main-scale));
--qb-card-start-width: calc(168px * var(--qb-main-scale));
--qb-grid-gap-x: calc(18px * var(--qb-main-scale));
--qb-grid-gap-y: calc(14px * var(--qb-main-scale));
--qb-main-gap-banner-grid: calc(12px * var(--qb-main-scale));
--qb-main-gap-grid-toast: calc(8px * var(--qb-main-scale));
position: relative;
z-index: 3;
width: 100%;
height: calc(100dvh - max(calc(10px * var(--qb-ui-scale)), env(safe-area-inset-top)) - max(calc(14px * var(--qb-ui-scale)), env(safe-area-inset-bottom)));
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 0;
overflow: hidden;
padding-top: calc(2px * var(--qb-main-scale));
}
.qb-main::-webkit-scrollbar {
width: 0;
height: 0;
}
.qb-main::-webkit-scrollbar-track {
background: transparent;
}
.qb-main::-webkit-scrollbar-thumb {
background: transparent;
}
.qb-room-banner {
flex: 0 0 auto;
width: var(--qb-banner-width);
height: var(--qb-banner-height);
max-width: 100%;
flex-shrink: 0;
margin: 0 auto var(--qb-main-gap-banner-grid);
margin-top: calc(60px * var(--qb-main-scale));
}
.qb-room-banner img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.35));
}
.qb-grid {
width: auto;
max-width: 100%;
display: grid;
grid-template-columns: repeat(5, var(--qb-card-width));
grid-auto-rows: var(--qb-card-height);
justify-content: center;
align-content: start;
column-gap: var(--qb-grid-gap-x);
row-gap: var(--qb-grid-gap-y);
padding: 0;
}
.qb-card {
position: relative;
width: var(--qb-card-width);
height: var(--qb-card-height);
padding: 0;
border: none;
background: transparent;
border-radius: 8px;
cursor: default;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
.qb-card-room {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
user-select: none;
}
.qb-card-start {
position: absolute;
left: 50%;
bottom: calc(25px * var(--qb-main-scale));
transform: translateX(-50%);
width: var(--qb-card-start-width);
height: var(--qb-card-start-height);
border: none;
padding: 0;
background: transparent;
cursor: pointer;
z-index: 2;
transition: transform 0.14s ease, filter 0.14s ease;
}
.qb-card-start:hover,
.qb-card-start:focus-visible {
transform: translateX(-50%) translateY(-1px);
filter: brightness(1.06);
}
.qb-card-start:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 3px;
border-radius: 8px;
}
.qb-card-start:active {
transform: translateX(-50%) translateY(0);
}
.qb-card-start img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.qb-card-count {
position: absolute;
top: calc(44px * var(--qb-main-scale));
right: calc(54px * var(--qb-main-scale));
font-family: 'NotoSansThaiMedium', 'NotoSansThai', 'Kanit', 'Sarabun', system-ui, sans-serif;
font-size: calc(22px * var(--qb-main-scale));
font-weight: 500;
line-height: 1;
color: #ffffff;
letter-spacing: 0;
text-shadow:
0 0 calc(10px * var(--qb-main-scale)) rgba(87, 239, 255, 0.55),
0 0 calc(2px * var(--qb-main-scale)) rgba(0, 0, 0, 0.65);
pointer-events: none;
user-select: none;
z-index: 3;
}
.qb-toast {
text-align: center;
font-size: calc(20px * var(--qb-main-scale));
color: rgba(248, 250, 252, 0.88);
line-height: 1.2;
min-height: calc(24px * var(--qb-main-scale));
margin-top: var(--qb-main-gap-grid-toast);
flex-shrink: 0;
padding: 0 calc(8px * var(--qb-main-scale)) calc(4px * var(--qb-main-scale));
}
/* มุมขวาล่าง — btn-voice (ซ่อนไว้ก่อน: ทับการ์ด Room 09–10 ในหน้าเลือกห้อง) */
.qb-room-lobby-br-fixed {
--lobby-br-btn: calc(121px * var(--qb-ui-scale, 1));
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
position: fixed;
bottom: max(calc(0.95rem * var(--qb-ui-scale, 1)), env(safe-area-inset-bottom));
right: max(calc(1.25rem * var(--qb-ui-scale, 1)), env(safe-area-inset-right));
z-index: 400;
flex-direction: row;
align-items: flex-end;
gap: calc(10px * var(--qb-ui-scale, 1));
pointer-events: none;
}
.qb-room-lobby-br-fixed .btn-voice-icon {
pointer-events: auto;
box-sizing: border-box;
width: var(--lobby-br-btn);
height: var(--lobby-br-btn);
min-width: var(--lobby-br-btn);
min-height: var(--lobby-br-btn);
max-width: var(--lobby-br-btn);
max-height: var(--lobby-br-btn);
margin: 0;
padding: 0;
border: none;
border-radius: 0;
appearance: none;
-webkit-appearance: none;
flex-shrink: 0;
cursor: pointer;
background: transparent;
box-shadow: none;
display: flex;
align-items: center;
justify-content: center;
}
.qb-room-lobby-br-fixed .btn-voice-icon img {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
object-position: center;
display: block;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
+25 -6
View File
@@ -6,14 +6,33 @@
'use strict';
if (typeof global.appPath === 'function') return;
/**
* @param {string} path - must start with / (e.g. '/Game/play.html')
* @returns {string}
*/
function sitePathPrefix() {
try {
var loc = global.location && global.location.pathname;
if (!loc) return '';
var m = loc.match(
/^(\/[^/]+)\/(?:Main-Lobby|Main-Menu|Login|Create(?:%20| )Room|Join(?:%20| )Room|Loading|Quiz-Battle|Game)(?:\/|$)/i
);
return m ? m[1] : '';
} catch (e) {
return '';
}
}
global.appPath = function appPath(path) {
if (path == null || path === '') return '';
var p = String(path);
if (p.charAt(0) !== '/') return '/' + p.replace(/^\/+/, '');
return p;
if (p.charAt(0) !== '/') p = '/' + p.replace(/^\/+/, '');
return sitePathPrefix() + p;
};
global.designLoginHref = function designLoginHref() { return appPath('/Login/'); };
global.designMainLobbyHref = function designMainLobbyHref() { return appPath('/Main-Lobby/'); };
global.designMainMenuHref = function designMainMenuHref() { return appPath('/Main-Menu/'); };
global.designCreateRoomHref = function designCreateRoomHref() { return appPath('/Create%20Room/'); };
global.designJoinRoomHref = function designJoinRoomHref() { return appPath('/Join%20Room/'); };
global.designLoadingHref = function designLoadingHref(query) {
var q = query ? String(query) : '';
return appPath('/Loading/') + (q ? (q.charAt(0) === '?' ? q : '?' + q) : '');
};
})(typeof window !== 'undefined' ? window : this);