diff --git a/www/html/Create Room/FIX-NOW.bat b/www/html/Create Room/FIX-NOW.bat new file mode 100644 index 0000000..fd30d8c --- /dev/null +++ b/www/html/Create Room/FIX-NOW.bat @@ -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 diff --git a/www/html/Create Room/FIX-NOW.ps1 b/www/html/Create Room/FIX-NOW.ps1 new file mode 100644 index 0000000..c999025 --- /dev/null +++ b/www/html/Create Room/FIX-NOW.ps1 @@ -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' diff --git a/www/html/Create Room/PASTE-INTO-index.html.txt b/www/html/Create Room/PASTE-INTO-index.html.txt new file mode 100644 index 0000000..c62a64f --- /dev/null +++ b/www/html/Create Room/PASTE-INTO-index.html.txt @@ -0,0 +1,13 @@ +Paste this block as the FIRST diff --git a/www/html/Create Room/create-room.js b/www/html/Create Room/create-room.js index 16cabf5..1814a7a 100644 --- a/www/html/Create Room/create-room.js +++ b/www/html/Create Room/create-room.js @@ -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; diff --git a/www/html/Create Room/index.html b/www/html/Create Room/index.html index ccfe652..57dae5d 100644 --- a/www/html/Create Room/index.html +++ b/www/html/Create Room/index.html @@ -87,7 +87,7 @@ - + + +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 diff --git a/www/html/Game/public/character.html b/www/html/Game/public/character.html index b8cdf32..8ead4d2 100644 --- a/www/html/Game/public/character.html +++ b/www/html/Game/public/character.html @@ -138,7 +138,7 @@

ตัวละคร (4 ทิศ)

- +

อัปโหลดตัวละคร (บน · ล่าง · ซ้าย · ขวา)

diff --git a/www/html/Game/public/css/customize-popup.css b/www/html/Game/public/css/customize-popup.css new file mode 100644 index 0000000..05f31b0 --- /dev/null +++ b/www/html/Game/public/css/customize-popup.css @@ -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; +} diff --git a/www/html/Game/public/css/profile-popup.css b/www/html/Game/public/css/profile-popup.css new file mode 100644 index 0000000..45d7780 --- /dev/null +++ b/www/html/Game/public/css/profile-popup.css @@ -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%; + } +} diff --git a/www/html/Game/public/css/room-lobby.css b/www/html/Game/public/css/room-lobby.css index a1661df..446f653 100644 --- a/www/html/Game/public/css/room-lobby.css +++ b/www/html/Game/public/css/room-lobby.css @@ -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; } diff --git a/www/html/Game/public/dev-local/INSTALL-WINDOWS.txt b/www/html/Game/public/dev-local/INSTALL-WINDOWS.txt new file mode 100644 index 0000000..b73f73b --- /dev/null +++ b/www/html/Game/public/dev-local/INSTALL-WINDOWS.txt @@ -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 + +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. diff --git a/www/html/Game/public/dev-local/bootstrap.js b/www/html/Game/public/dev-local/bootstrap.js new file mode 100644 index 0000000..dab8c3b --- /dev/null +++ b/www/html/Game/public/dev-local/bootstrap.js @@ -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: + * + */ +(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(' - +
v —
diff --git a/www/html/Game/public/img/03-5-Customize/tab-1-face-a.png b/www/html/Game/public/img/03-5-Customize/tab-1-face-a.png new file mode 100644 index 0000000..4c5ed7b Binary files /dev/null and b/www/html/Game/public/img/03-5-Customize/tab-1-face-a.png differ diff --git a/www/html/Game/public/img/03-5-Customize/tab-2-hair-a.png b/www/html/Game/public/img/03-5-Customize/tab-2-hair-a.png new file mode 100644 index 0000000..545f1ac Binary files /dev/null and b/www/html/Game/public/img/03-5-Customize/tab-2-hair-a.png differ diff --git a/www/html/Game/public/img/03-5-Customize/tab-3-cloth-a.png b/www/html/Game/public/img/03-5-Customize/tab-3-cloth-a.png new file mode 100644 index 0000000..f5d867c Binary files /dev/null and b/www/html/Game/public/img/03-5-Customize/tab-3-cloth-a.png differ diff --git a/www/html/Game/public/img/03-5-Customize/tab-line.png b/www/html/Game/public/img/03-5-Customize/tab-line.png new file mode 100644 index 0000000..f96595e Binary files /dev/null and b/www/html/Game/public/img/03-5-Customize/tab-line.png differ diff --git a/www/html/Game/public/index.html b/www/html/Game/public/index.html index 0bb2af7..f96dfd7 100644 --- a/www/html/Game/public/index.html +++ b/www/html/Game/public/index.html @@ -3,14 +3,21 @@ - Game + Game — JD JUSTICE DIVERS +
v —
diff --git a/www/html/Game/public/js/customize-popup.js b/www/html/Game/public/js/customize-popup.js new file mode 100644 index 0000000..87bb80a --- /dev/null +++ b/www/html/Game/public/js/customize-popup.js @@ -0,0 +1,1108 @@ +(function () { + 'use strict'; + + var script = document.currentScript; + var assetBase = (script && script.getAttribute('data-customize-asset-base')) || 'img/03-5-Customize'; + var triggerSelector = (script && script.getAttribute('data-customize-triggers')) || '#btn-customize,#btn-cloth'; + var triggerSelectors = triggerSelector.split(',').map(function (s) { return s.trim(); }).filter(Boolean); + var storageKey = 'customizePopupStateV1'; + var appliedStorageKey = 'customizePopupAppliedV1'; + function isRoomLobbyPage() { + return /room-lobby\.html$/i.test(String(window.location.pathname || '')); + } + + var overlayId = isRoomLobbyPage() ? 'room-cz-overlay' : 'customize-popup-overlay'; + var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:'; + var PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; + var PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' }; + /** ธีมสี 8 ช่อง — default ช่อง 8 = #fec1fe */ + var THEME_SWATCH_HEX = [ + '#d72520', '#ef8508', '#efe237', '#5bb443', + '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe' + ]; + /** สีผิว 3 ช่อง — default ช่อง 1 = #eaa78a */ + var SKIN_SWATCH_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1']; + var PLAY_TINT_HEAD = SKIN_SWATCH_HEX.slice(); + var PLAY_TINT_HAIR = THEME_SWATCH_HEX.slice(); + var PLAY_TINT_BODY = THEME_SWATCH_HEX.slice(); + var DEFAULT_ACTIVE_COLOR = 8; + var DEFAULT_ACTIVE_SKIN = 1; + var previewImagePromiseCache = Object.create(null); + var previewCompositeCache = Object.create(null); + var previewLayerManifestPromiseCache = Object.create(null); + var previewRenderToken = 0; + + function resolveAssetBase(raw) { + var base = String(raw || 'img/03-5-Customize').replace(/\/$/, ''); + if (/^\/(Game|img)\//i.test(base)) { + return typeof window.appPath === 'function' ? window.appPath(base) : base; + } + try { + return new URL(base + '/', window.location.href).pathname.replace(/\/$/, ''); + } catch (e) { + return base; + } + } + + var resolvedAssetBase = resolveAssetBase(assetBase); + + function asset(name) { + return resolvedAssetBase + '/' + name; + } + + function qs(sel) { return document.querySelector(sel); } + + function readInitialState() { + var state = { + activeTab: 1, + activeColor: DEFAULT_ACTIVE_COLOR, + activeSkin: DEFAULT_ACTIVE_SKIN, + activeItem: 1, + coins: '150' + }; + try { + var raw = localStorage.getItem(storageKey); + if (raw) { + var parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + if (parsed.activeTab >= 1 && parsed.activeTab <= 3) state.activeTab = parsed.activeTab; + if (parsed.activeColor >= 1 && parsed.activeColor <= 8) state.activeColor = parsed.activeColor; + if (parsed.activeSkin >= 1 && parsed.activeSkin <= 3) state.activeSkin = parsed.activeSkin; + if (parsed.activeItem >= 1 && parsed.activeItem <= 8) state.activeItem = parsed.activeItem; + } + } + var appliedRaw = localStorage.getItem(appliedStorageKey); + if (appliedRaw) { + var applied = JSON.parse(appliedRaw); + if (applied && typeof applied === 'object') { + if (applied.activeTab >= 1 && applied.activeTab <= 3) state.activeTab = applied.activeTab; + if (applied.activeColor >= 1 && applied.activeColor <= 8) state.activeColor = applied.activeColor; + if (applied.activeSkin >= 1 && applied.activeSkin <= 3) state.activeSkin = applied.activeSkin; + if (applied.activeItem >= 1 && applied.activeItem <= 8) state.activeItem = applied.activeItem; + } + } + var c = localStorage.getItem('jdCoins'); + if (c != null && c !== '') state.coins = String(Math.max(0, parseInt(c, 10) || 0)); + } catch (e) { /* ignore */ } + return state; + } + + var state = readInitialState(); + var refs = { + overlay: null, + dialog: null, + panelShell: null, + closeBtn: null, + confirmBtn: null, + previewFace: null, + nameEl: null, + coinVal: null, + colorRow: null, + skinRow: null, + tabRow: null, + itemsEl: null, + scrollEl: null, + scrollTrack: null, + scrollThumb: null + }; + var scrollbarDrag = { + active: false, + pointerOffsetY: 0 + }; + + function resetStateToConfirmed() { + var confirmed = readInitialState(); + state.activeTab = confirmed.activeTab; + state.activeColor = confirmed.activeColor; + state.activeSkin = confirmed.activeSkin; + state.activeItem = confirmed.activeItem; + state.coins = confirmed.coins; + } + + function persistConfirmedState() { + try { + localStorage.setItem(storageKey, JSON.stringify({ + activeTab: clampIndex(state.activeTab, 1, 3), + activeColor: clampIndex(state.activeColor, 1, 8), + activeSkin: clampIndex(state.activeSkin, 1, 3), + activeItem: clampIndex(state.activeItem, 1, 8) + })); + } catch (e) { /* ignore */ } + } + + function clampIndex(n, min, max) { + var v = parseInt(n, 10); + if (isNaN(v)) v = min; + return Math.max(min, Math.min(max, v)); + } + + function getStoredCharacterId() { + try { + return (localStorage.getItem('gameCharacterId') || '').trim(); + } catch (e) { + return ''; + } + } + + function projectPath(path) { + if (typeof window.appPath === 'function') return window.appPath(path); + return path; + } + + function characterAssetsBasePath() { + var p = projectPath('/Game/img/characters/'); + return p.replace(/\/?$/, '/'); + } + + function defaultShadowPath(dir) { + return projectPath('/Game/img/default-shadow-' + dir + '.png'); + } + + function characterLayerManifestUrl(characterId) { + return projectPath('/Game/api/characters/' + encodeURIComponent(characterId) + '/layer-manifest'); + } + + function fetchCharacterLayerManifest(characterId) { + if (!characterId) return Promise.resolve(null); + if (previewLayerManifestPromiseCache[characterId]) return previewLayerManifestPromiseCache[characterId]; + previewLayerManifestPromiseCache[characterId] = fetch(characterLayerManifestUrl(characterId), { credentials: 'same-origin' }) + .then(function (res) { + if (!res.ok) return null; + return res.json(); + }) + .then(function (json) { + if (!json || json.ok !== true) return null; + return json; + }) + .catch(function () { return null; }); + return previewLayerManifestPromiseCache[characterId]; + } + + function firstManifestFrame(manifest, dir) { + if (!manifest) return null; + var idleDir = manifest.byDirIdle && manifest.byDirIdle[dir]; + if (idleDir && Array.isArray(idleDir.frames) && idleDir.frames.length) return idleDir.frames[0] || null; + var walkDir = manifest.byDir && manifest.byDir[dir]; + if (walkDir && Array.isArray(walkDir.frames) && walkDir.frames.length) return walkDir.frames[0] || null; + return null; + } + + function characterLayerFileUrl(fileName) { + if (!fileName) return ''; + return characterAssetsBasePath() + encodeURIComponent(fileName); + } + + function hexToRgb01(hex) { + var h = String(hex || '').replace('#', '').trim(); + if (h.length !== 6) return [1, 1, 1]; + return [ + parseInt(h.slice(0, 2), 16) / 255, + parseInt(h.slice(2, 4), 16) / 255, + parseInt(h.slice(4, 6), 16) / 255 + ]; + } + + function tintImageData(imageData, tintHex) { + var rgb = hexToRgb01(tintHex); + var tr = rgb[0], tg = rgb[1], tb = rgb[2]; + var d = imageData.data; + for (var i = 0; i < d.length; i += 4) { + if (d[i + 3] < 12) continue; + // Force exact tint color for color layers (bodyColor/headColor/hairColor). + d[i] = Math.min(255, Math.round(tr * 255)); + d[i + 1] = Math.min(255, Math.round(tg * 255)); + d[i + 2] = Math.min(255, Math.round(tb * 255)); + } + } + + function drawTintedLayer(ctx, w, h, img, tintHex) { + if (!tintHex) { + ctx.drawImage(img, 0, 0, w, h); + return; + } + var c = document.createElement('canvas'); + c.width = w; + c.height = h; + var x = c.getContext('2d'); + x.drawImage(img, 0, 0, w, h); + try { + var idata = x.getImageData(0, 0, w, h); + tintImageData(idata, tintHex); + x.putImageData(idata, 0, 0); + } catch (e) { + x.clearRect(0, 0, w, h); + x.drawImage(img, 0, 0, w, h); + } + ctx.drawImage(c, 0, 0, w, h); + } + + function rgbToHsl01(r, g, b) { + var rn = r / 255; + var gn = g / 255; + var bn = b / 255; + var max = Math.max(rn, gn, bn); + var min = Math.min(rn, gn, bn); + var h = 0; + var s = 0; + var l = (max + min) / 2; + if (max !== min) { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case rn: h = (gn - bn) / d + (gn < bn ? 6 : 0); break; + case gn: h = (bn - rn) / d + 2; break; + default: h = (rn - gn) / d + 4; break; + } + h /= 6; + } + return [h, s, l]; + } + + function tintRgbByLuma(r, g, b, tintHex) { + var rgb = hexToRgb01(tintHex); + return [ + Math.min(255, Math.round(rgb[0] * 255)), + Math.min(255, Math.round(rgb[1] * 255)), + Math.min(255, Math.round(rgb[2] * 255)) + ]; + } + + function composeHeuristicTintFromSource(sourceSrc, tint) { + if (!sourceSrc || !tint) return Promise.resolve(null); + var cacheKey = ['heuristic', sourceSrc, tint.head, tint.hair, tint.body].join('|'); + if (Object.prototype.hasOwnProperty.call(previewCompositeCache, cacheKey)) { + return Promise.resolve(previewCompositeCache[cacheKey] || null); + } + return loadImageCached(sourceSrc).then(function (img) { + if (!img || !img.naturalWidth || !img.naturalHeight) return null; + var w = img.naturalWidth; + var h = img.naturalHeight; + var c = document.createElement('canvas'); + c.width = w; + c.height = h; + var ctx = c.getContext('2d'); + ctx.drawImage(img, 0, 0, w, h); + try { + var idata = ctx.getImageData(0, 0, w, h); + var d = idata.data; + function alphaAt(x, y) { + if (x < 0 || y < 0 || x >= w || y >= h) return 0; + return d[((y * w + x) * 4) + 3]; + } + function lumaAt(x, y) { + if (x < 0 || y < 0 || x >= w || y >= h) return 0; + var bi = (y * w + x) * 4; + var aa = d[bi + 3]; + if (aa < 12) return 0; + var rr = d[bi]; + var gg = d[bi + 1]; + var bb = d[bi + 2]; + return (0.299 * rr + 0.587 * gg + 0.114 * bb) / 255; + } + function isOutlinePixel(x, y, a) { + if (a < 12) return false; + // Keep outer contour/stroke untouched: + // pixels touching transparency are treated as outline. + if (alphaAt(x - 1, y) < 12) return true; + if (alphaAt(x + 1, y) < 12) return true; + if (alphaAt(x, y - 1) < 12) return true; + if (alphaAt(x, y + 1) < 12) return true; + return false; + } + function isNearOuterContour(x, y) { + for (var oy = -2; oy <= 2; oy += 1) { + for (var ox = -2; ox <= 2; ox += 1) { + if (ox === 0 && oy === 0) continue; + if (alphaAt(x + ox, y + oy) < 12) return true; + } + } + return false; + } + function isStrokeLikeByContrast(x, y, lig, sat) { + if (sat < 0.18 && lig < 0.68) return true; + var c = lumaAt(x, y); + var d1 = Math.abs(c - lumaAt(x - 1, y)); + var d2 = Math.abs(c - lumaAt(x + 1, y)); + var d3 = Math.abs(c - lumaAt(x, y - 1)); + var d4 = Math.abs(c - lumaAt(x, y + 1)); + var maxDiff = Math.max(d1, d2, d3, d4); + if (maxDiff >= 0.24 && lig < 0.78) return true; + return false; + } + for (var i = 0; i < d.length; i += 4) { + var a = d[i + 3]; + if (a < 12) continue; + var px = (i / 4) % w; + var py = Math.floor((i / 4) / w); + if (isOutlinePixel(px, py, a)) continue; + if (isNearOuterContour(px, py)) continue; + var r = d[i]; + var g = d[i + 1]; + var b = d[i + 2]; + var hsl = rgbToHsl01(r, g, b); + var hueDeg = hsl[0] * 360; + var sat = hsl[1]; + var lig = hsl[2]; + + // Preserve dark lines/stroke and tiny details. + if (lig < 0.23) continue; + if (isStrokeLikeByContrast(px, py, lig, sat)) continue; + + // Skin region: warm hue + upper-center body area + var inHeadArea = py <= h * 0.62 && px >= w * 0.18 && px <= w * 0.82; + var isSkinLike = inHeadArea && hueDeg >= 8 && hueDeg <= 45 && sat >= 0.14 && sat <= 0.78 && lig >= 0.24 && lig <= 0.95; + if (isSkinLike) { + var skinRgb = tintRgbByLuma(r, g, b, tint.head); + d[i] = skinRgb[0]; + d[i + 1] = skinRgb[1]; + d[i + 2] = skinRgb[2]; + continue; + } + + // Theme region: colored non-gray pixels (exclude very dark/very bright) + var isThemeCandidate = sat >= 0.2 && lig >= 0.3 && lig <= 0.95; + if (!isThemeCandidate) continue; + var themeHex = py <= h * 0.52 ? tint.hair : tint.body; + var themeRgb = tintRgbByLuma(r, g, b, themeHex); + d[i] = themeRgb[0]; + d[i + 1] = themeRgb[1]; + d[i + 2] = themeRgb[2]; + } + ctx.putImageData(idata, 0, 0); + } catch (e) { + return null; + } + var out = c.toDataURL('image/png'); + previewCompositeCache[cacheKey] = out; + return out; + }); + } + + function loadImageCached(url) { + if (!url) return Promise.resolve(null); + if (previewImagePromiseCache[url]) return previewImagePromiseCache[url]; + previewImagePromiseCache[url] = new Promise(function (resolve) { + var img = new Image(); + img.decoding = 'async'; + img.onload = function () { resolve(img.naturalWidth > 0 ? img : null); }; + img.onerror = function () { resolve(null); }; + img.src = url; + if (img.complete && img.naturalWidth > 0) resolve(img); + }); + return previewImagePromiseCache[url]; + } + + function resolveFirstImage(urls, idx) { + var index = idx || 0; + if (!urls || index >= urls.length) return Promise.resolve(null); + return loadImageCached(urls[index]).then(function (img) { + if (img) return img; + return resolveFirstImage(urls, index + 1); + }); + } + + function layerUrlCandidates(id, dir, layerName, frameIndex) { + var enc = encodeURIComponent(id); + var base = characterAssetsBasePath() + enc + '_' + dir; + var out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) { + var enc = encodeURIComponent(id); + var base = characterAssetsBasePath() + enc + '_' + dir + '_idle'; + var out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + function getSelectedTint() { + var skinIdx = clampIndex(state.activeSkin, 1, 3) - 1; + var colorIdx = clampIndex(state.activeColor, 1, 8) - 1; + return { + head: PLAY_TINT_HEAD[skinIdx], + hair: PLAY_TINT_HAIR[colorIdx], + body: PLAY_TINT_BODY[colorIdx] + }; + } + + function composeTintedCharacterPreview(characterId, tint) { + if (!characterId || !tint) return Promise.resolve(null); + var dir = 'down'; + var frameIndex = 0; + var cacheKey = [characterId, dir, frameIndex, tint.head, tint.hair, tint.body, 'customize'].join('|'); + if (Object.prototype.hasOwnProperty.call(previewCompositeCache, cacheKey)) { + return Promise.resolve(previewCompositeCache[cacheKey] || null); + } + + return fetchCharacterLayerManifest(characterId).then(function (manifest) { + var manifestFrame = firstManifestFrame(manifest, dir); + if (manifestFrame) { + var manifestLayerPromises = PLAY_LAYER_ORDER.map(function (layerName) { + var urls = []; + var fileName = manifestFrame[layerName]; + if (fileName) urls.push(characterLayerFileUrl(fileName)); + if (layerName === 'shadow') urls.push(defaultShadowPath(dir)); + return resolveFirstImage(urls); + }); + return Promise.all(manifestLayerPromises).then(function (layers) { + var baseImg = null; + for (var bi = 0; bi < layers.length; bi += 1) { + if (layers[bi] && layers[bi].naturalWidth > 0 && layers[bi].naturalHeight > 0) { + baseImg = layers[bi]; + break; + } + } + if (!baseImg) return null; + var c = document.createElement('canvas'); + c.width = baseImg.naturalWidth; + c.height = baseImg.naturalHeight; + var ctx = c.getContext('2d'); + var anyTintColorLayer = false; + for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) { + var layerName = PLAY_LAYER_ORDER[li]; + var img = layers[li]; + if (!img) continue; + var tintKey = PLAY_LAYER_TINT_KEY[layerName]; + var tintHex = tintKey ? tint[tintKey] : null; + if (tintKey) anyTintColorLayer = true; + drawTintedLayer(ctx, c.width, c.height, img, tintHex); + } + if (!anyTintColorLayer) return null; + var out = c.toDataURL('image/png'); + previewCompositeCache[cacheKey] = out; + return out; + }); + } + + var rawCandidates = [ + characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_idle.png', + characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '.png', + characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_0.png' + ]; + + return resolveFirstImage(rawCandidates).then(function (rawImg) { + if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null; + var layerPromises = PLAY_LAYER_ORDER.map(function (layerName) { + var urls = layerUrlCandidatesIdle(characterId, dir, layerName, frameIndex) + .concat(layerUrlCandidates(characterId, dir, layerName, frameIndex)); + if (layerName === 'shadow') urls.push(defaultShadowPath(dir)); + return resolveFirstImage(urls); + }); + return Promise.all(layerPromises).then(function (layers) { + var c = document.createElement('canvas'); + c.width = rawImg.naturalWidth; + c.height = rawImg.naturalHeight; + var ctx = c.getContext('2d'); + var anyTintColorLayer = false; + for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) { + var layerName = PLAY_LAYER_ORDER[li]; + var img = layers[li]; + if (!img) continue; + var tintKey = PLAY_LAYER_TINT_KEY[layerName]; + var tintHex = tintKey ? tint[tintKey] : null; + if (tintKey) anyTintColorLayer = true; + drawTintedLayer(ctx, c.width, c.height, img, tintHex); + } + if (!anyTintColorLayer) return null; + var out = c.toDataURL('image/png'); + previewCompositeCache[cacheKey] = out; + return out; + }); + }); + }); + } + + function resolveDomAvatarSource() { + var avatarSrc = ''; + var avatarAlt = 'ตัวละครของผู้เล่น'; + var candidates = [ + '#lobby-character-img', + '#room-lobby-profile-avatar', + '#room-lobby-profile-overlay-avatar', + '#lobby-profile-avatar', + '#lobby-avatar-wrap img' + ]; + for (var i = 0; i < candidates.length; i += 1) { + var el = document.querySelector(candidates[i]); + if (!el) continue; + var src = (el.getAttribute('src') || '').trim(); + if (!src) continue; + avatarSrc = src; + avatarAlt = (el.getAttribute('alt') || '').trim() || avatarAlt; + break; + } + return { src: avatarSrc, alt: avatarAlt }; + } + + function syncLiveAvatarElements(src, alt) { + if (!src) return; + var selectors = [ + '#lobby-profile-avatar', + '#room-lobby-profile-avatar', + '#room-lobby-profile-overlay-avatar', + '#lobby-character-img' + ]; + selectors.forEach(function (sel) { + var el = document.querySelector(sel); + if (!el || el.tagName !== 'IMG') return; + try { + el.setAttribute('src', src); + if (alt) el.setAttribute('alt', alt); + } catch (e) { /* ignore */ } + }); + } + + function applyCustomizeSelection() { + var characterId = getStoredCharacterId(); + if (!characterId) return Promise.resolve(null); + var tint = getSelectedTint(); + var domAvatar = resolveDomAvatarSource(); + var fallbackSrc = domAvatar.src || ''; + return composeTintedCharacterPreview(characterId, tint) + .then(function (outSrc) { + if (outSrc) return outSrc; + return composeHeuristicTintFromSource(fallbackSrc, tint); + }) + .then(function (finalSrc) { + if (!finalSrc || typeof finalSrc !== 'string' || finalSrc.indexOf('data:image/') !== 0) return null; + try { + localStorage.setItem(LOBBY_IDLE_DOWN_PREFIX + characterId, finalSrc); + localStorage.setItem(appliedStorageKey, JSON.stringify({ + characterId: characterId, + activeTab: clampIndex(state.activeTab, 1, 3), + activeColor: clampIndex(state.activeColor, 1, 8), + activeSkin: clampIndex(state.activeSkin, 1, 3), + activeItem: clampIndex(state.activeItem, 1, 8), + appliedAt: Date.now() + })); + persistConfirmedState(); + syncLobbyTintStorageKeys(); + } catch (e) { /* ignore */ } + syncLiveAvatarElements(finalSrc, 'ตัวละครที่ปรับแต่งแล้ว'); + try { + window.dispatchEvent(new CustomEvent('customize-popup:applied', { + detail: { + characterId: characterId, + activeColor: clampIndex(state.activeColor, 1, 8), + activeSkin: clampIndex(state.activeSkin, 1, 3), + avatarSrc: finalSrc + } + })); + } catch (e2) { /* ignore */ } + return finalSrc; + }) + .catch(function () { return null; }); + } + + function removeLegacyCustomizeOverlays() { + if (!isRoomLobbyPage()) return; + var wrong = document.getElementById('customize-popup-overlay'); + if (wrong && wrong.parentNode) wrong.parentNode.removeChild(wrong); + var old = document.getElementById('room-cz-overlay'); + if (old && !old.querySelector('.customize-popup-dialog') && old.parentNode) { + old.parentNode.removeChild(old); + } + var legacyStyle = document.getElementById('room-cz-style'); + if (legacyStyle && legacyStyle.parentNode) legacyStyle.parentNode.removeChild(legacyStyle); + } + + function createPopupIfNeeded() { + removeLegacyCustomizeOverlays(); + var existing = document.getElementById(overlayId); + if (existing && existing.querySelector('.customize-popup-dialog')) { + bindRefs(existing); + return; + } + if (existing && existing.parentNode) existing.parentNode.removeChild(existing); + + var overlay = document.createElement('div'); + overlay.id = overlayId; + overlay.className = 'customize-popup-overlay is-hidden'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-label', 'ห้องแต่งตัว'); + overlay.setAttribute('aria-hidden', 'true'); + overlay.innerHTML = '' + + ''; + + document.body.appendChild(overlay); + bindRefs(overlay); + bindPopupEvents(); + renderAll(); + } + + function bindRefs(root) { + refs.overlay = root; + refs.dialog = root.querySelector('.customize-popup-dialog'); + refs.panelShell = root.querySelector('.customize-popup-panel-shell'); + refs.closeBtn = root.querySelector('.customize-popup-close'); + refs.confirmBtn = root.querySelector('.customize-popup-confirm'); + refs.previewFace = root.querySelector('.customize-popup-char-preview'); + refs.nameEl = root.querySelector('#customize-popup-player-name'); + refs.coinVal = root.querySelector('.customize-popup-coin-val'); + refs.colorRow = root.querySelector('.customize-popup-color-row'); + refs.skinRow = root.querySelector('.customize-popup-skin-row'); + refs.tabRow = root.querySelector('.customize-popup-tabs'); + refs.itemsEl = root.querySelector('.customize-popup-items'); + refs.scrollEl = root.querySelector('.customize-popup-scroll'); + refs.scrollTrack = root.querySelector('.customize-popup-scroll-track'); + refs.scrollThumb = root.querySelector('.customize-popup-scroll-thumb'); + } + + function syncScale() { + if (!refs.dialog) return; + var w = refs.dialog.clientWidth || 0; + var h = refs.dialog.clientHeight || 0; + if (!w || !h) return; + // Scale from full popup canvas, not right panel only. + // This prevents left character column from shifting when right panel is tweaked. + var scaleW = w / 1600; + var scaleH = h / 951; + var vw = window.innerWidth || 0; + var vh = window.innerHeight || 0; + var minScale = (vw <= 1200 || vh <= 820) ? 0.32 : 0.45; + var scale = Math.max(minScale, Math.min(1, Math.min(scaleW, scaleH))); + refs.dialog.style.setProperty('--cc-scale', String(scale.toFixed(4))); + if (refs.panelShell) { + var shellW = refs.panelShell.clientWidth || 0; + var shellH = refs.panelShell.clientHeight || 0; + if (shellW && shellH) { + var shellScaleW = shellW / 990; + var shellScaleH = shellH / 951; + var shellScale = Math.max(0.28, Math.min(1, Math.min(shellScaleW, shellScaleH))); + refs.panelShell.style.setProperty('--cc-shell-scale', String(shellScale.toFixed(4))); + } + } + } + + function getPlayerName() { + if (typeof window.getProfileDisplayName === 'function') { + var fromLobby = String(window.getProfileDisplayName() || '').trim(); + if (fromLobby) return fromLobby; + } + var name = ''; + try { + name = (localStorage.getItem('roomLobbyDisplayName') || '').trim() || + (localStorage.getItem('playerName') || '').trim() || ''; + } catch (e) { /* ignore */ } + return name || 'MONE'; + } + + function syncLobbyTintStorageKeys() { + try { + localStorage.setItem('lobbyThemeColor', String(clampIndex(state.activeColor, 1, 8))); + localStorage.setItem('lobbySkinTone', String(clampIndex(state.activeSkin, 1, 3))); + if (clampIndex(state.activeTab, 1, 3) === 1) { + localStorage.setItem('lobbyItem_face', String(clampIndex(state.activeItem, 1, 8))); + } + } catch (e) { /* ignore */ } + } + + function readLobbyTintIntoState() { + try { + var c = localStorage.getItem('lobbyThemeColor'); + if (c) state.activeColor = clampIndex(c, 1, 8); + var s = localStorage.getItem('lobbySkinTone'); + if (s) state.activeSkin = clampIndex(s, 1, 3); + var f = localStorage.getItem('lobbyItem_face'); + if (f) state.activeItem = clampIndex(f, 1, 8); + } catch (e) { /* ignore */ } + } + + function swatchButtonHtml(hex, isActive) { + return '' + + '' + + ''; + } + + function renderColorRow() { + if (!refs.colorRow) return; + refs.colorRow.setAttribute('role', 'radiogroup'); + refs.colorRow.setAttribute('aria-label', 'ธีมสี'); + refs.colorRow.textContent = ''; + for (var i = 1; i <= 8; i += 1) { + var hex = THEME_SWATCH_HEX[i - 1] || '#ffffff'; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'customize-popup-color-btn' + (state.activeColor === i ? ' is-active' : ''); + btn.setAttribute('aria-label', 'เลือกสีที่ ' + i); + btn.setAttribute('aria-pressed', state.activeColor === i ? 'true' : 'false'); + btn.innerHTML = swatchButtonHtml(hex, state.activeColor === i); + (function (idx) { + btn.addEventListener('click', function () { + state.activeColor = idx; + renderColorRow(); + renderPreview(); + }); + })(i); + refs.colorRow.appendChild(btn); + } + } + + function renderSkinRow() { + if (!refs.skinRow) return; + refs.skinRow.setAttribute('role', 'radiogroup'); + refs.skinRow.setAttribute('aria-label', 'สีผิว'); + refs.skinRow.textContent = ''; + for (var i = 1; i <= 3; i += 1) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'customize-popup-color-btn' + (state.activeSkin === i ? ' is-active' : ''); + btn.setAttribute('aria-label', 'เลือกสีผิวที่ ' + i); + btn.setAttribute('aria-pressed', state.activeSkin === i ? 'true' : 'false'); + var skinHex = SKIN_SWATCH_HEX[i - 1] || '#eaa78a'; + btn.innerHTML = swatchButtonHtml(skinHex, state.activeSkin === i); + (function (idx) { + btn.addEventListener('click', function () { + state.activeSkin = idx; + renderSkinRow(); + renderPreview(); + }); + })(i); + refs.skinRow.appendChild(btn); + } + } + + function tabImageName(index, isActive) { + if (index === 1) return isActive ? 'tab-1-face-a.png' : 'tab-1-face.png'; + if (index === 2) return isActive ? 'tab-2-hair-a.png' : 'tab-2-hair.png'; + return isActive ? 'tab-3-cloth-a.png' : 'tab-3-cloth.png'; + } + + function renderTabs() { + if (!refs.tabRow) return; + if (state.activeTab === 1) { + refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(-27px * var(--cc-scale))'); + } else if (state.activeTab === 3) { + refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(0px * var(--cc-scale))'); + } else { + refs.tabRow.style.setProperty('--customize-tab-mid-line-shift-x', 'calc(0px * var(--cc-scale))'); + } + refs.tabRow.textContent = ''; + for (var i = 1; i <= 3; i += 1) { + var active = state.activeTab === i; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'customize-popup-tab-btn' + (active ? ' is-active' : ''); + btn.setAttribute('aria-label', 'หมวด ' + i); + btn.innerHTML = '' + + '' + + ''; + (function (idx) { + btn.addEventListener('click', function () { + state.activeTab = idx; + renderTabs(); + renderItems(); + }); + })(i); + refs.tabRow.appendChild(btn); + } + } + + function renderItems() { + if (!refs.itemsEl) return; + refs.itemsEl.setAttribute('role', 'radiogroup'); + refs.itemsEl.setAttribute('aria-label', 'รายการปรับแต่งตัวละคร'); + refs.itemsEl.textContent = ''; + for (var i = 1; i <= 8; i += 1) { + var li = document.createElement('li'); + li.className = 'customize-popup-item' + (state.activeItem === i ? ' is-active' : ''); + li.setAttribute('role', 'radio'); + li.setAttribute('aria-checked', state.activeItem === i ? 'true' : 'false'); + li.setAttribute('aria-label', 'ไอเท็มที่ ' + i); + li.tabIndex = 0; + var price = (state.activeTab === 1 ? (i <= 2 ? 50 : i <= 5 ? 100 : 200) : (i <= 3 ? 120 : 220)); + li.innerHTML = '' + + '' + + '' + + '' + + '' + price + '' + + ''; + (function (idx) { + function selectItem() { + state.activeItem = idx; + renderItems(); + renderPreview(); + } + li.addEventListener('click', function () { + selectItem(); + }); + li.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selectItem(); + } + }); + })(i); + refs.itemsEl.appendChild(li); + } + syncScrollUi(); + } + + function renderPreview() { + if (!refs.previewFace) return; + var token = ++previewRenderToken; + var domAvatar = resolveDomAvatarSource(); + var fallbackSrc = domAvatar.src || asset('face-' + state.activeItem + '.png'); + refs.previewFace.src = fallbackSrc; + refs.previewFace.alt = domAvatar.alt; + var characterId = getStoredCharacterId(); + composeTintedCharacterPreview(characterId, getSelectedTint()) + .then(function (outSrc) { + if (token !== previewRenderToken || !refs.previewFace) return; + if (outSrc) { + refs.previewFace.src = outSrc; + refs.previewFace.alt = 'ตัวอย่างตัวละครที่ปรับแต่งสีแล้ว'; + return; + } + return composeHeuristicTintFromSource(fallbackSrc, getSelectedTint()).then(function (heuristicSrc) { + if (token !== previewRenderToken || !refs.previewFace) return; + refs.previewFace.src = heuristicSrc || fallbackSrc; + refs.previewFace.alt = heuristicSrc ? 'ตัวอย่างตัวละครที่ปรับแต่งสีแล้ว' : domAvatar.alt; + }); + }) + .catch(function () { + if (token !== previewRenderToken || !refs.previewFace) return; + refs.previewFace.src = fallbackSrc; + refs.previewFace.alt = domAvatar.alt; + }); + } + + function syncCoins() { + if (!refs.coinVal) return; + var c = '150'; + try { + var raw = localStorage.getItem('jdCoins'); + if (raw != null && raw !== '') c = String(Math.max(0, parseInt(raw, 10) || 0)); + } catch (e) { /* ignore */ } + refs.coinVal.textContent = c; + } + + function syncPlayerName() { + if (!refs.nameEl) return; + refs.nameEl.textContent = getPlayerName().toUpperCase(); + } + + function syncScrollUi() { + if (!refs.itemsEl || !refs.scrollEl || !refs.scrollThumb) return; + var total = refs.itemsEl.scrollHeight || 0; + var visible = refs.itemsEl.clientHeight || 0; + // Hide scrollbar when overflow is only tiny visual jitter. + var overflowPx = Math.max(0, total - visible); + var overflow = overflowPx > 12; + refs.scrollEl.classList.toggle('is-hidden', !overflow); + refs.itemsEl.style.overflowY = overflow ? 'auto' : 'hidden'; + if (!overflow) { + refs.itemsEl.scrollTop = 0; + return; + } + var maxScroll = Math.max(1, total - visible); + var ratio = Math.max(0, Math.min(1, refs.itemsEl.scrollTop / maxScroll)); + var minBarPct = 18; + var maxBarPct = 42; + var rawBarPct = (visible / Math.max(1, total)) * 100; + var barPct = Math.max(minBarPct, Math.min(maxBarPct, rawBarPct)); + var topPct = 0; + var travelPct = Math.max(0, 100 - topPct - barPct); + refs.scrollThumb.style.height = barPct.toFixed(2) + '%'; + refs.scrollThumb.style.top = (topPct + (travelPct * ratio)).toFixed(2) + '%'; + } + + function setItemsScrollByRatio(ratio) { + if (!refs.itemsEl) return; + var total = refs.itemsEl.scrollHeight || 0; + var visible = refs.itemsEl.clientHeight || 0; + var maxScroll = Math.max(0, total - visible); + var safe = Math.max(0, Math.min(1, ratio)); + refs.itemsEl.scrollTop = maxScroll * safe; + syncScrollUi(); + } + + function scrollRatioFromClientY(clientY, offsetY) { + if (!refs.scrollEl || !refs.scrollThumb) return 0; + var rect = refs.scrollEl.getBoundingClientRect(); + var thumbRect = refs.scrollThumb.getBoundingClientRect(); + var thumbH = Math.max(1, thumbRect.height); + var trackTop = rect.top; + var maxTop = rect.height - thumbH; + if (maxTop <= 0) return 0; + var thumbTop = clientY - trackTop - Math.max(0, offsetY || 0); + thumbTop = Math.max(0, Math.min(maxTop, thumbTop)); + return thumbTop / maxTop; + } + + function stopScrollbarDrag() { + if (!scrollbarDrag.active) return; + scrollbarDrag.active = false; + if (refs.scrollThumb) refs.scrollThumb.classList.remove('is-dragging'); + try { document.body.style.userSelect = ''; } catch (e) { /* ignore */ } + } + + function startScrollbarDrag(e) { + if (!refs.scrollThumb || !refs.scrollEl || refs.scrollEl.classList.contains('is-hidden')) return; + e.preventDefault(); + var thumbRect = refs.scrollThumb.getBoundingClientRect(); + scrollbarDrag.active = true; + scrollbarDrag.pointerOffsetY = e.clientY - thumbRect.top; + refs.scrollThumb.classList.add('is-dragging'); + try { document.body.style.userSelect = 'none'; } catch (err) { /* ignore */ } + } + + function renderAll() { + renderColorRow(); + renderSkinRow(); + renderTabs(); + renderItems(); + renderPreview(); + syncCoins(); + syncPlayerName(); + syncScale(); + requestAnimationFrame(syncScale); + requestAnimationFrame(syncScrollUi); + } + + function openPopup() { + createPopupIfNeeded(); + if (!refs.overlay) return; + resetStateToConfirmed(); + readLobbyTintIntoState(); + renderAll(); + refs.overlay.classList.remove('is-hidden'); + refs.overlay.setAttribute('aria-hidden', 'false'); + syncCoins(); + syncPlayerName(); + syncScale(); + syncScrollUi(); + renderPreview(); + requestAnimationFrame(syncScale); + requestAnimationFrame(syncScrollUi); + } + + function closePopup() { + if (!refs.overlay) return; + stopScrollbarDrag(); + refs.overlay.classList.add('is-hidden'); + refs.overlay.setAttribute('aria-hidden', 'true'); + } + + function bindPopupEvents() { + if (!refs.overlay) return; + refs.overlay.addEventListener('click', function (e) { + if (e.target === refs.overlay) closePopup(); + }); + refs.closeBtn && refs.closeBtn.addEventListener('click', closePopup); + refs.confirmBtn && refs.confirmBtn.addEventListener('click', function () { + applyCustomizeSelection().finally(function () { + closePopup(); + }); + }); + refs.itemsEl && refs.itemsEl.addEventListener('scroll', syncScrollUi, { passive: true }); + refs.scrollThumb && refs.scrollThumb.addEventListener('mousedown', startScrollbarDrag); + refs.scrollTrack && refs.scrollTrack.addEventListener('mousedown', function (e) { + if (refs.scrollEl && refs.scrollEl.classList.contains('is-hidden')) return; + if (e.target === refs.scrollThumb) return; + e.preventDefault(); + var ratio = scrollRatioFromClientY(e.clientY, (refs.scrollThumb.getBoundingClientRect().height || 0) / 2); + setItemsScrollByRatio(ratio); + }); + document.addEventListener('mousemove', function (e) { + if (!scrollbarDrag.active) return; + e.preventDefault(); + var ratio = scrollRatioFromClientY(e.clientY, scrollbarDrag.pointerOffsetY); + setItemsScrollByRatio(ratio); + }); + document.addEventListener('mouseup', function () { + stopScrollbarDrag(); + }); + window.addEventListener('resize', function () { + syncScale(); + syncScrollUi(); + }); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && refs.overlay && !refs.overlay.classList.contains('is-hidden')) { + stopScrollbarDrag(); + closePopup(); + } + }); + } + + function bindTriggers() { + triggerSelectors.forEach(function (sel) { + document.querySelectorAll(sel).forEach(function (el) { + el.addEventListener('click', function (e) { + e.preventDefault(); + openPopup(); + }); + }); + }); + } + + function init() { + createPopupIfNeeded(); + if (triggerSelectors.length) bindTriggers(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); + } + + window.openCustomizePopup = openPopup; + window.closeCustomizePopup = closePopup; +})(); diff --git a/www/html/Game/public/js/display-name.js b/www/html/Game/public/js/display-name.js new file mode 100644 index 0000000..88186fb --- /dev/null +++ b/www/html/Game/public/js/display-name.js @@ -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 = '' + + '' + + ''; + document.body.appendChild(wrap); + } + editorReady = true; + } + + /** + * เปิดกล่องแก้ชื่อ (ใช้แทน window.prompt — ทำงานใน Cursor browser / mobile) + * @param {{ initialValue?: string, title?: string }} [options] + * @returns {Promise} ชื่อใหม่ หรือ 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); diff --git a/www/html/Game/public/js/editor.js b/www/html/Game/public/js/editor.js index 3049045..594d91a 100644 --- a/www/html/Game/public/js/editor.js +++ b/www/html/Game/public/js/editor.js @@ -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)); diff --git a/www/html/Game/public/js/game-http-base.js b/www/html/Game/public/js/game-http-base.js new file mode 100644 index 0000000..70091f7 --- /dev/null +++ b/www/html/Game/public/js/game-http-base.js @@ -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); diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index ffb9ea2..7cb8fb2 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -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() { diff --git a/www/html/Game/public/js/profile-popup.js b/www/html/Game/public/js/profile-popup.js new file mode 100644 index 0000000..4eb7a92 --- /dev/null +++ b/www/html/Game/public/js/profile-popup.js @@ -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 = '' + + '' + + ''; + 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(); + } +})(); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index 24194b3..b7df0b5 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -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 = '

โหลด Socket ไม่สำเร็จ (localhost)

เพิ่มในไฟล์ room-lobby.html บนเครื่องคุณเท่านั้น:
<script src="https://srv1361159.hstgr.cloud/Game/dev-local/bootstrap.js"></script>
ดู Game/dev-local/INSTALL-WINDOWS.txt

กลับเมนูหลัก
'; + (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 = - '
' + - '
' + - '

ห้องแต่งตัว

' + - '
ธีมสี
' + - '
สีผิว
' + - '
' + - '
' + - '' + - '
'; - 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'); diff --git a/www/html/Game/public/lobby.html b/www/html/Game/public/lobby.html index 0bddc14..a735a26 100644 --- a/www/html/Game/public/lobby.html +++ b/www/html/Game/public/lobby.html @@ -4,57 +4,16 @@ ล็อบบี้ — Game - + + + -
-

ล็อบบี้

-

กลับ · เลือกตัวละคร

-

- -
-

ห้องสาธารณะ (Public) — เลือกเข้าร่วมได้เลย

-
-

โหลดรายการห้อง...

-
    -
    -
    - -
    -

    สร้างห้องใหม่

    -

    เลือกประเภทห้อง แล้วกดสร้าง

    -

    - — โชว์ในรายการด้านบน คนอื่น join ได้เลย -
    - — สร้างรหัสอัตโนมัติ แชร์รหัสเพื่อให้คนอื่นเข้า -

    -

    - -

    - -
    - -
    -

    เข้าร่วมห้องส่วนตัว (Private)

    -

    กรอกรหัสห้องที่ได้จาก host เพื่อเข้า

    -
    - - -
    -
    -
    - - -
    v —
    +

    กำลังไปเมนูหลัก… คลิกที่นี่ ถ้าไม่ redirect อัตโนมัติ

    diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index b8e7ff0..a39e7c1 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -3211,9 +3211,10 @@
    + - +
    v —
    diff --git a/www/html/Game/public/room-lobby.html b/www/html/Game/public/room-lobby.html index 578705b..79faeb1 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -7,7 +7,9 @@ - + + +