From 740c8582241406ff52b0c14c6b3ee1d56a9dcb78 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Wed, 27 May 2026 08:02:22 +0000 Subject: [PATCH] update design --- www/html/Create Room/FIX-NOW.bat | 34 + www/html/Create Room/FIX-NOW.ps1 | 25 + .../Create Room/PASTE-INTO-index.html.txt | 13 + www/html/Create Room/create-room.js | 30 +- www/html/Create Room/index.html | 2 +- www/html/Create Room/jd-local-dev.js | 78 + www/html/Create Room/sync-from-production.cmd | 23 + www/html/FONTS/NotoSansThai-ExtraBold.ttf | Bin 0 -> 47524 bytes www/html/FONTS/NotoSansThai-Medium.ttf | Bin 0 -> 47472 bytes www/html/Game/css | 1 + www/html/Game/data/maps/mlsbbxfe.json | 2 +- www/html/Game/js | 1 + www/html/Game/public/LOBBY-FIX-PASTE.txt | 14 + www/html/Game/public/character.html | 2 +- www/html/Game/public/css/customize-popup.css | 613 ++++++ www/html/Game/public/css/profile-popup.css | 592 ++++++ www/html/Game/public/css/room-lobby.css | 543 +---- .../Game/public/dev-local/INSTALL-WINDOWS.txt | 16 + www/html/Game/public/dev-local/bootstrap.js | 46 + www/html/Game/public/editor.html | 3 +- .../img/03-5-Customize/tab-1-face-a.png | Bin 0 -> 9297 bytes .../img/03-5-Customize/tab-2-hair-a.png | Bin 0 -> 8988 bytes .../img/03-5-Customize/tab-3-cloth-a.png | Bin 0 -> 7463 bytes .../public/img/03-5-Customize/tab-line.png | Bin 0 -> 1524 bytes www/html/Game/public/index.html | 11 +- www/html/Game/public/js/customize-popup.js | 1108 ++++++++++ www/html/Game/public/js/display-name.js | 250 +++ www/html/Game/public/js/editor.js | 35 + www/html/Game/public/js/game-http-base.js | 96 + www/html/Game/public/js/play.js | 75 +- www/html/Game/public/js/profile-popup.js | 309 +++ www/html/Game/public/js/room-lobby.js | 420 ++-- www/html/Game/public/lobby.html | 59 +- www/html/Game/public/play.html | 3 +- www/html/Game/public/room-lobby.html | 59 +- www/html/Game/server.js | 112 +- www/html/Game/start-dev.cmd | 17 + www/html/Join Room/index.html | 34 +- www/html/Join Room/join.js | 139 +- www/html/Join Room/style.css | 371 +++- www/html/Login/index.html | 2 +- www/html/Main-Lobby/IMAGE/Daily/Day-1-h.png | Bin 0 -> 3917 bytes www/html/Main-Lobby/IMAGE/Daily/Day-1.png | Bin 0 -> 1084 bytes www/html/Main-Lobby/IMAGE/Daily/Day-2-h.png | Bin 0 -> 4281 bytes www/html/Main-Lobby/IMAGE/Daily/Day-2.png | Bin 0 -> 1164 bytes www/html/Main-Lobby/IMAGE/Daily/Day-3-h.png | Bin 0 -> 4347 bytes www/html/Main-Lobby/IMAGE/Daily/Day-3.png | Bin 0 -> 1244 bytes www/html/Main-Lobby/IMAGE/Daily/Day-4-h.png | Bin 0 -> 4228 bytes www/html/Main-Lobby/IMAGE/Daily/Day-4.png | Bin 0 -> 1148 bytes www/html/Main-Lobby/IMAGE/Daily/Day-5-h.png | Bin 0 -> 4311 bytes www/html/Main-Lobby/IMAGE/Daily/Day-5.png | Bin 0 -> 1206 bytes www/html/Main-Lobby/IMAGE/Daily/Day-6-h.png | Bin 0 -> 4419 bytes www/html/Main-Lobby/IMAGE/Daily/Day-6.png | Bin 0 -> 1282 bytes www/html/Main-Lobby/IMAGE/Daily/Day-7-h.png | Bin 0 -> 4151 bytes www/html/Main-Lobby/IMAGE/Daily/Day-7.png | Bin 0 -> 1146 bytes .../IMAGE/Daily/btn-claim-reward.png | Bin 0 -> 7380 bytes www/html/Main-Lobby/IMAGE/Daily/daily-bg.png | Bin 0 -> 95899 bytes www/html/Main-Lobby/IMAGE/Daily/icon-bg.png | Bin 0 -> 7655 bytes .../Main-Lobby/IMAGE/Daily/icon-frame-h.png | Bin 0 -> 3734 bytes .../IMAGE/Daily/icon-frame-lock.png | Bin 0 -> 1997 bytes .../Main-Lobby/IMAGE/Daily/icon-frame.png | Bin 0 -> 1235 bytes www/html/Main-Lobby/IMAGE/Daily/reward-01.png | Bin 0 -> 4755 bytes www/html/Main-Lobby/IMAGE/Daily/reward-02.png | Bin 0 -> 6161 bytes www/html/Main-Lobby/IMAGE/Daily/reward-03.png | Bin 0 -> 8034 bytes www/html/Main-Lobby/IMAGE/Daily/reward-04.png | Bin 0 -> 9620 bytes www/html/Main-Lobby/IMAGE/Daily/reward-05.png | Bin 0 -> 10452 bytes www/html/Main-Lobby/IMAGE/Daily/reward-06.png | Bin 0 -> 9012 bytes www/html/Main-Lobby/IMAGE/Daily/reward-07.png | Bin 0 -> 8810 bytes .../IMAGE/Daily/reward-redeemed.png | Bin 0 -> 3832 bytes .../Daily/rewards-frame-d7-received-top.png | Bin 0 -> 1877 bytes .../IMAGE/Daily/rewards-frame-d7.png | Bin 0 -> 5113 bytes .../IMAGE/Daily/rewards-frame-hilight-d7.png | Bin 0 -> 4873 bytes .../IMAGE/Daily/rewards-frame-hilight.png | Bin 0 -> 4239 bytes .../IMAGE/Daily/rewards-frame-received-d7.png | Bin 0 -> 4994 bytes .../Daily/rewards-frame-received-top.png | Bin 0 -> 1521 bytes .../IMAGE/Daily/rewards-frame-received.png | Bin 0 -> 4408 bytes .../Main-Lobby/IMAGE/Daily/rewards-frame.png | Bin 0 -> 4533 bytes .../Main-Lobby/IMAGE/Daily/txt-locked.png | Bin 0 -> 1202 bytes .../Main-Lobby/IMAGE/Daily/txt-recieved.png | Bin 0 -> 1347 bytes www/html/Main-Lobby/daily-popup.css | 235 +++ www/html/Main-Lobby/daily-popup.js | 578 ++++++ www/html/Main-Lobby/index.html | 45 +- www/html/Main-Lobby/lobby.js | 433 ++-- www/html/Main-Lobby/style.css | 319 +-- www/html/Main-Menu/index.html | 6 +- www/html/Main-Menu/menu.js | 15 + www/html/Main-Menu/style.css | 1795 ++++++----------- www/html/Quiz-Battle/index.html | 32 +- www/html/Quiz-Battle/quiz-battle.js | 156 +- www/html/Quiz-Battle/style.css | 701 ++++--- www/html/app-base.js | 31 +- 91 files changed, 6556 insertions(+), 2928 deletions(-) create mode 100644 www/html/Create Room/FIX-NOW.bat create mode 100644 www/html/Create Room/FIX-NOW.ps1 create mode 100644 www/html/Create Room/PASTE-INTO-index.html.txt create mode 100644 www/html/Create Room/jd-local-dev.js create mode 100644 www/html/Create Room/sync-from-production.cmd create mode 100644 www/html/FONTS/NotoSansThai-ExtraBold.ttf create mode 100644 www/html/FONTS/NotoSansThai-Medium.ttf create mode 120000 www/html/Game/css create mode 120000 www/html/Game/js create mode 100644 www/html/Game/public/LOBBY-FIX-PASTE.txt create mode 100644 www/html/Game/public/css/customize-popup.css create mode 100644 www/html/Game/public/css/profile-popup.css create mode 100644 www/html/Game/public/dev-local/INSTALL-WINDOWS.txt create mode 100644 www/html/Game/public/dev-local/bootstrap.js create mode 100644 www/html/Game/public/img/03-5-Customize/tab-1-face-a.png create mode 100644 www/html/Game/public/img/03-5-Customize/tab-2-hair-a.png create mode 100644 www/html/Game/public/img/03-5-Customize/tab-3-cloth-a.png create mode 100644 www/html/Game/public/img/03-5-Customize/tab-line.png create mode 100644 www/html/Game/public/js/customize-popup.js create mode 100644 www/html/Game/public/js/display-name.js create mode 100644 www/html/Game/public/js/game-http-base.js create mode 100644 www/html/Game/public/js/profile-popup.js create mode 100644 www/html/Game/start-dev.cmd create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-1-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-1.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-2-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-2.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-3-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-3.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-4-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-4.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-5-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-5.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-6-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-6.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-7-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/Day-7.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/btn-claim-reward.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/daily-bg.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/icon-bg.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/icon-frame-h.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/icon-frame-lock.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/icon-frame.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-01.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-02.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-03.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-04.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-05.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-06.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-07.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/reward-redeemed.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-d7-received-top.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-d7.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-hilight-d7.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-hilight.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-received-d7.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-received-top.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame-received.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/rewards-frame.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/txt-locked.png create mode 100644 www/html/Main-Lobby/IMAGE/Daily/txt-recieved.png create mode 100644 www/html/Main-Lobby/daily-popup.css create mode 100644 www/html/Main-Lobby/daily-popup.js 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 0000000000000000000000000000000000000000..4c5ed7bc91523b99045c2cea56e2133030ee55d9 GIT binary patch literal 9297 zcmV-XB(B?uP)B z-)+Y-GBRc(MNFCTy+nVs6FUI42>@y*dTRfmLLnm~qaCxzo!U=+zlad%#0L^uKPGwj zIZps&@@Hf`IA)Q(%|amkPPsfw5Yz-fi`H)=d`bW;Vi9CyWXwk!`Q!Z>=~HD8Ky5<6 z3xFBqPmw;g#lH(b<8m`HGMZ>J{Zpimw^Ug*Yak^AI{IaBY!SknmgDj7!_OF57-VF$ zo9By?pA*NzGG+>a&VuAOiTySq@E~qL76ut-3G;$;2O&S9E(ADHR;v&QV*n}g$7DAx z$5P}siLx^?GUg)|0I`DHj#n5H}Fe;*# zu$)sXK!pI@r$LJl2(5tF0!W$u;Z`J$wOIu~wgNIT4vN@^=U~4?B>&qMO#Tk)r0*s^L+c>4 z1QyCq&d509h=YG6EcvIfpnZd zZ^H63GBRc(R+6gervS!o>jK!vY&Sd38jQ(L2x0(jWB#$`S7IyCdjOV$O^>AnK&o+% z8xAVuPRso`NV`r;n@%didmZ}!6Ss|E$sh7`AN6=ayeSXEm_Th5V3Fv*Zc2V7ecx@% zaMr-T8si_o!1z0bJ^t9@uzUJRF_8|lT_>eYrx_#Q-fIQE_T?`A7b;gfRMv`Fi9;d3 zy%i8!0I9M6IMAnNzY>2X*kAh9F@Etm#@{~NTX1r4lX$ZF*d=3|#)P z62HVi@s%#ViRMV#tn}b8PXN*WOcx*)2BG#}vh~7Bu=Rg+j9++;@d*H!F!Nt;-rpke z6|6eIj73OJ8xEr-CzTiv)3%e^rV|OO?pAQmNeJi_g?ahH#R~xN6D3{|z`SaOKuCTL zK6TU}UHBO(xh z;%@br$O56(F5fmZ13D#wa7No2I4x~Db|h}85J5l?0)dFw7!o47yT3pF@vGOK0Dw=H z_;~>IY8QWqdU8-WOsK~S)GdH`L?CGa%*qD{1Akr!d~S?ap0AVt#{Ip$QNhCN-EQ~N zQm<>t53l`CtA=Q@SdUZU==@^5K#U4Lb{F8_?K*L7I_aq09ZUix5bdyko!ASwW)at9vnCO%5kADC|eH6LX1k9rBxIKw$h)j5xf zFb8kf{DMwGyG}IfH^}NnveXkITs?no^@aZO^7r=!Llk1Zd-vWpP!j?_PXK?Vi*LXx z;xQ~Q$7rVLhg%I{D)1~J5RL%&AwU=K^FF_lzdM)meW$(7~)joWuN$K(Ae1VUn` zAj~rp=>!G|Dv4DwBsdmm%rp9G<3! zkEcy14TAaplMn^;Bw`}m7!4;MdSd;;^Zowv^}%2`1rQ5?pD6K40ZhNt#qXn@h&P7i zYnUmR+GJKLupW;`13CaYuT1bMjrM!9-;=-7d1_^)fAiLz&FTLBxL^^(>9duXW65NQy<4r>Y3t{^1I?*qad?tO~9;5(5J_|X#o>>lCfH#+z~ zu-sIxwsE?Kcu~;sq;Qy_AqZY%{4h`j{O<4VjXKkk*Sp=$Q!6X|_it@%PWSi6ohb{6 zz<@c_{*|^k*&U!p{?IR`O1e9;0A!mNbEjTl5Caj*ii#+ea6Br)G>Gs1ui8{qK5Um( zhi0^EUYl%b&32thAwX2Ks)PvQZZ4A{F$9r`+_-h;{xjFsFFm)i(*NFYI06xU=idFA z5O|64(Qg2Mj_ zom;{Y5lRUHf3Dt}ls~GQqaX)koWvyqIg3-UzLGEC|AO zrG(q#DF`gW_)6HPtk5M$Da3_gBTO!UFET0ly^T9tPhY)y@%h!2{=38BzEBZ!G1mAE zZJ(-7$^}UMaJEw`BA1qW-4C|5_r~M>NvCvVXZP@#VegjAry?W1cCjX;vj-k^rEgo_Lw-wQ^cB_P@`V} zZJet!xzMcIH`V=thBGVkUV=6{tFO_ndYjIQAaDx7D-nAWtar)Q$P!aBabd&=lL!e3 zi%glX-@Ui}gHK(%jKN?OzR@ReCFWW4h`w*W7$kL4faKyHn&cx~GT1&iBh187Bfnu0 zB|W*$j#f9|m{Ajfp8QtZvk=JAhyy~5!IZIXL@hEwE$=U;Xy1W z{Y~YNTVF-2ydto`x5<5Fxk{ogj~7=pApqNdMI^WhqIaULU6omJh6DjsZBYR#Wc!p5 z$Pk2$6#~RUVn~PuWGrulS)adXUua$-xgQ>D!~o{~Al&yVsdiQ%f{d_rjh%TLlb=M? z9JH2m(6LXF5Ku|2oEP`R7(`Bol4e&pA9=Qw1WP`vR+|s6exucLvMi~^2!QB5Ey9j^ zFvArQhy-%(zkM{;(+>octA+Bt(I)#F^3OD|_1)iMT?0pcZI?UW3xc3+HRXWEKnQsD z*c;7hyPAwZ*qvnWn|BnTnxf*3FFNLB%7RZ>0W!-1SQuWLJ4?U`1V{9W^zPH|*L>kN z`X9rC56xQ_oduCt5&&wh*-j6Hx812h`?Q5BgDEiQVeN;wVeY}|!hZszOlzWwXf3S~;q8NFyWA}k z)o4)S1C?dq1&esGcN&C%s49ryZnqDU2tf=$3=+~YCKZPwRp9bz#{Sk8f*{{f%RidpwnvCndX8KW?~_2y6_2Kul!Er|tbvM?t6* zDT!d5k7_Z)D^eOz)gq0Y+WzBZL1hit-BTe1QiK&Xf^1+Uj3L1Q#J+7rHr^DJcZBUd z5(@xo{(FgTB{;?8ulaz$vDo_yzzvOA^VdoOr;-Tlo_1-7gg~V+D5)SZL~INI7!A4` z79tTAf*58aMZu;l9si`}tf^7qm9YyrSo+mX@I~9;_f;M_0i|w^4My3X>GB)Y0w6Zz z1b`tn^$7z8wq0<2-;^v>JL>H+DnyAuY-N~@Ay{TOj(DQ0G?2g&kOQ{;BtShQ_m{xO z|FwbMp>*GR3-Do-@?0F1007`kf4d(zL6sS(8Ipp`wf@rb)#cujPVXcEzO^&hG0X%*LQ^m?8!n6#CC_iL(R=xT{h^k^r&?bn z3E6!wdC2;nu*BD6QWW&kT7TuEYyI;=L>rUo{{PBW$9e?0kxh%_cbu75Z?#1epl?84Jz2>=NAE8_hw&wug3B?GYoF;>xNx=HO}9|bQ` z0^m?2j`IQigCb}B=$J6PfndqsHih&!p}{Q+^8BkDIeU`M<7Of}xxBRe&tJIq;xkLV z#&T#wmhykz-?{VKx9;D9v3UfB*$(}P&6_Y_=Z}l$qk8BRzuJ+iXVnZUqRLo|;f#0N z%SZ!zRQdaX3jiV}tMz~S$;;P%YW>1fVNsvCd~W#f-ud7yVL?%r!X2O}1!RZ;w(X)I zzzKmE<8r1=a6IgI)*1lokcmb>9A~^yKqp=Z)DnJOvY&v0*sExoB@p_9>gCCy@gmt5 zj>ZD0Ka|=ZHSHggzwYxK=TogcF88-hl~>k?%LKGN5aGSOy43BZ$PWO0L&ZP5a{d|s z`JG$$ZUZHgzz8`5pCC*I#9s8cD5TdFJ~yX=I?Jb?v9VLDEK@lo6%JGA9jOZt!1YpzkJQM2VjpJnVR(gA zi>@zN+$S%rUVe3B^ER=#BoJ0YID6Y^MVI%XF%u0u)`Cn2O9Ke?5h^VYC4Ly>0~>D@ zkPzW7b_csRu9t6pY^}fc)0fX(^W_)=HpXa3p9GMC#4sCxgsjlCZF0e2!we<@fo&YY zx)2}{Fx%*ORb@CY+4dC)tRbn~hZ6~{n+gJF{#OKd_Ki1!kH+I~E%i|efI2+kt3xOD zAHpZ+y!rZIw0C1#PJj5^>e?@^UwFE2sLfgXCRt^<^IHCk8-hgFDyomMgYvRMjsQzGwpON`_0|q;EmyA#Dy`_PGP>e zGu-}r=T|Skyxd*YbB8RY*oe9?gbWj`uGEbxE71;>+VD|e`nqc_e4}f@Wb)cJ^gj`d z01!Z6qr-$&C_L5c^?vl?%EeD#Jh!ez^$it2b7}2rc&*>PD<6D&Yp~<4Vcf8m*bu&T zpImyY5Q!9J2{wRX12&Zq5GJn|T}(+EQ?mk26^>5JKM)?31yBp8ohr0Eu$G8C!P&f; zy|LCY-76i_3r~7&Fxs{yFhGX5Adnp*bVOk>>=d66*iYIC>s1RyhSyKEz zCIGwNTVW(*42ZxwP;Ur7exZNiA6!0v4RfF|1naywkzySKkchAm5Q`MdtTT;^vSbwk zE?3wM54hI4c(N0ZH7nqxy8#Nd0;=4OS$H(fIbs3eJKTwXI5V-J)UJdAdH?#KUO9ht zy;F4coFPm3J9oG45QJcCw-q6S0w7^={xFq=&71I7CJO9*pG8T=4Y7$IH}eLAKgWL` zME)J|b%;&;i;7_)PTW{@8Mp_NE9@I&<1pv49Cu1o{jTfE%pjHgoWT=va zjV2EX0HZ8aLT*ZA~?!69+?1;l-AcAniFgfG<~&M=hpv@mGsYD zIlunNwf@EM*w?pq@4i1S$1c=oboREmpa6-8odw2D>+6g$mqMd%fryZ>&zZCSx^X$S z-gWR3Z-B~_7eJGAg={R|(}@GWA2#(@g@fFjOeZ(T(=pU4ARj*a91$X{b&AfXFP*#WqXKTR z)>m(D-WFI>|KsOZ&%L~ip zRBY;fKOJsB46^PG9ar4!s4ReZs0nd?^J$daf&BtFSr#Q~hz|As*oBpg@$bKQcl$P6 zM4w#;|Lmo@koaxv+5K!bg^SKG^ui zXuS7(o4faQjxQ6@=bl-A?&aVd0Qk-K?_Celg3XJ|y_NV{U)tQgD@?*Vf5YLk(Rp9l z+`au5gV7!{lV$PhMDE|AUpKjW6E3^FG_9<4A~%5V5#Cac>2X zj+j-GqE5O3ISoiI@bQ3X>dF%o?R;n?l}nZBSU;Q=Z8yi0v8MOfX+ur1FGO}+oroA@ z#MtvVaV{GoDeT74Og#$%7lz7CL1m|)fAQp{r(^Pe@y^!GKi=NkcKLeMOk)kLA- zXsY`-y4qaUxjo=h7uTNn$@L3Y*tj?07#}|Y;4kQ739ec7Xh_Bb<1ht4%?FPC;nNF| zS`fy$)G_J(Y{v!41N^(3ngRf1ljHpGn*kFgJGxjQL2QZJV6G5|O<~v+<}(-9F30hR zS2uTW|K8^AeI>n5X-?Ur{!qZl6vD$!5Dr<&H%8;VH%8;V)K5NfZuJ5iGUDeQ`1S>x z7MXFFc%+FyCu|6GHd2+a?fNzj;1jXKOJ>RNKsZzj;Bkg_I5&Wn{O&M1 zp#+;Nyp{aoULNU^ikdQq2p?YV^?&8^`KQCfLzePShxBf&B6I z-qvfo!(I60;2^tYIsNADa5Fr$Z>W&Sun<%^QR(7OjVd|O$O2#n78-wzPnq|VmiRzh z(G7uc0IU!dzkG4+TBvOSOSpf%e&N~AKYiteAM7vn?UY|P7hsWs7x2H`2!nTx>=JNR z5Db{P<;^}_xZJmK2zm2uG7l8JJWLV{mFC`A7Aej-Orp~y<~|^XeD*zAh=<=RHdq) zSkGOI6OJU+@r;v!ECA*pt<9_T&Zwz|9XAp#c@;)bA&_Q*HF-&N@Qc!{tm(-a8h6N2 z{@0DI_x{a$ci#DLceZcD-+y$ux8g?GW~kC;!`vtT65{e{)^XrX#O(Qj?=mmmu-QNCl2h8t&4Tfii!{v|MEDL?=Iz3+W@Z!{nl z{-d42)?e>WYQu3aFLzg+ZQokXIa3ruM6hG^2FB2bmzxWpgoF=?ujJRv<+dgP=^@r^ zG#m}G09Xi`dKP5;`2bfW6lO&1Jo}xGY=jYyjsMVcZzVXFbp7lXdY$F)ygSo!Vw`v| zI!Kqe=5K6lzW3&EG&CYoxK!T18jT0>dEt^>l+d~#S?(-pYLAY~e|%+WH9oIVmLsn& z2!fU{sDD$CgyldI7FKv5i-m_o761#v`>mqu*VX-h5)l^&v_5aDZwd%pEq`r9Fk~*n zQ=ad3me-1+1Gh{!2((rdoxizc1%u+f@nmS2?F>RzqSdeY#%R3vt=-|CVY>*2zN#%1 zGN7`)ik;{o2;x?jD!rsws+}y0CI39?JXPXfmWy7=WE)+m?FCA`oF!z9hs+ zE?IM(NdzB^CyllLex%9WtnVp&OK*$1M4*cQ`+K&kkzk-``PGCgr8h1|X4a%~N%lZ!=TK@q6 z_%H8myz}jy!43%c)6Lz@pSgVQTHhE?@Mn9S<$w9a`DfbdG-NLS;O^EPW7iK5n-a_o z7k~g1%xu`%;X*doEFw@2gAcZcKM5BGa(;W_J_qWhV1{VUOVmE4nTKOa7Ai}avJ_L6^5t9i-k(>eKiMAK|L+@{w`%2xuqm01Fsl$KO83bn z8x}4YjSATMxi?C?Hz=t)fjF@sFb=Y7@Nmcipao4c)s1BX{(XkVFU;XIo}JPvreqoH zPbRczKn-Nb^+!g ziUU9*H$bYj+`$54AR@vj*}Sv4xBGAZ^6mfo(%Q=EYOg3nKyM93gWX9vEu@x6Be(ZP zqhEjf<~!#)o#LgHrInSADX#Xqz1!o-crfMit-)xpGo6+skisg(+M0L(Hc~KLG)!Hq$$*;fB;W^!?lqFKo zVqI>PI>R6~bs=Eiufa+`Vq+jgWSsvUEWFWVQ))VXedVDxJY1A4EIV?p8_S?K7eQ&D zErp3-Mvm1USd z)2|Z%AL)0q2E9=54LR)9z_Q}%xlaqt4-qvUj)q;pHr#Be8G@klKj4mO#X*O761#PT?pu~ z0O>3`uOWjGL;Y!$LHsy(-5G7GnL8Q`)Igv+=85k$eMJ7p`QANZWC3shq=W$A0)u`4 z&c3+9B?~zd0TAurf$J}rYyMZ)K47;$tg;s=XQ8?!lr=@)HHmM(Op!lp|Cw0$-eduA zKs1B^(69!=vo+@LlzwfhJuSEf=YvdpCqU*dr?-f&en{oi%B%f0{H{s-=J8Da2SDrn z$^zg45DEdn3Ig}?Yrww;Hx>qP(}{yaN8OJPtNaHnXKtcS?d$3yjkNb%A z_en%SRHtkRg1~e3GNIv0pY0Cast^)~Gx?7W*#$TRgdH>%2L7B%I-Fe`fX12hj|N!) z95UjL+gLg~ck3iF;g1Kg0C=c}5N3d-vcb-x^2;4NV^R2N=MN5P*>62|m@5J`3eP#) z85xfZ3lToXQ7s7;4Cw@mIwsG^$jCS_W*e5VB_L=rPJp@qm?uHPLi~Q4!II1ltB-U=NhMHSpc&NsJ^R~Z9;e~*l9|iO#FzFC)ac4mv4^(F8EA&Z5KGl+WjBNF;Ccs-h1K-#`D z!SXE}T{4#R%1uUHNvC-1`QAZON~C;!y};CM6VeL^+(M38L2>V@3md5EaP?L7^mraB z%Q&+TMAIVuA6`$A*w?1nm6k;cVL@5502sgol@LghJ=zBV=_I}gNgsY{;bQ=W3IKwj zC`&fNY=rGgDj))Iu%6r5%mK*ASO_8@T`<a1;7-Q6`Pvq%K``m!PK2Qfpr`FtacJeQI>39i6v~9?F%wEj9f~v-~$rzfsBkZ1h4f)bTJt{ z?pL3Nzm-jDe6{y=_(jj zU=nQMg<7Q#GT}4MHrlxH$oLn|1xQsww=$@JxC@$~0vaF;E5T^j_6;GBwSUGL$FcA) zwg4EFc|xw>u!4HAA~+|MQd*I4SRxK8W};_gRFQyVJRm2=P7XHOSz_yCS6=|jr2GqG0C6SM$I#)1Fo~h9A@Rj$5x0)QwxCNFQWoek|+L6e37<`XVPb6R5(`tkxfVT1;(E#$umy^wN<8e@vQwb z&N`?`==k%<`4@BXjOA-s)(k>;KQY@3JhqGY>&zWd)t6oZI} za`Sv%^2@+BH;t(xkj_bd5m+x1fs?oaQ4At35@rSGjzfMzQ3PZltx^$iV*nxY`(zg- z+d||QfwUtcB4)!E0Kb+uzsdP8z4KflD2)>s0hmR8e+h{}$RdD~ZbU@Hd?>R3Byyr> zP@FSXfDr-c{{kf<;93E`1rRd*-6aWZE3*oKXaz(>92dS1FP-%axd0e~XPB&TfpQT5 zkh=mYCTr?x#813!i9X)nE}V2-5r~M0n2nI{pCK0jLwZyDXU@4UpC!T-??en>TJ7(W z-(Po^)cJ2jA*4(Z5fSsjSOA9jW4%5?E(oE(yYK5uZ@YB`3|Xg{{XY3q6iHtR{CBMb z*Akeke{w{`1&1H}%b>_Vh9ZCBtOGg6#+Y;=3ds=eg#;jU za}f~{MYt8E>;I4R(uX3y%hLgh@dAG_Jal6Mg;s!hqW@+wm=%o&A4Lm=_Gw;vv zckcXIL_|c4fTI^ueDymCUPJC`yOp)Uw2nhAzpWMETL7W9e?QP?%zi`srn6rA^avmN zaE8yF?jwkZxY+oG5#GbVum7nM0l%efTK8bLp8&k|sV;yo25#+NgR1eH&eHhV5kBzY z44*ZbzTNBfPW357L|ia1fi~)mdJ5D(H^Q$-;J>_-;0Gv@UpoIytAWd3f7GD00;UB4 z-S&M``==<@{?-Ec)EFOpF2m28#J+v==H~U?-Hwow!OVi0!2tPTfEX2sxQGaZ#F+>T z0uu;CB!wU*qW4_BwEUrstLrIH{|6)da{ym_DZwi!b`Elz3C*?yMGL@h5eQlU)A|Fr zfj=t(pBm$X9~l?P|LXSkej-`6h{Q^@S|J9AVaFlFCV_~Hj>)=Dk)H)2W8l_cFl0gE zmF=B<0B}U$pAqmI*Al#nyoF!{)k0vJ3r!yZzq@cj1dcNnz*qoA1Zn_kKRdz){F?vO zt*yN@ld=V($5)n`onC(+Gs&v3?hJ-1+d<1)*FJvjl-iJ z5*Dv+?{t7%1pfXAzeFk~a2={59zyYQh{-Pc?oxo;6?mEmxGey-2~Y?8tk3`Ln>ROq zu(h>U8I7|x5xr|=slBzk*F88q?1RC?$^f7#6OpP6%s3z-;vRt_^^9YP9C{K-BJ>WA z`VX&NUKNc-{f(oe-WY%{0-qbk4eP`>g>ffb{t> zo_3@Crsh}V2R**L+}_^VKRE1mdx>NgQtAYmwcM%Sk?Ro`5NvdTp!|M-j1UMS8fV$) z?OWS>4{uyqd92xNym55YV*syAiom}-B>d|41&*M&X}DU(=>q&oLEVGgW`+qN_!}8M zU=B1se>%=&n}{A?UT*K~?j0N+bbIMoGBHCyO1JhWBLVl30Pt+FzwZ$ReNGTr(;v_Q zKvD%rl2CDgevjY2wYB%q)wR{fn$5-!d%Zr0=#`yaD*_47_{bQ4`})wg|Dla5tB;B% zz258hNibdC-R*p6F2Jw|4ishuv;3%_Ju* zr5H;#$5_%cgY5fdrhDXF7fJ69tu8zM&MtRHXP+|8_#Mqm{eBUIyiAZ@*O7lB1`#-8 zl8KOlCFuY@kNShrTU$GO4{ofl{)iBb*L%GIQ{tB{zs8@a?L+wqxd5RPrx#^reyG)I zYQR6hVkeDgID{Zifag@a3aCy$Ql<`&-KW4c#_?QC$en-4s* zi7ks-R_1{&PXsV4vtKpI695Si5wXZ5$waVVNzBB=k_Us~_{Ppo=dty*RU94l-7orZ zv7+|>hxluMPkb08bdZDK6Cwh1u2a_?N!m|#v80D&buZVzU4-o*C zVY`|HgQY%5>4J3>R06Ep4BYZYle`RB@F6RJr&$1V!`9~763~L6iJp^Q|F`RT4+wT| zqKI>T!G5iu6Q9AR%?5`0%Kh1y9_R)Eu!pGi&)D@`@^%Wx_J;6oVVL%BG{?2LI28vY+CjzW0?}_bB`LKmc03<<7x_bfM%HmvZd~lmO?7VtyOn+1k&rt^w(8CB_n9ERG2Hpu$rihK>K58zE~= zs=Y9kat1pwUX1X zlVE#vT@?8DTMz{QNz8h+)_>CUU{+!PW|C?HscUFy&`rw=XbAvt_-n+BRMo+{kP$H$ zB*2iGFQ(ox=BL3~@FFAtr&4&yge+l#gn7UZdQgx{elPHIZeSu)*2u*_zClKvirFEeGXg&&jo?ld!zR0IF%dQePmmSjKih&vo;Et99!Cw z-kk6MUjP(3{OZnr3@rlM%v24&q?w0UbISPqx{7w%>k^kOJz9!?LoRgo- zW3{$D*%WH3)H9a~Dw#Fw(1QvPi&!j8^No0b6)*{g9-4W?$#Sl)h5F96;CBk4Mc#}Iz zvgtOpl2bkm0sQ-uLf8J%sstMsnOKL>7lQIdm@;jIK%4(L!IeE=Lm6jORg|WBUII{r zC49c=ZjgV{S9fZ2OLV{?KS|*VfKWnVT?8BIp^4bd30|zuL!N|f5<7+1(CI#Hw;g2m zm3;G7ozKw3Cjca+6#`<09!}2MKFiw-oE8Doaw~Gz0F-X z&PD+4u^54L5mvS&9ND2WVC8PigX`M7p8!|z2n;{iz zTMRd#fU~RRPlWTb00Ms9f`GhrvhhhmPIBnlKPSH+WFjFZ5=sDava=Bkv)u_Z5L+U# zLue81hWJNma}lP;uX`dvtUp`zJb$b%EmROj%-IZ7xB0Y6Lz0?dAQ zGqaF-D6G8&VIun~;-4{uloC=x2qgk+pRe}$-SbWU*|yX!9E=rE2!NL2eD2|l1%UFB zzhDTO|Hz4)2l-%Xd$N{+A-^DzCBogGr){hOkQIi!3$*DA0l+j5p%7ot{STZThq`?0 zQI@PsX5$VUvQ8E%H?4ehrqIo5OvP%wmp zYXgJDMG{d?{zOP2kR>EI4+a8ICM&Ek=#9)wk|ET8CYDT`*SJy;v7QQA3SskLv&QCJ z$$GkVvnMCI3nz}jAfg0$&rB(qgeydv%z{Xg$p`F<nhm>(Y9fRM*qA;^ua1lnsB_ANIbr-`($y zvoQdC{POaZW-1bM1Ba5cuk9Y}K2%MsA6;!IwQ=W-%wO*-zXt}kl zO$H_~zP)$2d#g7X=uR&H5hMwG{L1o`W|AZX75M9q#-l&DxxJ}7XsFq8GGp$WuG+d$ z-EjjPL0il?fCB>9^!vXXI4cXl31XSrso1}H(X0Ri6`dkmz-9_$D*CN;o91s6ZD zx^(3;tL=6F+&m4mo?U8O`tf#sXjann*B-n2q<{UjN>cf~-Oje{f2U(2pSrw!)jjUF zb`EZn%mw9Yy;}Ly<)y+guk}a0cLt*ovDD2P0+K-LSytQTxW7E=AG~mLcZ*3%r&(4v z1sFHPb~Rnd^0ep(PVjL;ln0^~6<+>qTYl(3(>y4MBne`IGSi(z9;v4F=T@4_pTDyF zU>Sh{;PY3OAAEMDy($F>DTH7lgd`HrF1J@de|hPFY3Jry%0=q6!Km;FwuYeOiDs?w z);Jq&jkA$|>ROsq{?!AQe&pTFMl%yaNK#8ifd3Na$Xx*{{sv6T0C65DEVu@4I=^^n>4AB!_cJT4E0P3B$_w~sS6dtYaW_We{-}N3EDyi5vGRx%f@C6K`Zqk)sI}Z< zdXlqiM}y95gHca6gPMh8Xv_nFjEjIg22g<1!BOqRlW8sSh1Wd>w!y)T1C-ra#Jo7y zjzA061nR&mrOq!s7@G?FYyqE)&zjov#cpr!|8@KO0O+sPEA>yWwhQEbw3^m!lwJQ_ zOOuLw%-6fUo&T|YuxWySruE>xwMzYOHtQ`#FV%d0?P$<>x>;K`=l(>qw)ExQ?)Dcq z_ilc1bMNLu)k@`;H&!2cw3^n7g3d2YG6@?z((qq#;tslL`Z+WD=W&K5B<2@-5O z-vJPz=Er=SS`T%EAkqWj?TAaBG$!=1Iv8uZ2vZ>))u2YCXV{BQz;SV^7QlT4Yibq> zv}4(nOWC!fLFd2T+I#z@!`|UbhrRBXcMi5*?DqHkykj`Ju~?|KjE4jVJ20hRnwxkZYL`7~cxG z0UvJFmjJ^W#yi9D=*DPV5Q_CQshGfL8Cd)5|N4Q;k9^^wwa1=ln7ovpG8MY_LeZ+4 zhJXl!(~Yb@F3wE=F3QX4NzUFGjD{*oZ9D96H<9EXz2D*7;VAA;vs+LxYb z)|TE^ueAQ*<)sZXHsV-m`4EK|!Nn*b3x;R`xG+&jud>udb%c@e z;{MV8m$p0alpVR2iptNewAO$9k@a^!x71v*1k~5~i~g`+W&C)fw#3E_FE_`ng;0_^ zw9_R0>`Kdih30*=O7kP_#?naxy~;H)!+4lX*S=D?eHB%|p&%Bu3-BT%3cyS_L)P@j zG`&6CiAfD?(ayPJ;!h1w=};FlzP{Vt`8PLi|KP=LZ_h^jJv0O*|8#xj;b&U)mTsK{ zG4r1t72S-6pyWOEYC}z_qZbcm)+R!@Fm##a^GnSYziIGuE3NgDS^!ro%I(No6?6}w zK;C@v7xtT7+veWe z|Ki5xUw&!l;GOIJ(PaDpz)vo>F0sA>N$|D9{(&zVKiaIdR8u3Noh0IYwMx_7zcn5Y zi6Mz0;m_U!pes51n^vRB_Q*(}5lL5+v=#I1T*Y>-+ z|L*4Y8(+A&`}$TU3qiskYu1(pGX$6k5`rbZ@3{d#(X1`$KKt}syS@w$uMb9jVhOu= z`+urcUp2M=SN6J_|9kgf)9m}nR&B-4lQRpn?!Y&B-|uhMmQ^1JAy|;egc=9X^sEeR z7%1_yPl(NRtOXW>2PenH7J$jx#D1rsKv-S)biZk^B z)9pt4*`?+RlMqZone19SUL6ekf86a&G$rcMfp&n=%U)~4qm{J2l!&yQh=LyvsDS0S z_PhH+O163Q@@XuFp5*NJ_qw~^J?M2`)cJU{lGfJ}QRusnKd<~*_`BEAr1DSJmN!@c z83{0mo@v!vZXX46vK|%;A!RHs)k<1@eyMrc zJ^#(Ys7F#tlRrTc-#a)u_(xZkA9RoV!qwGB){>;+`|;n{>uz;N*+`Gb<6yTv_t@ch zL;$|J-MRhYc4Ia8{G|RI908I6Q4(GJ_;T~gQ}t@=<^J$+BTXu9CnkgI-TndTmK0-! z*-1mST!52xdqk2$o(@5Wy+W z2mtBvFf4&*n#x4Kk|x!UEjO=xY`J-5THaqeINHlUxXO@%u-P9D-W-m4k5ttNKtR>h z{<&`sM!nzO+`X0P;k!&k5|b_f0PKvj;a6_&ZKgV!vDqK?U)by3{^)9Z-9Oe?A^;!+ zAY}2rx5wGADMZo`lssBZ>yx#9j+ghl9U&!CrsqCzk}_^qal1FY{DO}JTm)Qf7eJ#> zJDa9JClE+ZK_h9*(u{>A z)?n28gYEq-B{ou$`s|t%nUvQKCS3&=ul5Gr|M>dN*ODw_k!4IWQ}?4j>pqdsZ)|sN zCu4Q2$TAk=jDP3W?q*L;_H7WEl={9{WKxnWc00J%?d|{O=HA=0?(dJb_HVt`IqV84 zRX-#*wxC@vJAYuf5g}p@Tv!XBY?8)&52;$;%u~nen}<@JE0atLAS2(x70fK;n1zIC z<+gsT50ObuvP=TN3!8gef|=jfYAik3thF0L1jV2yW%kYO&aH25?Qb0o$C=Qzo|&bG ziTLIH!-K!)xdCqtM!g$@Vee1&5BI*ie|SLZ%gy=S6GKownMZ!!u=m1kl{XW|U;_6> zQhsy0v-vaYtA*<%*_ho4L6WR^<2!@l;9EPL-J|hn^qKWb4?bMWEfoOhOPT%Q?fsiy zeRK1j)QD`BDdBb8GCd1O-ZJ3&|4|F(27$Q&Ab}DCvm^j2da3E91z<@p?Oz?>-(F%o zal6;+{r)SjzVT$c-Q3&T?~V?-{mN)8lPt@UaVC>Y)dnP)ZOvf)I9&+&?wD&v0PM&z z(xd(CtT+Tog3Lr2W(+(FflLT9B^Ve&=)o-HnW1{Lf8jc$8vb;^(ZsOq;39SGyW6#9 zJ4up_YNgsrMDqI4pa+1zyWc%1&MWAWSmnXa)&s9)&0071x(=yz9{qU?^&GsWwZeQf z33mFh{0amrxhJnPugWBYfCp>U+D5HX6;SH>y}g42Z58F@SJn+1*GfnRnRTZu`QMmW zL>ItJ7bjKcEHdLPq!4;S7_&~4B?-wyh%8OSFs&rxYMMOp@I!0g+uQAo(^UN9r=EHe z0Im!C&VR4se*x&|z%}6UCB*2gW zGb9VCVrNpaBpoLtK+jmE390I5HJHuB7!A0P4ZV$t@cRCdKa7`&8HuxA(iF+L5affG zykjS3_|<{<)#_bP|lsan{@H_IdxgxW00FrOK!Q|7DXs0zX2K9}^q{)X^j^5;aOb?q z)|YM{13SSgS&Jf? z|0ltP761$7Jr`R8Ou+&GsRH?MMF0#T^FH>r&%GMlJ1GKXSDsaWIyeT2&jiQl`yU?r zFfA(1WTyGd%6aWyd@!;0pC3^G92Bg8+}l;wfF}a^j5KWLJT$KJ zr2Ty7ebw$Z`CXjWY)`%bxN!!jrqBON_qU1shrcfwF&{3x09ZG`vjPgAaCSt12qZ~a z*zP>_+Q-+w-J)1NQBB!+`tq5Q&szn`m_Ez>n37^K6v6H`Lv+2)&@C_U#lSq`K;%Ex z{>Q_G7XTl&NsYblxI0Q9v;(VbQu~F9iH>wy6heW%Pmsh(Ywfu&cH;XA#5HWQ ztNCV1-uus~`5kK^;^c?|fJrL=Ol2aVTzoQ9@#*7@^KU=@tkaz=ouT4-j3~g&y``9| zqs+-I`+4pwe4JB@7eDn4>+9$1Zf$>WH2W<&q5x>jDFT4D>5UF(i&RZs%K+ykdIni- zm0WUENnyT|B0h`Yr$V1u_XQO0&3RnT6#tg^ZqH2J<=MIok^Fa#C;$%1MZoJBqxcTp zV^G&ey6~Fv?-WN4Qg8kOTN*?-E>4*ur*>aC@of@R(9%hhKazhc=6)Vg06h32plWqb z2yE@HKS||uxHF&nEToA-_kiPLLCg1P-d8RL=C>Qw_iBCjxJdqaQ2LCb00iKQ01y@e zpuqe5Tv+3^h*K`7#~J)`rW3NB!hKE5z2GEVsPPN?Bl%AQ^9aD{c!u{_BW+=@P(TP2 zbbasLT7Ivm{duI575LM6_Gk5}r^$ON4C#%;KP?2GSx5ll!__baZ9)hf@7E4|2?xFJ zqi1>c_c9?=fh$JxpDv;cFbCX1=Zk?orcgzlL#AfY;5_nhnwWAwk@RPWC;-QVUl@Z^ zV=qE+U4%TG5Rvfb2VVg0=)un#t#+?fO7kEV)`-)B9hQD_aLIb4v9o2{0xZ$3Rg4W+{l9h+Qu;Sc1LRycY6kAw)zd zgm@GhCplRlT=kVC?h=>RczbTn>m)UO1$3$q0+s>B6g=2C~x*{+uC};(s zOeOp!GmA4LB%80nkQu)X*eRaKnYt+-<;Adq5M?3a0;1U45rO(qPkXMoqj)71@kvjT zYK|0&^oE-f#Y#HFW6SpxlPVGN_00v$w8A4Y7hv`E7h;n^Gj)@DgQmM&aX?IKi~s-v z!bwCyRMx$GLdR$wL|jlXio+oDAGV$(W(bz7zAeF=uw-Nf3qV3m|7}DdNOo@>05}co za7pj}H;E4cBt`%Tj3mp{mvC4zNy+MaaOU$^`hyO4`k*2r?pLU}B^3Lu8(;>RXp32z z*^!xr_pudHyfpLw8!NzYS7MU*7Sk*MR|v-X*b$W5U?;jG1}9l2)rUV=3Mm1}5YmlY zvNOR00`Y-}hzkT;>#GT9ZQB?lpO+GfBZ*WdwIHHIe+V`iXVP^UQpL-mUW}FQ62H_1 zATZaUNS!!~`*olcK-F1l`hTi#LjoATH{<%IpzU44A=DR120A0IY2y1Rh0Jh_2t?e+ zaE6cTP#J`?%*DT_Gk*A5LLGSggW_8cN(AQSAAz|V$bkQ;uoU^H!VLKnF9WU$nRWM5 zM1BS)nk}LLMBL8^2!JaBcZGlMSb&h}ZMQ9 z!oL)g7j@Oi{rt1r8jv{0miUfQL>h=_;_1``RmtNil?z|Vw>#LI-6F}IbV ztW9!Sb3{bMeGdlow-`%r7k2-bnICSt(fp6N=(ubA zO9P2a%bYPesARu?;aUEFM8rjf&w;zbKR*aqmJvS#WtsFlAo!h${BvUMA911JxB-D( z_W!x@&+iBXeXwvYM@aFw1VlzxY(* z$J#&QA|hY~+%^6g3m`DNeIdb+^7!ZY7b&}VBz;6gj=Rb~LjEbM(EpI~%u=AT%oHvj zYyXIg4w@vi|NqSS=L&$Ic$wKTEd!xUOdJ!*9}%HYA_8Xp%=t&GM(O#9PRmpof*14F zBl#mDJlswG*||7MnkMGmR_4F>8zT85A_BNu{QLj^vkvCp*E^&D0000ky+K#kLr2P?#{kEwsy^e>iYJ>B)_%KSbK@yLj%$f}yze$-6IPs+~htgOha%=~@th#(O`8wjBE?&Nmr|F(S@ z85z@&BBo6FT%v#4iS2>f1OT-YJ+*zWLLnm~qaD-8o!U-*K93M6;~fdDACuf)&k_Kc z{23Ydk7;CYvk=IBX1P2~5Cj6CMeDZ_J|zI=u?R9UGG?QV{PA```cxYPP@52N0$>XH zQ>0HV@%Q|7+-^oj#w6NI{}k!tCDm474WxuXX+QM#%|m$AvOoU5zmAcGK}JTqc|I@s zIkC@gW2z7+=On*MY_|!4`*8!ZFvvJbm=&D67xEKAA;5{YT7`fg14xlSCcA3cmm&lD?Bz`__SP3CvZW zoRM+D5eNTjX!2Lk>K1Z83m07obF_={o?H0CdgLG3(O%j^BPp zM#gl+O44fj3c$WuIsitP?qeY#x3dCb3m}#Ij{|+y?6<^U@wS&*PJM8UkH1jkSs1>V#kam~0DfAL9=!F`AfTV$x7xo%sQsM<@U9w9 ze|U_KS@M7L%{Ome-QM0&%mNWnBBE`Z$P>tpLB>f$A|x(E5D`Ta9m*B{Ukp2Vf0G3cahw z(|;I||K(e^?hp%inXz0JibOy{0y6?pP6=e3bOgGO^dBK1A%zOS9y5y~RVzjBf8vQp zuJn360H}cB7drUtO9fsAunSVN6cBF{r7 zVj?Upb<4*uoI9gL`nSg8ssivI*RI{f6Hmap10NjY;{<&Ae+qmbVIkf=G++Hn!PFwt zN`dYEI2%v`DBm4L`>p2hjH`N$KvzzmUK{iWqm5fzI|Yjt zAuSb{6p13#!-2ehWApaqM=qR^!C0HOw6S#Vd!1h6p}46bf(?-VS&N{ZgLzP2{l8w@u#wsurqixLsVEW{!N zG(kWSPYA?!Ckur($ZU)9K#?u9w~EB`B3JAqTI&-8qW@O}B2g>~4C3)(1p% z=g;t9IM}G`nv~GjU%!6y$~%9i2LMZg6-;)`Pw{!G3oz~KrUF+*BdlA;Jg+MV!kLIe_l5RvG}I4Od#(n5e(NE8XNfRyIVXn$M-012cecs5JpCu0D!zTj`iW0kr8 zT);qBNBT9vB_=CM`0 zJHg=x1Yn$Igt^3bM71J3=dE9$NK^+4p|5|#Bx>~ zEd)eR5I~5SAVfl}lvRbsNzhh+8%9SU#I0T%FvHYu!+{8Z7|IeK+QqScKZ%H9EhvJ3 zAkg|=whD%a0FL|w5*Xq`Y^4M_F;4-lJdH3P;$!E@Bm`QXJV9+OSl=~hghGzzzz{ug z56D<`<_`%$6hI6TvJc(%m=Pfm87ttvI55ki2Qa-3;9en$CH&QDHVGQrzpq=QMO?z45W2eHpAm*Y1V?rGN^@g}-M_CA@pMe{I~Y2_4#A+OY;JmubqguM##MkB<5*ro(Ty#qguZq zhFP{qq;VFS=34(roZ4@z4J6_W`{q#d_OiV?SkqMNE1(esrrP5q3xOD+2S)-I|2I-p zi0l#iQ&{DAC>bD&!x8`hoawK(JtxRGD<1j7T7ZYy5{NawecA*eQI=KaWw17*E)k91 zp`cA;KF(&`Eb^y7A_9W7L`3&h6(BH;AU!@Lk$7!W~3+l401t0Mwr zY(>CqllT3I%ydB7f+M=!Z*7f_7^1Sd)v(IB+*HTV26YXz;YZ{ud_rM>gs&iV%>$AjPC$ECPxeL+JQV1_f+x8E7mJy=~(PHs=Tc z0)`Uk!Y)>|sZS8*0uV53fu$4_GngRC6b)Ecz%-MsfCIo`ro|6U07y#f`+?;X^g{TA zm5{nd6FcqiMuuU{M@RmuD8QtYs3t}L*c7X{Qd04(3jI0UXN{0hjXOb2dQrTGV#~Ib z2!U8ofg#%9F8~oRz}kJWfi0Z~)FRDV0lr|$R>0z69szKm9*ge+**cgLi6-_#HudMy zd{!_hf+(V@D8Q;vZ|^Oy{#iVmsPvTLB}jef8eYDX&O4Q&v*_HAhz!RIB-}NfT!`SRtxK!Pns0~ME*a_&ky}@ z3ba3&EC5KYc8ktmU3u(TzrAOcyKBF8X6@qVH}1Ut)vcW^K;Q45Utc-<>~eS29{2KK zwELBh6zH;t&FP?tvUvJ)i<7+pzH_`r2qb zNVWAFXV)&UQm6|>b)o2dr|9^f_gQK#6iGqgLJ^k*z3=SW>5pAH`(zvWeJm?g{K2E= zp7^B;r_a`9L0l-XQohULte0ktJ1@nRv9LHu0T6UEJ?g92Z%G7 zH_`=@-`X48`L{d$&8x%lp5OHOvgooR#i~#rINLjSzAU=_=ieKQ_5?%>rI?gjEsM@a z&Y!-Vy3DJ?@!r)@*uKT@T{?4_iIj02R1-rm*X-9O(8NZ@0^&#uffg(Ux)|^U1Honn zn!Q@13IXz^jXRrFr&Q0cE%!cg>ddO&3@b(dc;(UO;^wao_xHYhdv}{m-2f|v_pYz5 z$D04@aJ=`Ko7>lSE3N^-TBj&J`N+Aap6r%xqStesvitm*wcdAc?c5PXf*N2VDnPcA zlc{wJ2YB)0067r2SU6A%AY&e+D*}@ilQj^GpCG2p<{jNmF#GaL{n1WJv<_J6f7$78 ziLEG9q=cY;cDcJ2AN!db+t+r-^;iiriOBA_uD@`5_hx+D#cpRwR1>rW!u|@uPb6fC zzu?FMV4>ir3&|r@Q{Q*FTXuhIedV10t*`C&Z-WG1y0f?O({VMl`wW zKQ~@Uw&PC-vH&UOo2NsvLRbcA{TFxzQArWj=p_}mBsqU(Y z5r_*s+6=_Z-_mwWVs{oQ3x_NKjtQ1HljW0e{=5=~@vK@r)my=2pm>1(&`HG(QQPxu=%78l1eBG3s%+z;GV z&c~zeIO^C60NpcBUg?h}N`e3QT<=i;c(ba;k95k;@2sz!kB|9If4FOlZxn#<5B7K8 zy}Z=(kNL|-&OZH>JA1cY8;%BhHP=^%<3WnPV(3jTF>uyV;&RJj_GAKgh@~z!AVN zeGS}ne`X{{BG-rCOd6{vSXIoTnECT>ZohGDKiw%P#(<^%;;o$<#1LYzVqvAn^OMir z*#7B&B^f}xSe6}P7R8f}yuCIB79}=qXo`o-cJup?je`Y8768WrF@-j8b0aE|zzs0k zAF5_;1+Yj_v-HQ~@!$UV#%uq5x4+d!U;y~9-NE)>U)y~3<^HIzghh$g+KO2UX8uus z*#EoNH-G%Y;r>Lm-}$oW7B#ctn&Z!uun@CQ?FSb1XJ!T{_q!}k(%`QT8nO#;ln6#c z(JMVjl9&L29}8%<6*EK;gKJ1FY9KQHhiebLb2Zjhp` zMO7>XvmOzwnfK8wHialQ<%qWIMaE(v3xFerj>Jdf!UF=KV0x(7w=U`$Kp`Z|b)f_B z!b0D^v%4cosh4)ut|&W7r&pn`dlG2k8=qd!Im>pOKp9lb-x-a^dS+UN5=beoYo8r* z48+WZb`4n7wTS~Xnja8vw&&=FXECsl$O7Pq&^9i-F_0;31;AMV3Of(xlD}TX>jo- zc7>{q=C5lj6htxWwRmkb0Z0AYGy4}7SpXb0B-vv=wgNQa3{P1#f&c(S5uu@*(w+sO z&vos0^f_M^-S_lXPXF^@bQ2H|Gjw22TrkgWJJLHpUWC{dC^fS>D-kK@>+k?fAMaA+dt=X z%S&s}!bBRa?P+eatxbCtRfp?>nU(#FO#raTN(6#na^hF(@*Leg&bT*Z3*b0loogXy zhnr-e8LVIHV-g|7FynEvKq-J3tQ5@nyrfu|gjEf~w|DmLf&jlxfJ_fRpX}C8Cl)(J zyVLOGViI;{Ah#_+JiuSG|9@KhXDlYN061c#Vgb!?K!$J(Qi_FH0ml3Xg9r?zlz>?e z#cksPN=Z@I0u-P`?p|y2+G=l0d`(=pPLaN`T0(Olq8hV*lKctnpUwZpL>2(Y1>XuF z6A*0X?`h%z3^63v({X_TrL+OSY)gyvP~CtG$5r*C{^+h{+qL0nplW6kVLj0YODF&| z*_8?2rATkLA-Z0{Y4=w0XYK!>APazFfU^PuS3rcw2mzg^6F?gPED+V0{-nTS=k~L~ zL{Y>L(5pNB-A~;eylkhMAQmZDP)My~2U-9G#Mi;T(8RabX##?_#8+INKiHlmf7bpF z2C@J+ZX|_(D;d&4fI+OZ5YVPSL6Oqg0R}(}J-Lk5@ct~qhV&p2Ry9u$-}mK#1nGTk z?^Q1PWz3oU3x~Pyc@_Z2gg6%9dJ0+sAt3|^0zES?X|=71u872r<-wwKb1LX#AlAfp zYYzQ)zy<;lYI>ja!M>3A`Zs0rFDP1{LlywX2;UX3B|$WD zv0raaq&HgVor(WoBNhN}>0y%hE>PmV$iu?VMl1w0N&i4#F2ct+EEmA@AHBgMlSgEv5C{PzTmr{y5<8}-3I9#Z{gEa;;Cl?I zUVEAN3ykTHCbk3wlZ+D}6acd%h~J3YZZlYtW5P>l;A9J7I)Y-M*d>?{1kJaC|0V)= zvwi$b{G&&hyD|GFU1}5Qoy(nW<8#32X%@h=2Aa=Y4>A8m6Mth;`sCMZZdfwL)_@fR zuw{Dzp`+f0L3{pj$i!bz%v6ryqp}=h?|*-PtNon{0!~PYXding(iiKejBIyQiGM_X zx!ww5YXE3ce~mD(NZ;$WG+rkDF~P)mEc`JbZ93O;-;BlWsyQ3Zh2-~zz^tI4698>e zB3^2{kh>t5US{EtP%Z!^;=bKPp#|)R7lW*L(`fxvVuu@`82nR!3f znVaEAcy@n&5$_HD{R##bne#5@`@KL2tN`()xYLs)x|gRO6H=rQ7Sw?SzyQW*gg}z) z(Kg`40NP0JuWjS|017Jr2!f)nSqZb9G(tfHU~kaKTmx`e$Yg#9pldla`<)wL13SiI zW^X*lnj;q+HM9Vz&{zSMyNpTVJ4~|xd_ho|W5>_~iIQ^x1X9#Bn}I4>F$1hev~41n z5{?%?5g*9NI6-h)zbWpr``w7q&>SgPo79Sk3T7q|6jjZBn5Wdd+&7E0(tYAjbpg=w zAxBGPPCx+gjRK!t?+-4I#v^s@smrgecDv_qpX%Q1pXu$CRkhz8?eCXW&7J*fT+|hp z<6gi@O&DAGzAgsrvtdQVN}(ar-RPFYk%p{h>Vsmnr*%d%6IMW-%{PVaQ@*=tM1bs<&!=gysX%G%1twNB?% z03&FAS_{CB2gI&`13{zW;>9$oybG`a!m+o+7R0o`L?(a6NyoA9uf*g@ed*-S zKI>-#3hx--MX(p*rzPT`VkUY6ww&a&bVNqRLk|I@ z>9P@2{~jCv+Jb=byzq8EnF~SU2l#=9wv)%i&*pzd#xe4*Of)j>;;hL*EuY6Hp5^~% zWSmsQ95@#KrFsL-l0efgGA=-fi;3dzWbL1EqTsm!iI3d+$oLl*0s(f~$uLFdOP<(2 zNlBOZVzLF0kuixB4~~?7eheVK24`?CT~r&ACyTZ-`7<&i94G%=ck-6h#Jv03 l{Fit~CVxgo0>{O_{|DQ*aKxLO=;8nX002ovPDHLkV1i~IG-Ch& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f96595e9f104b8b98c49d8d8eb913c0b405324d0 GIT binary patch literal 1524 zcmVZ0)m80000$B9s6CfJzWb002N02qgdj zpbE$LJa{8D?|FOx0Pth<(!1Xovsox1fB*pSaYfhpt%J%#p=3c2A*cWV@G(pRh|K|8 z1eCi%NhXXe-3I^w_HPtIQZ{%boU95Z3&KclyR;7g0My5aize7N`D`OK_Xd+@>58sF zAZgs|+`io3005vaJ{A^0Fw#rvGRG

0U_5jk}4y_@ruGw(!^Db()7*2 z3?EPkCIA5J!(1RqK_mr_i~GF4-lyX=lKQzRoTbw@zG>B0M-JCxwUS=kemk8b5k>$2 zs9g#UY04%Ak~GemUeE26Ph(!~dqrz+3C>kXY#=$BS6@LPkN^O%54kXM(r`UL>6RmG zMa5=??QRJ+O6!rNaFT+^s~ImEhZY)M0RX6%Z*c821d)q|5b~z+_4}l_RU;crN>ezK zP!jZ$l-4Tojjb;l!pJ9$!^u(8Y6+nP0D!vKP?D-7XANN_UB}b=w(<2~{x0>9K6Fd) zn#I$Ox=r1rVDd@QPv$*eEdc-kb@5e_vu@XLX*`{CQQUiKx3_!tP4FXjpM5WdlY^w= zpJ#pkrPCi5>DWtc8XC@yPyzrzg__%9XI&Zx>D)@^zhCu5(_Ycd)+Afg=sIV#aN4h( ze*dl0UmG4D002DKSIy6Q++BER=RYE8eqDU&Hm}rcNLH&lZUF#5_3X92_;lcVG<`q* zZqVpz*BMFWn$)|1>IjXSB_;wRB6Z)Xib<#c_O+0WfK007j_U*}WMcr)XqY4C_DC#7miXc9{L z>oTwlPSoBmlqxA{K^2)3eiQ4I006KDOV%lw3nsaMGHYq0tF4;Y6K%}N*d=Fs^AE71 zD*yodlUva>t?GK#c#0;Rs`0OnKKsU=oDU=^e54A#St$!``l=SYP_dWZ@R7?CV z!FHZ0?Q7i$+aQuYw=|RxS^xleUvrggc8?V5;eWrCjds%G^J@9f(QTrb;+$S5(SgF5go5+wo(!_ zz2<(vPSZq>FqQ}+008VoGzAldj$9Dw&6hWAbd65X?~j5Afdl}6{VD~M+`iHGmIe|j zcpzW)gXQTLA3{oDgm3}?K;3fFICgt(->N_&tq3LGzpyNv`0dg@002;*B@M9I&#nz5 z(jB2h3WJGxtN1qn0NAIwkdk(+4;EKzdrC$>7=)9%PXqt}RIPZ8e asKS30@KdY}cg|4&0000 - 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 @@ - + + +