diff --git a/www/html/Game/public/css/customize-popup.css b/www/html/Game/public/css/customize-popup.css
index 05f31b0..db0cc0f 100644
--- a/www/html/Game/public/css/customize-popup.css
+++ b/www/html/Game/public/css/customize-popup.css
@@ -195,6 +195,8 @@
min-width: 0;
display: flex;
flex-direction: column;
+ z-index: 2;
+ pointer-events: auto;
}
@media (max-width: 1200px) {
@@ -347,6 +349,16 @@
display: block !important;
}
+.customize-popup-color-btn.is-taken:not(.is-active) {
+ opacity: 0.32;
+ cursor: not-allowed;
+ filter: grayscale(0.45);
+}
+
+.customize-popup-color-btn.is-taken:not(.is-active) > .customize-popup-color-swatch--hex {
+ border-color: rgba(255, 80, 80, 0.65);
+}
+
.customize-popup-tabs {
display: flex;
align-items: flex-end;
@@ -395,7 +407,26 @@
width: 100%;
height: 100%;
object-fit: contain;
+ object-position: center bottom;
display: block;
+ pointer-events: none;
+ transition: filter .18s ease, opacity .18s ease;
+}
+
+/* inactive: ลดความสว่าง/ความอิ่มสี ให้ดูเป็นแท็บที่ยังไม่เลือก */
+.customize-popup-tab-btn:not(.is-active) .customize-popup-tab-img {
+ filter: saturate(0.35) brightness(0.78) contrast(0.95);
+ opacity: 0.85;
+}
+
+.customize-popup-tab-btn:not(.is-active):hover .customize-popup-tab-img {
+ filter: saturate(0.55) brightness(0.92);
+ opacity: 0.95;
+}
+
+.customize-popup-tab-btn.is-active .customize-popup-tab-img {
+ filter: none;
+ opacity: 1;
}
.customize-popup-tab-btn-line {
@@ -455,7 +486,8 @@
.customize-popup-items-wrap {
position: relative;
flex: 1 1 auto;
- min-height: 0;
+ min-height: calc(180px * var(--cc-shell-scale));
+ overflow: hidden;
padding-right: calc(2px * var(--cc-shell-scale));
padding-left: calc(2px * var(--cc-shell-scale));
}
@@ -480,15 +512,19 @@
.customize-popup-item {
position: relative;
- min-height: calc(171px * var(--cc-shell-scale));
+ width: 100%;
+ aspect-ratio: 181 / 171;
+ min-height: 0;
isolation: isolate;
+ overflow: hidden;
}
.customize-popup-item-bg {
position: relative;
z-index: 1;
- width: calc(181px * var(--cc-shell-scale));
- height: calc(171px * var(--cc-shell-scale));
+ width: 100%;
+ height: auto;
+ aspect-ratio: 181 / 171;
max-width: 100%;
object-fit: contain;
display: block;
@@ -499,35 +535,42 @@
position: absolute;
z-index: 2;
left: 50%;
- top: calc(30px * var(--cc-shell-scale));
+ top: 17.5%;
transform: translateX(-50%);
- width: calc(101px * var(--cc-shell-scale));
- height: calc(101px * var(--cc-shell-scale));
+ width: 56%;
+ height: auto;
+ max-height: 59%;
object-fit: contain;
display: block;
+ pointer-events: none;
}
.customize-popup-item-price {
position: absolute;
- z-index: 2;
+ z-index: 3;
left: 50%;
- bottom: calc(-9px * var(--cc-shell-scale));
+ bottom: -5%;
transform: translateX(-50%);
- width: calc(121px * var(--cc-shell-scale));
- height: calc(51px * var(--cc-shell-scale));
+ width: 67%;
+ height: auto;
+ aspect-ratio: 121 / 51;
object-fit: contain;
display: block;
+ pointer-events: none;
}
.customize-popup-item-price-text {
position: absolute;
- z-index: 2;
+ z-index: 4;
left: 54%;
- bottom: calc(5px * var(--cc-shell-scale));
+ bottom: 3%;
transform: translateX(-50%);
color: #ffffff;
font-weight: 700;
- font-size: calc(16px * var(--cc-shell-scale));
+ font-size: clamp(10px, calc(16px * var(--cc-shell-scale)), 18px);
+ line-height: 1;
+ white-space: nowrap;
+ pointer-events: none;
}
.customize-popup-item:first-child .customize-popup-item-price,
@@ -539,12 +582,13 @@
position: absolute;
left: 50%;
top: 50%;
- width: calc(191px * var(--cc-shell-scale));
- height: calc(181px * var(--cc-shell-scale));
+ width: 106%;
+ height: auto;
+ aspect-ratio: 191 / 181;
transform: translate(-50%, -50%);
object-fit: contain;
display: none;
- z-index: 1;
+ z-index: 5;
pointer-events: none;
}
diff --git a/www/html/Game/public/css/evidence-view-overlay.css b/www/html/Game/public/css/evidence-view-overlay.css
index 96b709a..846209d 100644
--- a/www/html/Game/public/css/evidence-view-overlay.css
+++ b/www/html/Game/public/css/evidence-view-overlay.css
@@ -123,17 +123,19 @@
opacity: 1;
}
+/* hotspots ตรงกับ mockup 06-3-evidence-view — ev-tabs canvas = 1481×129 (3 ปุ่ม 240×110 + gap 20) */
#lobby-evidence-overlay .ev-tab-click {
position: absolute;
- top: calc(5 / var(--ev-h) * 100%);
- height: calc(110 / var(--ev-h) * 100%);
- width: calc(240 / var(--ev-w) * 100%);
+ top: calc(5 / 129 * 100%);
+ height: calc(110 / 129 * 100%);
+ width: calc(240 / 1481 * 100%);
padding: 0;
margin: 0;
border: none;
background: transparent;
cursor: pointer;
border-radius: 8px;
+ z-index: 5;
}
#lobby-evidence-overlay .ev-tab-click[data-evidence-tab="0"] {
@@ -141,11 +143,15 @@
}
#lobby-evidence-overlay .ev-tab-click[data-evidence-tab="1"] {
- left: calc(260 / var(--ev-w) * 100%);
+ left: calc(260 / 1481 * 100%);
}
#lobby-evidence-overlay .ev-tab-click[data-evidence-tab="2"] {
- left: calc(520 / var(--ev-w) * 100%);
+ left: calc(520 / 1481 * 100%);
+}
+
+#lobby-evidence-overlay .ev-tab-click:hover {
+ background: transparent;
}
#lobby-evidence-overlay .ev-tab-click:focus-visible {
@@ -159,15 +165,23 @@
width: calc(940 / var(--ev-w) * 100%);
height: calc(560 / var(--ev-h) * 100%);
display: flex;
- gap: calc(24 / var(--ev-w) * 100%);
+ flex-wrap: nowrap;
+ align-content: flex-start;
+ align-items: stretch;
+ justify-content: flex-start;
+ gap: calc(24 / 940 * 100%);
pointer-events: none;
}
+/* การ์ดเต็ม — อิง mockup 06-3 (296×540 ในกรอบ 940×560) */
#lobby-evidence-overlay .ev-card {
position: relative;
- flex: 1 1 0;
+ flex: 0 0 calc(296 / 940 * 100%);
+ width: calc(296 / 940 * 100%);
+ max-width: calc(296 / 940 * 100%);
min-width: 0;
height: 100%;
+ max-height: 100%;
border-radius: 14px;
background: linear-gradient(180deg, #cfd5dd 0%, #b2bac4 100%);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
@@ -175,6 +189,7 @@
flex-direction: column;
overflow: hidden;
pointer-events: auto;
+ box-sizing: border-box;
}
#lobby-evidence-overlay .ev-card.rarity-common {
@@ -235,9 +250,9 @@
}
#lobby-evidence-overlay .ev-card-art {
- flex: 1 1 auto;
- min-height: 120px;
- margin: 8px;
+ flex: 0 0 calc(290 / 540 * 100%);
+ min-height: 0;
+ margin: 8px 8px 0;
border-radius: 8px;
background: #707a8a;
color: rgba(255, 255, 255, 0.55);
@@ -246,6 +261,7 @@
display: grid;
place-items: center;
text-align: center;
+ box-sizing: border-box;
}
#lobby-evidence-overlay .ev-card-desc {
@@ -254,9 +270,11 @@
font-weight: 500;
color: #2a2f3c;
line-height: 1.35;
- flex-shrink: 0;
+ flex: 1 1 auto;
+ min-height: 0;
text-align: center;
margin: 0;
+ overflow: hidden;
}
#lobby-evidence-overlay .ev-card-link-wrap {
@@ -329,3 +347,59 @@
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
+
+/* ===== การ์ดที่ยังไม่ได้รับ — locked overlay ===== */
+#lobby-evidence-overlay .ev-card.ev-card--locked {
+ filter: grayscale(0.85) brightness(0.78);
+ position: relative;
+}
+
+#lobby-evidence-overlay .ev-card.ev-card--locked > *:not(.ev-card-lock) {
+ opacity: 0.18;
+}
+
+#lobby-evidence-overlay .ev-card-lock {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ align-content: center;
+ justify-items: center;
+ gap: 10px;
+ padding: 16px;
+ background: linear-gradient(180deg, rgba(8, 14, 32, 0.55) 0%, rgba(8, 14, 32, 0.85) 100%);
+ text-align: center;
+ z-index: 3;
+ border-radius: 10px;
+}
+
+#lobby-evidence-overlay .ev-card-lock-icon {
+ font-size: 64px;
+ line-height: 1;
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.6));
+ color: #cbd5e1;
+}
+
+#lobby-evidence-overlay .ev-card-lock-text {
+ font-size: 18px;
+ font-weight: 800;
+ color: #e2e8f0;
+ letter-spacing: 0.04em;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
+}
+
+#lobby-evidence-overlay .ev-card-lock-hint {
+ font-size: 12px;
+ font-weight: 500;
+ color: #94a3b8;
+ line-height: 1.4;
+ max-width: 90%;
+}
+
+#lobby-evidence-overlay .ev-empty-hint {
+ grid-column: 1 / -1;
+ margin: 0;
+ padding: 48px 24px;
+ text-align: center;
+ color: #94a3b8;
+ font: 500 15px/1.5 Kanit, system-ui, sans-serif;
+}
diff --git a/www/html/Game/public/css/testimony-overlay.css b/www/html/Game/public/css/testimony-overlay.css
index cb8839c..325d69c 100644
--- a/www/html/Game/public/css/testimony-overlay.css
+++ b/www/html/Game/public/css/testimony-overlay.css
@@ -9,11 +9,8 @@
position: fixed;
inset: 0;
z-index: 9000;
- display: flex;
- align-items: flex-start;
- justify-content: center;
overflow: hidden;
- font-family: 'Kanit', 'Noto Sans Thai', system-ui, sans-serif;
+ font-family: 'Kanit', 'Noto Sans Thai', 'Noto Sans Thai', system-ui, sans-serif;
color: #e7ecff;
background: #04050d;
}
@@ -23,22 +20,19 @@
}
#testimony-overlay .stage-wrap {
- position: relative;
- width: 100%;
- height: 100%;
- min-height: 100dvh;
- display: flex;
- align-items: flex-start;
- justify-content: center;
+ position: absolute;
+ inset: 0;
overflow: hidden;
}
#testimony-overlay .es3-stage {
- position: relative;
- width: calc(var(--es3-w) * 1px);
- height: calc(var(--es3-h) * 1px);
- flex-shrink: 0;
- transform-origin: top center;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 1920px;
+ height: 1080px;
+ transform-origin: 0 0;
+ will-change: transform;
background: #04050d;
}
@@ -70,12 +64,12 @@
object-fit: cover;
}
-/* ส่วนบน: popup CSS */
+/* ส่วนบน: popup CSS — พิกัด px ตรง evidence-summary-3.css */
#testimony-overlay .es3-top-popup {
- left: calc(50 / var(--es3-w) * 100%);
- top: calc(25 / var(--es3-h) * 100%);
- width: calc(1820 / var(--es3-w) * 100%);
- height: calc(295 / var(--es3-h) * 100%);
+ left: 50px;
+ top: 25px;
+ width: 1820px;
+ height: 295px;
background: linear-gradient(180deg, rgba(28, 50, 90, 0.55) 0%, rgba(15, 30, 60, 0.55) 100%);
border: 2px solid #5bd8ff;
border-radius: 26px;
@@ -86,39 +80,39 @@
#testimony-overlay .es3-top-popup .corner {
position: absolute;
- width: calc(86 / var(--es3-w) * 100%);
- height: calc(86 / var(--es3-h) * 100%);
+ width: 86px;
+ height: 86px;
pointer-events: none;
}
#testimony-overlay .es3-top-popup .c-tl {
- top: calc(-5 / var(--es3-h) * 100%);
- left: calc(-5 / var(--es3-w) * 100%);
+ top: -5px;
+ left: -5px;
border-top-left-radius: 26px;
}
#testimony-overlay .es3-top-popup .c-tr {
- top: calc(-5 / var(--es3-h) * 100%);
- right: calc(-5 / var(--es3-w) * 100%);
+ top: -5px;
+ right: -5px;
border-top-right-radius: 26px;
}
#testimony-overlay .es3-top-popup .c-bl {
- bottom: calc(-5 / var(--es3-h) * 100%);
- left: calc(-5 / var(--es3-w) * 100%);
+ bottom: -5px;
+ left: -5px;
border-bottom-left-radius: 26px;
}
#testimony-overlay .es3-top-popup .c-br {
- bottom: calc(-5 / var(--es3-h) * 100%);
- right: calc(-5 / var(--es3-w) * 100%);
+ bottom: -5px;
+ right: -5px;
border-bottom-right-radius: 26px;
}
#testimony-overlay .es3-popup-tab {
position: absolute;
left: 50%;
- top: calc(-22 / var(--es3-h) * 100%);
+ top: -22px;
transform: translateX(-50%);
padding: 8px 70px;
background: linear-gradient(180deg, #2a5588 0%, #1a3a6a 100%);
@@ -136,10 +130,10 @@
/* suspect */
#testimony-overlay .es3-suspect {
- left: calc(90 / var(--es3-w) * 100%);
- top: calc(38 / var(--es3-h) * 100%);
- width: calc(215 / var(--es3-w) * 100%);
- height: calc(213 / var(--es3-h) * 100%);
+ left: 90px;
+ top: 38px;
+ width: 215px;
+ height: 213px;
z-index: 5;
}
@@ -157,10 +151,10 @@
#testimony-overlay .es3-suspect-portrait {
position: absolute;
- left: calc(26 / 215 * 100%);
- top: calc(36 / 213 * 100%);
- width: calc(163 / 215 * 100%);
- height: calc(138 / 213 * 100%);
+ left: 26px;
+ top: 36px;
+ width: 163px;
+ height: 138px;
border-radius: 4px;
overflow: hidden;
z-index: 1;
@@ -176,9 +170,9 @@
#testimony-overlay .es3-suspect-name {
position: absolute;
- left: calc(20 / 215 * 100%);
- right: calc(20 / 215 * 100%);
- bottom: calc(12 / 213 * 100%);
+ left: 20px;
+ right: 20px;
+ bottom: 12px;
text-align: center;
color: #cfeeff;
font-weight: 800;
@@ -191,14 +185,14 @@
/* title */
#testimony-overlay .es3-title-wrap {
- left: calc(320 / var(--es3-w) * 100%);
- top: calc(72 / var(--es3-h) * 100%);
- width: calc(1500 / var(--es3-w) * 100%);
- height: calc(50 / var(--es3-h) * 100%);
+ left: 320px;
+ top: 72px;
+ width: 1500px;
+ height: 50px;
display: flex;
align-items: center;
justify-content: center;
- z-index: 4;
+ z-index: 5;
pointer-events: none;
}
@@ -215,15 +209,16 @@
/* player status row */
#testimony-overlay .es3-ps-row {
- left: calc(320 / var(--es3-w) * 100%);
- top: calc(148 / var(--es3-h) * 100%);
- width: calc(1500 / var(--es3-w) * 100%);
- height: calc(170 / var(--es3-h) * 100%);
+ left: 320px;
+ top: 148px;
+ width: 1500px;
+ height: 170px;
display: grid;
grid-template-columns: repeat(5, 1fr);
- column-gap: calc(24 / var(--es3-w) * 100%);
- padding: 0 calc(30 / var(--es3-w) * 100%);
- z-index: 4;
+ column-gap: 24px;
+ padding: 0 30px;
+ box-sizing: border-box;
+ z-index: 5;
}
#testimony-overlay .es3-ps {
@@ -232,6 +227,7 @@
flex-direction: column;
align-items: center;
gap: 6px;
+ min-width: 0;
}
#testimony-overlay .es3-ps-name {
@@ -526,19 +522,28 @@
cursor: not-allowed;
}
+/* โซนคลิกทับปุ่ม "ส่งหลักฐาน" ใน tab-01.png — ไม่วาด btn-send ซ้ำ (เดิม 920×944 ทับผิดจุด) */
#testimony-overlay .es3-btn-submit {
- left: calc(920 / var(--es3-w) * 100%);
- top: calc(944 / var(--es3-h) * 100%);
- width: calc(234 / var(--es3-w) * 100%);
- height: calc(100 / var(--es3-h) * 100%);
- max-width: 234px;
- max-height: 100px;
- min-width: 120px;
- min-height: 48px;
- background: url('/Main-Lobby/IMAGE/Showcard/btn-send.png') center / contain no-repeat;
+ left: calc(708 / var(--es3-w) * 100%);
+ top: calc(727 / var(--es3-h) * 100%);
+ width: calc(180 / var(--es3-w) * 100%);
+ height: calc(77 / var(--es3-h) * 100%);
+ max-width: 180px;
+ max-height: 77px;
+ min-width: 96px;
+ min-height: 40px;
+ background: transparent;
z-index: 5;
}
+#testimony-overlay .es3-btn-submit:not(:disabled):hover {
+ filter: brightness(1.08);
+}
+
+#testimony-overlay .es3-btn-submit:not(:disabled):active {
+ filter: brightness(0.92);
+}
+
#testimony-overlay .es3-btn-open {
left: calc(1329 / var(--es3-w) * 100%);
top: calc(944 / var(--es3-h) * 100%);
@@ -607,10 +612,10 @@
}
#testimony-overlay .es-reveal-wrap .es-popup {
- left: calc(120 / var(--es3-w) * 100%);
- top: calc(22 / var(--es3-h) * 100%);
- width: calc(1680 / var(--es3-w) * 100%);
- height: calc(1036 / var(--es3-h) * 100%);
+ left: 120px;
+ top: 22px;
+ width: 1680px;
+ height: 1036px;
pointer-events: none;
z-index: 1;
}
@@ -622,10 +627,10 @@
}
#testimony-overlay .es-reveal-wrap .es-suspect {
- left: calc(150 / var(--es3-w) * 100%);
- top: calc(35 / var(--es3-h) * 100%);
- width: calc(230 / var(--es3-w) * 100%);
- height: calc(228 / var(--es3-h) * 100%);
+ left: 150px;
+ top: 35px;
+ width: 230px;
+ height: 228px;
z-index: 5;
}
@@ -643,10 +648,10 @@
#testimony-overlay .es-reveal-wrap .es-suspect-portrait {
position: absolute;
- left: calc(30 / 230 * 100%);
- top: calc(40 / 228 * 100%);
- width: calc(170 / 230 * 100%);
- height: calc(150 / 228 * 100%);
+ left: 30px;
+ top: 40px;
+ width: 170px;
+ height: 150px;
border-radius: 4px;
overflow: hidden;
z-index: 1;
@@ -662,9 +667,9 @@
#testimony-overlay .es-reveal-wrap .es-suspect-name {
position: absolute;
- left: calc(20 / 230 * 100%);
- right: calc(20 / 230 * 100%);
- bottom: calc(12 / 228 * 100%);
+ left: 20px;
+ right: 20px;
+ bottom: 12px;
text-align: center;
color: #fff3a8;
font-weight: 800;
@@ -676,10 +681,10 @@
}
#testimony-overlay .es-reveal-wrap .es-title-wrap {
- left: calc(285 / var(--es3-w) * 100%);
- top: calc(102 / var(--es3-h) * 100%);
- width: calc(1340 / var(--es3-w) * 100%);
- height: calc(175 / var(--es3-h) * 100%);
+ left: 285px;
+ top: 102px;
+ width: 1340px;
+ height: 175px;
display: flex;
flex-direction: column;
align-items: center;
@@ -713,15 +718,17 @@
}
#testimony-overlay .es-reveal-wrap .es-players {
- left: calc(120 / var(--es3-w) * 100%);
- top: calc(300 / var(--es3-h) * 100%);
- width: calc(1680 / var(--es3-w) * 100%);
- height: calc(650 / var(--es3-h) * 100%);
+ left: 120px;
+ top: 300px;
+ width: 1680px;
+ height: 650px;
display: grid;
grid-template-columns: repeat(5, 1fr);
- column-gap: calc(24 / var(--es3-w) * 100%);
- padding: 0 calc(70 / var(--es3-w) * 100%);
- z-index: 3;
+ column-gap: 24px;
+ padding: 0 70px;
+ box-sizing: border-box;
+ align-items: start;
+ z-index: 6;
}
#testimony-overlay .es-reveal-wrap .es-player {
@@ -731,6 +738,7 @@
align-items: center;
gap: 12px;
min-width: 0;
+ align-self: start;
}
#testimony-overlay .es-reveal-wrap .es-player-name {
@@ -763,9 +771,10 @@
#testimony-overlay .es-reveal-wrap .es-card {
position: relative;
- width: 100%;
+ width: 220px;
max-width: 220px;
height: 275px;
+ flex-shrink: 0;
border-radius: 10px;
background: linear-gradient(180deg, #cfd5dd 0%, #b2bac4 100%);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
@@ -905,10 +914,10 @@
}
#testimony-overlay .es-reveal-wrap .es-ready {
- left: calc(818 / var(--es3-w) * 100%);
- top: calc(951 / var(--es3-h) * 100%);
- width: calc(280 / var(--es3-w) * 100%);
- height: calc(96 / var(--es3-h) * 100%);
+ left: 818px;
+ top: 951px;
+ width: 280px;
+ height: 96px;
max-width: 280px;
max-height: 96px;
z-index: 10;
diff --git a/www/html/Game/public/js/customize-popup.js b/www/html/Game/public/js/customize-popup.js
index 87bb80a..e015687 100644
--- a/www/html/Game/public/js/customize-popup.js
+++ b/www/html/Game/public/js/customize-popup.js
@@ -567,10 +567,12 @@
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)
+ return new Promise(function (resolve) {
+ function runApply() {
+ var tint = getSelectedTint();
+ var domAvatar = resolveDomAvatarSource();
+ var fallbackSrc = domAvatar.src || '';
+ composeTintedCharacterPreview(characterId, tint)
.then(function (outSrc) {
if (outSrc) return outSrc;
return composeHeuristicTintFromSource(fallbackSrc, tint);
@@ -603,7 +605,26 @@
} catch (e2) { /* ignore */ }
return finalSrc;
})
- .catch(function () { return null; });
+ .catch(function () { return null; })
+ .then(resolve);
+ }
+ if (isRoomLobbyPage() && typeof window.validateLobbyThemeColorChange === 'function') {
+ window.validateLobbyThemeColorChange(
+ clampIndex(state.activeColor, 1, 8),
+ clampIndex(state.activeSkin, 1, 3),
+ function (ok, err) {
+ if (!ok) {
+ if (err) try { alert(err); } catch (e) { /* ignore */ }
+ resolve(null);
+ return;
+ }
+ runApply();
+ }
+ );
+ return;
+ }
+ runApply();
+ });
}
function removeLegacyCustomizeOverlays() {
@@ -777,19 +798,28 @@
refs.colorRow.textContent = '';
for (var i = 1; i <= 8; i += 1) {
var hex = THEME_SWATCH_HEX[i - 1] || '#ffffff';
+ var taken = isRoomLobbyPage() && typeof window.isLobbyThemeIndexAvailable === 'function'
+ && !window.isLobbyThemeIndexAvailable(i, true);
var btn = document.createElement('button');
btn.type = 'button';
- btn.className = 'customize-popup-color-btn' + (state.activeColor === i ? ' is-active' : '');
- btn.setAttribute('aria-label', 'เลือกสีที่ ' + i);
+ var cls = 'customize-popup-color-btn';
+ if (state.activeColor === i) cls += ' is-active';
+ if (taken) cls += ' is-taken';
+ btn.className = cls;
+ btn.setAttribute('aria-label', taken ? ('สีที่ ' + i + ' มีคนใช้แล้ว') : ('เลือกสีที่ ' + i));
btn.setAttribute('aria-pressed', state.activeColor === i ? 'true' : 'false');
+ if (taken) btn.setAttribute('aria-disabled', 'true');
btn.innerHTML = swatchButtonHtml(hex, state.activeColor === i);
- (function (idx) {
+ (function (idx, isTaken) {
btn.addEventListener('click', function () {
+ if (isRoomLobbyPage() && typeof window.isLobbyThemeIndexAvailable === 'function') {
+ if (!window.isLobbyThemeIndexAvailable(idx, true)) return;
+ }
state.activeColor = idx;
renderColorRow();
renderPreview();
});
- })(i);
+ })(i, taken);
refs.colorRow.appendChild(btn);
}
}
@@ -818,10 +848,11 @@
}
}
- 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';
+ /** ใช้รูปปุ่มเดี่ยว 256×100 (-a.png) ทุก state — ไฟล์ tab-N.png เป็นแถบ 3 ปุ่มของ mockup ใช้เป็น sprite ปุ่มเดี่ยวไม่ได้ */
+ function tabImageName(index) {
+ if (index === 1) return 'tab-1-face-a.png';
+ if (index === 2) return 'tab-2-hair-a.png';
+ return 'tab-3-cloth-a.png';
}
function renderTabs() {
@@ -841,7 +872,7 @@
btn.className = 'customize-popup-tab-btn' + (active ? ' is-active' : '');
btn.setAttribute('aria-label', 'หมวด ' + i);
btn.innerHTML = '' +
- '' +
+ '
' +
'
';
(function (idx) {
btn.addEventListener('click', function () {
@@ -1029,6 +1060,7 @@
syncScale();
syncScrollUi();
renderPreview();
+ if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
requestAnimationFrame(syncScale);
requestAnimationFrame(syncScrollUi);
}
@@ -1105,4 +1137,5 @@
window.openCustomizePopup = openPopup;
window.closeCustomizePopup = closePopup;
+ window.refreshCustomizeColorRow = renderColorRow;
})();
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 0cae327..15ab2f5 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -328,6 +328,13 @@
};
if (socket && socket.connected) {
socket.emit('detective-minigame-finished', {}, function (res) {
+ try {
+ if (res && Array.isArray(res.myPlayerEvidence)) {
+ var hasAny = res.myPlayerEvidence.some(function (r) { return Array.isArray(r) && r.length; });
+ if (hasAny) sessionStorage.setItem('justiceDetectiveMyEvidence', JSON.stringify(res.myPlayerEvidence));
+ else sessionStorage.removeItem('justiceDetectiveMyEvidence');
+ }
+ } catch (e) { /* ignore */ }
go();
});
} else {
@@ -800,8 +807,57 @@
return gauntletCrownRunwayBgImagesReadyPlay();
}
+ /** ตำแหน่งเส้น FINISH บนจอ (0=ซ้าย 1=ขวา) จากขอบซ้าย viewport — ใช้หยุดเลื่อน + fade สิ่งกีดขวาง */
+ function gauntletCrownRunwayFinishLineScreenFracPlay(g) {
+ if (!g) return null;
+ const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
+ let lineInArt = Number(raw.finishLineInFinishArtFrac);
+ if (!Number.isFinite(lineInArt)) {
+ /* mno9kb07: จบที่ประตูตู้เซฟ (ขวาของ finish art) ไม่ใช่แถบ FINISH ด้านซ้าย */
+ lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.72 : 0.12;
+ }
+ lineInArt = Math.max(0, Math.min(0.92, lineInArt));
+ const finishLineWorldX = g.finishStart + g.w4Safe * lineInArt;
+ return (finishLineWorldX - g.scrollView) / Math.max(1e-6, g.cwWorld);
+ }
+
+ function gauntletCrownRunwayFinishStopViewportFracPlay() {
+ const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
+ const stopHi = gauntletCrownRunwayBgMapEligiblePlay() ? 0.85 : 0.75;
+ let vp = Number(raw.finishStopViewportFrac);
+ if (Number.isFinite(vp)) return Math.max(0.08, Math.min(stopHi, vp));
+ let legacy = Number(raw.finishStopScreenFrac);
+ if (Number.isFinite(legacy)) {
+ legacy = Math.max(0.08, Math.min(0.96, legacy));
+ return Math.max(0.12, Math.min(0.62, 0.14 + legacy * 0.42));
+ }
+ return gauntletCrownRunwayBgMapEligiblePlay() ? 0.5 : 0.34;
+ }
+
+ function gauntletCrownRunwayObstacleFadeStartViewportFracPlay() {
+ const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
+ let v = Number(raw.obstacleFadeStartViewportFrac);
+ if (!Number.isFinite(v)) v = 0.9;
+ return Math.max(0.45, Math.min(1.15, v));
+ }
+
+ /** 1 = สิ่งกีดขวางเต็ม · 0 = หาย — ค่อยๆ จางเมื่อเส้น FINISH เข้าใกล้จุดหยุดบนจอ */
+ function gauntletCrownRunwayObstacleDrawAlphaPlay() {
+ if (!gauntletCrownRunwayBgDrawActivePlay()) return 1;
+ if (gauntletCrownRunwayBgFinishLatched) return 0;
+ const g = gauntletCrownRunwayScrollGeometryPlay();
+ if (!g) return 1;
+ const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g);
+ if (screenFrac == null) return 1;
+ const stopAt = gauntletCrownRunwayFinishStopViewportFracPlay();
+ const fadeStart = gauntletCrownRunwayObstacleFadeStartViewportFracPlay();
+ if (screenFrac >= fadeStart) return 1;
+ if (screenFrac <= stopAt) return 0;
+ return (screenFrac - stopAt) / Math.max(1e-6, fadeStart - stopAt);
+ }
+
function gauntletCrownRunwayBgHideObstaclesPlay() {
- return gauntletCrownRunwayBgDrawActivePlay() && gauntletCrownRunwayBgFinishLatched;
+ return gauntletCrownRunwayObstacleDrawAlphaPlay() <= 0.001;
}
/** Last Light: หยุดแอนิเมชันวิ่งเมื่อหยุดที่ FINISH หรือ latch แล้ว */
@@ -979,7 +1035,9 @@
/**
* Shared runway strip math (viewport in strip space). null if not drawable.
- * Map tuning: gauntletCrownRunwayBg.finishStopScreenFrac 0.08–0.96 = จุดกลางจอต้องถึงภายในแถบ FINISH ก่อนหยุด (ค่าเริ่ม ~0.56 ใกล้กล่องหยุดขวาเส้น FINISH บนงานศิลป์)
+ * Map tuning (mno9kb07): finishStopViewportFrac 0.08–0.75 = เส้น FINISH อยู่ที่สัดส่วนนี้จากซ้ายจอก่อนหยุด (ค่าเริ่ม 0.34)
+ * finishLineInFinishArtFrac = ตำแหน่งเส้นใน finish.png (ค่าเริ่ม 0.12)
+ * obstacleFadeStartViewportFrac = เริ่มจางสิ่งกีดขวางเมื่อเส้น FINISH อยู่ที่สัดส่วนนี้บนจอ (ค่าเริ่ม 0.9)
*/
function gauntletCrownRunwayScrollGeometryPlay() {
if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return null;
@@ -1007,19 +1065,14 @@
};
}
- /** True when viewport center crosses “หยุดที่ FINISH” (ไม่ใช้แค่ขอบขวาถึงปลายแทร็ก) */
+ /** True when เส้น FINISH ถึงตำแหน่งบนจอที่กำหนด (ขวาเส้น FINISH ตามงานศิลป์ — ไม่เลื่อนเกินไป) */
function gauntletCrownRunwayScrollStripStopZoneReachedPlay() {
const g = gauntletCrownRunwayScrollGeometryPlay();
if (!g) return false;
- const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
- let frac = Number(raw.finishStopScreenFrac);
- if (!Number.isFinite(frac)) frac = 0.56;
- frac = Math.max(0.08, Math.min(0.96, frac));
- const targetCenter = g.finishStart + g.w4Safe * frac;
- const center = g.scrollView + g.cwWorld * 0.5;
if (g.scrollView < g.minScrollForFinishPass - 0.01) return false;
- if (center < targetCenter - 1.5) return false;
- return true;
+ const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g);
+ if (screenFrac == null) return false;
+ return screenFrac <= gauntletCrownRunwayFinishStopViewportFracPlay() + 0.008;
}
function resetGauntletCrownRunwaySpawnScrollSnap() {
@@ -1728,6 +1781,56 @@
}
const LOBBY_PLAYER_TINT_KEY = 'justiceLobbyPlayerTint';
+ /** ธีมสี 8 ช่อง — ตรง room-lobby / customize */
+ const PLAY_LOBBY_THEME_HEX = [
+ '#d72520', '#ef8508', '#efe237', '#5bb443',
+ '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe',
+ ];
+ const PLAY_LOBBY_SKIN_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1'];
+ let playLobbyBotThemes = [];
+
+ function parsePlayLobbyThemeIndex(n) {
+ const v = Math.floor(Number(n));
+ return (v >= 1 && v <= 8) ? v : null;
+ }
+
+ function parsePlayLobbySkinIndex(n) {
+ const v = Math.floor(Number(n));
+ return (v >= 1 && v <= 3) ? v : null;
+ }
+
+ function playTintFromLobbyThemeIndices(themeIndex, skinIndex) {
+ const ti = parsePlayLobbyThemeIndex(themeIndex);
+ const si = parsePlayLobbySkinIndex(skinIndex);
+ const bodyHex = ti ? PLAY_LOBBY_THEME_HEX[ti - 1] : '';
+ const headHex = si ? PLAY_LOBBY_SKIN_HEX[si - 1] : '';
+ if (!bodyHex && !headHex) return null;
+ return playTintFromLobbyRgb(bodyHex || null, headHex || null);
+ }
+
+ function playTintFromJoinPeer(peerRow) {
+ if (!peerRow) return null;
+ return playTintFromLobbyThemeIndices(peerRow.lobbyColorThemeIndex, peerRow.lobbySkinToneIndex);
+ }
+
+ function playTintForPreviewBotId(peerId) {
+ if (!isPreviewBotId(peerId)) return null;
+ const idx = parseInt(String(peerId).slice(PREVIEW_BOT_PREFIX.length), 10);
+ if (!Number.isFinite(idx) || idx < 0) return null;
+ const slot = playLobbyBotThemes[idx];
+ if (!slot) return null;
+ const tint = playTintFromLobbyThemeIndices(slot.themeIndex, slot.skinToneIndex);
+ if (!tint) return null;
+ try {
+ const map = JSON.parse(sessionStorage.getItem(lobbyBotTintsStorageKeyPlay()) || '{}');
+ map['__lobby_bot_' + idx] = {
+ colorTheme: PLAY_LOBBY_THEME_HEX[parsePlayLobbyThemeIndex(slot.themeIndex) - 1] || '',
+ colorSkin: PLAY_LOBBY_SKIN_HEX[parsePlayLobbySkinIndex(slot.skinToneIndex) - 1] || '',
+ };
+ sessionStorage.setItem(lobbyBotTintsStorageKeyPlay(), JSON.stringify(map));
+ } catch (e) { /* ignore */ }
+ return tint;
+ }
function lobbyBotTintsStorageKeyPlay() {
const sp = (typeof spaceId === 'string' && spaceId) ? spaceId : (new URLSearchParams(window.location.search).get('space') || 'default');
@@ -1803,8 +1906,12 @@
}
/** สีจากล็อบบี้ (ผู้เล่น + บอท) — fallback hash ตาม peer id */
- function resolvePlayTintForPeer(peerId) {
+ function resolvePlayTintForPeer(peerId, peerRow) {
const sid = String(peerId || '');
+ const fromRow = peerRow ? playTintFromJoinPeer(peerRow) : null;
+ if (fromRow) return fromRow;
+ const previewTint = playTintForPreviewBotId(sid);
+ if (previewTint) return previewTint;
if (myId != null && sid === String(myId)) {
const meTint = loadPlayTintFromLobbyPlayerStorage();
if (meTint) return meTint;
@@ -1815,6 +1922,45 @@
return playTintFromPeerId(sid);
}
+ function applyPlayTintFromLobbyIndices(peerId, themeIndex, skinIndex) {
+ const tint = playTintFromLobbyThemeIndices(themeIndex, skinIndex);
+ if (!tint) return;
+ const sid = peerId != null ? String(peerId) : '';
+ if (myId != null && sid === String(myId)) {
+ me.playTint = tint;
+ return;
+ }
+ const o = others.get(peerId) || others.get(sid);
+ if (o) o.playTint = tint;
+ if (isPreviewBotId(sid)) playTintForPreviewBotId(sid);
+ }
+
+ function applyPlayLobbyTintSync(data) {
+ if (!data || typeof data !== 'object') return;
+ if (Array.isArray(data.lobbyBotThemes)) playLobbyBotThemes = data.lobbyBotThemes;
+ if (Array.isArray(data.peers)) {
+ data.peers.forEach(function (p) {
+ if (!p || p.id == null) return;
+ applyPlayTintFromLobbyIndices(p.id, p.lobbyColorThemeIndex, p.lobbySkinToneIndex);
+ });
+ }
+ if (playBotsEnabled()) rebalancePreviewBots();
+ try {
+ preloadPlayOccupantsTints();
+ preloadPlayLobbyIdleAvatarsForOccupants();
+ } catch (e) { /* ignore */ }
+ }
+
+ function getPlayTintForMissionAvatar(peerId) {
+ const sid = peerId != null ? String(peerId) : '';
+ if (myId != null && sid === String(myId)) {
+ return me.playTint || resolvePlayTintForPeer(sid);
+ }
+ const o = others.get(peerId) || others.get(sid);
+ if (o && o.playTint) return o.playTint;
+ return resolvePlayTintForPeer(sid);
+ }
+
/** สรุปภารกิจ — ผู้เล่นจริงต้องอยู่ใน top N เสมอ */
function missionEnsureHumanInTopRanked(rows, maxN) {
const cap = Math.max(1, maxN || 5);
@@ -4200,7 +4346,7 @@
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
const o = others.get(bid);
if (!o) return;
- o.playTint = resolvePlayTintForPeer(bid);
+ o.playTint = playTintForPreviewBotId(bid) || resolvePlayTintForPeer(bid);
if (o.botTier == null) {
const tierRoll = Math.random();
const stackPrev = mapData && mapData.gameType === 'stack';
@@ -4234,7 +4380,11 @@
}
botIds = [...others.keys()].filter(isPreviewBotId);
while (botIds.length < wantBots) {
- const id = PREVIEW_BOT_PREFIX + (++previewBotSeq);
+ /* หา slot ว่างเล็กสุด (0,1,2,…) — IDX ของ ID ต้องตรงกับ playLobbyBotThemes[idx] เพื่อให้สีตรง lobby */
+ let nextSlot = 0;
+ while (others.has(PREVIEW_BOT_PREFIX + nextSlot)) nextSlot++;
+ previewBotSeq = Math.max(previewBotSeq, nextSlot + 1);
+ const id = PREVIEW_BOT_PREFIX + nextSlot;
const joinIdx = countPlayHumans() + botIds.length;
let x;
let y;
@@ -4267,7 +4417,7 @@
nickname: 'บอท',
characterId: pickPreviewBotCharacterId(botIds.length),
spawnJoinOrder: joinIdx,
- playTint: resolvePlayTintForPeer(id),
+ playTint: playTintForPreviewBotId(id) || resolvePlayTintForPeer(id),
botTier,
gauntletJumpTicks: 0,
gauntletJumpVis: 0,
@@ -8189,13 +8339,7 @@
if (fallbackUrl) imgEl.src = fallbackUrl;
return;
}
- const isMePeer = peerId != null && myId != null && String(peerId) === String(myId);
- const lobbySrc = isMePeer ? playLobbyIdleAvatarSrcForCharacter(characterId) : '';
- if (lobbySrc) {
- imgEl.src = lobbySrc;
- return;
- }
- const tint = resolvePlayTintForPeer(peerId != null ? String(peerId) : String((myId != null && myId !== '') ? myId : 'local'));
+ const tint = getPlayTintForMissionAvatar(peerId != null ? peerId : ((myId != null && myId !== '') ? myId : 'local'));
const out = resolvePlayDrawCharSource(characterId, 'down', nowT, false, tint, isMePeer);
if (out && out.tagName === 'CANVAS' && out.width > 0) {
try {
@@ -12207,6 +12351,7 @@
const [sx, sy] = worldToScreen(ent.spaceShooterCx, ent.spaceShooterCy);
const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length];
const elimShip = !!(isSpaceShooterMissionUiMapPlay() && ent.spaceShooterEliminated);
+ if (elimShip) return;
const slot = Math.max(1, Math.min(6, Number(ent.spaceShooterSlot) || ((idx % 6) + 1)));
const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim();
const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null;
@@ -14477,7 +14622,21 @@
gauntletObsRenderPrev = [];
gauntletObsRenderNext = [];
gauntletObsBlendT0 = 0;
- me.playTint = resolvePlayTintForPeer(String((myId != null && myId !== '') ? myId : (nick || 'local')));
+ if (Array.isArray(res.lobbyBotThemes)) playLobbyBotThemes = res.lobbyBotThemes;
+ const myPeerJoin = (Array.isArray(plist) ? plist : []).find((q) => q && q.id === myId);
+ me.playTint = playTintFromJoinPeer(myPeerJoin) || resolvePlayTintForPeer(String((myId != null && myId !== '') ? myId : (nick || 'local')), myPeerJoin);
+ if (myPeerJoin && myPeerJoin.lobbyColorThemeIndex) {
+ try {
+ const ti = parsePlayLobbyThemeIndex(myPeerJoin.lobbyColorThemeIndex);
+ const si = parsePlayLobbySkinIndex(myPeerJoin.lobbySkinToneIndex) || 1;
+ if (ti) {
+ localStorage.setItem(LOBBY_PLAYER_TINT_KEY, JSON.stringify({
+ themeRgb: PLAY_LOBBY_THEME_HEX[ti - 1],
+ skinRgb: PLAY_LOBBY_SKIN_HEX[si - 1],
+ }));
+ }
+ } catch (eTintStore) { /* ignore */ }
+ }
// อุ่น cache เลเยอร์สีตัวละครก่อนเข้าเกม (ตัวเรา + เพื่อนร่วมห้อง) กันกระตุกตอนโหลดสีกลางเกม
try {
preloadPlayCharacterLayers(me.characterId || getPlayCharacterId());
@@ -14509,7 +14668,7 @@
others.set(p.id, {
x: px, y: py, tx: px, ty: py, direction: p.direction, nickname: p.nickname, characterId: p.characterId,
spawnJoinOrder: spawnOrd,
- playTint: resolvePlayTintForPeer(p.id),
+ playTint: playTintFromJoinPeer(p) || playTintForPreviewBotId(p.id) || resolvePlayTintForPeer(p.id, p),
gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
@@ -14882,6 +15041,8 @@
socket.on('peer-display-name', applyPlayDisplayNameUpdate);
+ socket.on('lobby-tint-sync', applyPlayLobbyTintSync);
+
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;
@@ -14901,7 +15062,7 @@
direction: data.direction || 'down', nickname: data.nickname,
characterId: data.characterId ?? null,
spawnJoinOrder: spawnOrdJ,
- playTint: resolvePlayTintForPeer(data.id),
+ playTint: playTintFromJoinPeer(data) || playTintForPreviewBotId(data.id) || resolvePlayTintForPeer(data.id, data),
gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0,
gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0,
gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
@@ -17978,10 +18139,13 @@
drawStackMinigame(ctx, worldToScreen, zDraw);
}
- const gauntletObsDraw = (isGauntlet() && !gauntletCrownRunwayBgHideObstaclesPlay())
+ const gauntletObsAlpha = isGauntlet() ? gauntletCrownRunwayObstacleDrawAlphaPlay() : 1;
+ const gauntletObsDraw = (isGauntlet() && gauntletObsAlpha > 0.001)
? getGauntletObsDrawPositionsAt(performance.now())
: [];
if (isGauntlet() && gauntletObsDraw.length) {
+ ctx.save();
+ ctx.globalAlpha = gauntletObsAlpha;
const stx = Math.max(0, Math.floor(worldMinX / tileSize));
const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize));
const sty = Math.max(0, Math.floor(worldMinY / tileSize));
@@ -18024,6 +18188,7 @@
drawGauntletLaserColumnScreen(rx, ry, rw, rh);
}
}
+ ctx.restore();
}
/** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */
diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js
index 7ddedec..665a002 100644
--- a/www/html/Game/public/js/room-lobby.js
+++ b/www/html/Game/public/js/room-lobby.js
@@ -158,6 +158,8 @@
let suspectCardMinigames = [];
/** หลักฐาน (ติกถูก) ที่เก็บได้ต่อผู้ต้องสงสัย 0–2 (เล่นมินิเกมจบ +1 ต่อช่อง สูงสุด 3) */
let suspectProgress = [0, 0, 0];
+ /** การ์ดหลักฐานที่ผู้เล่นคนนี้เก็บได้ — [suspectIdx][cardIdx 0–2] จากเซิร์ฟ */
+ let myPlayerEvidence = [[], [], []];
/** ขั้นพิจารณาคดี: null | 'voting' | 'revealed' */
let trialMode = null;
let myTrialVote = null;
@@ -414,6 +416,23 @@
}
function rlResolveMyColors(cb) {
+ var me = peers.get(socket.id);
+ var serverTi = me && parseLobbyThemeIndex(me.lobbyColorThemeIndex);
+ var serverSi = me && parseLobbySkinIndex(me.lobbySkinToneIndex);
+ if (serverTi) {
+ applyPeerThemeFromServer(me, serverTi, serverSi, function () {
+ myTintTheme = me.colorTheme || myTintTheme;
+ myTintSkin = me.colorSkin || myTintSkin;
+ try {
+ localStorage.setItem('lobbyThemeColor', String(serverTi));
+ if (serverSi) localStorage.setItem('lobbySkinTone', String(serverSi));
+ } catch (eLs) { /* ignore */ }
+ persistLobbyPlayerTintJson();
+ if (mapData && canvas) drawLobbyMap();
+ if (cb) cb();
+ });
+ return;
+ }
var c = '', s = '';
try { c = localStorage.getItem('lobbyThemeColor') || ''; s = localStorage.getItem('lobbySkinTone') || ''; } catch (e) {}
var need = 0, done = false;
@@ -538,6 +557,127 @@
];
/** สีผิว 3 ช่อง */
var ROOM_CZ_SKIN_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1'];
+ /** สี bot จากเซิร์ฟเวอร์ (ลำดับช่อง) — [{ themeIndex, skinToneIndex }, ...] */
+ var lobbyBotThemesFromServer = [];
+
+ function parseLobbyThemeIndex(n) {
+ var v = Math.floor(Number(n));
+ return (v >= 1 && v <= 8) ? v : null;
+ }
+
+ function parseLobbySkinIndex(n) {
+ var v = Math.floor(Number(n));
+ return (v >= 1 && v <= 3) ? v : null;
+ }
+
+ function getMyLobbyThemeIndex() {
+ var me = peers.get(socket.id);
+ return parseLobbyThemeIndex(me && me.lobbyColorThemeIndex) || parseLobbyThemeIndex(localStorage.getItem('lobbyThemeColor'));
+ }
+
+ function peerLobbyThemeIndex(p) {
+ if (!p) return null;
+ return parseLobbyThemeIndex(p.lobbyColorThemeIndex) || parseLobbyThemeIndex(p.colorThemeIndex);
+ }
+
+ function getLobbyTakenThemeIndices(excludePeerId) {
+ var taken = [];
+ var seen = {};
+ peers.forEach(function (p, id) {
+ if (excludePeerId && id === excludePeerId) return;
+ var ti = peerLobbyThemeIndex(p);
+ if (ti && !seen[ti]) { seen[ti] = true; taken.push(ti); }
+ });
+ lobbyBots.forEach(function (b) {
+ var ti = peerLobbyThemeIndex(b);
+ if (ti && !seen[ti]) { seen[ti] = true; taken.push(ti); }
+ });
+ return taken;
+ }
+
+ function isLobbyThemeIndexAvailable(themeIndex, forSelfChange) {
+ var ti = parseLobbyThemeIndex(themeIndex);
+ if (!ti) return false;
+ var myIdx = getMyLobbyThemeIndex();
+ if (forSelfChange && myIdx === ti) return true;
+ return getLobbyTakenThemeIndices(forSelfChange ? socket.id : null).indexOf(ti) < 0;
+ }
+
+ function applyThemeIndexToOccupant(target, themeIndex, skinIndex, done) {
+ if (!target) { if (done) done(); return; }
+ var ti = parseLobbyThemeIndex(themeIndex) || 1;
+ var si = parseLobbySkinIndex(skinIndex) || 1;
+ target.colorThemeIndex = ti;
+ target.colorSkinIndex = si;
+ var need = 2;
+ function tick() {
+ need--;
+ if (need <= 0 && done) done();
+ }
+ rlSampleSwatch('color', ti, function (rgb) {
+ if (rgb) target.colorTheme = rgb;
+ tick();
+ });
+ rlSampleSwatch('skin', si, function (rgb) {
+ if (rgb) target.colorSkin = rgb;
+ tick();
+ });
+ }
+
+ function applyPeerThemeFromServer(peer, themeIndex, skinIndex, done) {
+ applyThemeIndexToOccupant(peer, themeIndex, skinIndex, done);
+ }
+
+ function applyAllPeerThemesFromServer(done) {
+ var pending = 1;
+ function tick() {
+ pending--;
+ if (pending <= 0 && done) done();
+ }
+ peers.forEach(function (p, id) {
+ var ti = parseLobbyThemeIndex(p.lobbyColorThemeIndex);
+ if (!ti) return;
+ pending++;
+ applyPeerThemeFromServer(p, ti, p.lobbySkinToneIndex, tick);
+ if (id === socket.id) {
+ try {
+ localStorage.setItem('lobbyThemeColor', String(ti));
+ if (p.lobbySkinToneIndex) localStorage.setItem('lobbySkinTone', String(p.lobbySkinToneIndex));
+ } catch (e) { /* ignore */ }
+ }
+ });
+ tick();
+ }
+
+ function applyLobbyTintSyncPayload(data) {
+ if (!data) return;
+ if (Array.isArray(data.lobbyBotThemes)) lobbyBotThemesFromServer = data.lobbyBotThemes;
+ try { sessionStorage.removeItem(lobbyBotTintsStorageKey()); } catch (eClr) { /* ignore */ }
+ (data.peers || []).forEach(function (row) {
+ if (!row || !row.id) return;
+ var p = peers.get(row.id);
+ if (!p) return;
+ if (parseLobbyThemeIndex(row.lobbyColorThemeIndex)) {
+ p.lobbyColorThemeIndex = row.lobbyColorThemeIndex;
+ }
+ if (parseLobbySkinIndex(row.lobbySkinToneIndex)) {
+ p.lobbySkinToneIndex = row.lobbySkinToneIndex;
+ }
+ });
+ applyAllPeerThemesFromServer(function () {
+ if (peers.get(socket.id)) {
+ var me = peers.get(socket.id);
+ if (me && parseLobbyThemeIndex(me.lobbyColorThemeIndex)) {
+ myTintTheme = me.colorTheme || myTintTheme;
+ myTintSkin = me.colorSkin || myTintSkin;
+ }
+ }
+ syncLobbyCaseBots();
+ if (mapData && canvas) drawLobbyMap();
+ if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
+ });
+ }
+
/** จุด "ห้องแต่งตัว" ในฉาก — ตั้งจาก mapData.customizeSpot (วางใน editor) ถ้าไม่มี = null = ไม่แสดง */
var ROOM_CZ_SPOT = null;
var roomCzIcon = new Image();
@@ -624,6 +764,37 @@
function setupRoomCustomize() {
window.openRoomCustomize = openRoomCustomize;
window.closeRoomCustomize = closeRoomCustomize;
+ window.isLobbyThemeIndexAvailable = isLobbyThemeIndexAvailable;
+ window.getLobbyTakenThemeIndices = function (forSelf) {
+ return getLobbyTakenThemeIndices(forSelf ? socket.id : null);
+ };
+ window.validateLobbyThemeColorChange = function (themeIndex, skinIndex, cb) {
+ var ti = parseLobbyThemeIndex(themeIndex);
+ if (!ti) { if (cb) cb(false, 'เลือกสีเสื้อไม่ถูกต้อง'); return; }
+ if (!isLobbyThemeIndexAvailable(ti, true)) {
+ if (cb) cb(false, 'สีเสื้อนี้มีคนใช้แล้ว — เลือกสีอื่น');
+ return;
+ }
+ socket.emit('lobby-tint-update', {
+ themeIndex: ti,
+ skinToneIndex: parseLobbySkinIndex(skinIndex) || 1,
+ }, function (res) {
+ if (!(res && res.ok)) {
+ if (cb) cb(false, (res && res.error) ? res.error : 'เปลี่ยนสีไม่สำเร็จ');
+ return;
+ }
+ try {
+ localStorage.setItem('lobbyThemeColor', String(res.lobbyColorThemeIndex || ti));
+ localStorage.setItem('lobbySkinTone', String(res.lobbySkinToneIndex || skinIndex || 1));
+ } catch (e) { /* ignore */ }
+ var me = peers.get(socket.id);
+ if (me) {
+ me.lobbyColorThemeIndex = res.lobbyColorThemeIndex || ti;
+ me.lobbySkinToneIndex = res.lobbySkinToneIndex || skinIndex;
+ }
+ if (cb) cb(true);
+ });
+ };
window.addEventListener('customize-popup:applied', function () {
rlResolveMyColors(function () {
persistLobbyPlayerTintJson();
@@ -1971,35 +2142,26 @@
botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 900),
};
lobbyBots.set(id, bot);
- var restored = applySavedLobbyBotColors(id, bot);
- if (restored) {
- if (bot.characterId) preloadLobbyTintedCharacter(bot.characterId, bot.colorTheme, bot.colorSkin, function () {});
- if (mapData && canvas) drawLobbyMap();
- } else {
- var botColorIdx = 1 + Math.floor(Math.random() * 8);
- var botSkinIdx = 1 + Math.floor(Math.random() * 3);
- var colorDone = false;
- var skinDone = false;
- function maybeFinishBotTint() {
- if (!colorDone || !skinDone) return;
- saveLobbyBotTint(id, bot);
+ var serverSlot = lobbyBotThemesFromServer[i];
+ var serverTheme = serverSlot && parseLobbyThemeIndex(serverSlot.themeIndex);
+ var serverSkin = serverSlot && parseLobbySkinIndex(serverSlot.skinToneIndex);
+ if (serverTheme) {
+ applyThemeIndexToOccupant(bot, serverTheme, serverSkin || ((i % 3) + 1), function () {
if (bot.characterId) preloadLobbyTintedCharacter(bot.characterId, bot.colorTheme, bot.colorSkin, function () {});
- }
- rlSampleSwatch('color', botColorIdx, function (rgb) {
- colorDone = true;
- if (rgb) {
- bot.colorTheme = rgb;
- if (mapData && canvas) drawLobbyMap();
- }
- maybeFinishBotTint();
+ if (mapData && canvas) drawLobbyMap();
});
- rlSampleSwatch('skin', botSkinIdx, function (rgb) {
- skinDone = true;
- if (rgb) {
- bot.colorSkin = rgb;
- if (mapData && canvas) drawLobbyMap();
- }
- maybeFinishBotTint();
+ } else {
+ var taken = {};
+ getLobbyTakenThemeIndices(null).forEach(function (t) { taken[t] = true; });
+ var botColorIdx = null;
+ for (var cTry = 1; cTry <= 8; cTry++) {
+ if (!taken[cTry]) { botColorIdx = cTry; break; }
+ }
+ if (!botColorIdx) botColorIdx = ((i % 8) + 1);
+ var botSkinIdx = (i % 3) + 1;
+ applyThemeIndexToOccupant(bot, botColorIdx, botSkinIdx, function () {
+ if (bot.characterId) preloadLobbyTintedCharacter(bot.characterId, bot.colorTheme, bot.colorSkin, function () {});
+ if (mapData && canvas) drawLobbyMap();
});
}
}
@@ -2627,7 +2789,13 @@
if (lobbyBotSlotCount === 0 && maxPlayers > 0 && maxPlayers < 6) {
lobbyBotSlotCount = 6 - maxPlayers;
}
- if (lobbyBotSlotCount > 0) syncLobbyCaseBots();
+ if (Array.isArray(res.lobbyBotThemes)) lobbyBotThemesFromServer = res.lobbyBotThemes;
+ applyAllPeerThemesFromServer(function () {
+ rlResolveMyColors(function () {
+ if (lobbyBotSlotCount > 0) syncLobbyCaseBots();
+ if (mapData && canvas) drawLobbyMap();
+ });
+ });
updatePlayersHud();
syncLobbyBUiChrome();
@@ -2646,7 +2814,7 @@
ensureReadyControlEnabled();
if (res.cardMinigames) setSuspectCardMinigames(res.cardMinigames);
- if (Array.isArray(res.suspectProgress)) setSuspectProgress(res.suspectProgress);
+ applyPersonalSuspectState(res);
serverSuspectPhaseActive = !!res.suspectPhaseActive;
if (res.suspectPhaseActive) {
openSuspectOverlay(res.suspectPickIndex != null ? res.suspectPickIndex : 0);
@@ -2704,9 +2872,16 @@
const LERP = 0.2;
socket.on('user-joined', (data) => {
peers.set(data.id, normalizeLobbyPeerFromServer(data, mapData));
- updatePlayersHud();
- renderPeers();
- redrawLobbyMap();
+ applyPeerThemeFromServer(peers.get(data.id), data.lobbyColorThemeIndex, data.lobbySkinToneIndex, function () {
+ updatePlayersHud();
+ renderPeers();
+ redrawLobbyMap();
+ if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
+ });
+ });
+
+ socket.on('lobby-tint-sync', function (data) {
+ applyLobbyTintSyncPayload(data || {});
});
socket.on('user-left', (data) => {
@@ -2715,6 +2890,7 @@
updatePlayersHud();
renderPeers();
redrawLobbyMap();
+ if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
});
socket.on('host-console-kicked', (data) => {
@@ -2726,6 +2902,7 @@
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 (Array.isArray(data.lobbyBotThemes)) lobbyBotThemesFromServer = data.lobbyBotThemes;
if (data.botSlotCount != null) {
lobbyBotSlotCount = Math.max(0, parseInt(data.botSlotCount, 10) || 0);
syncLobbyCaseBots();
@@ -2733,6 +2910,7 @@
updatePlayersHud();
refreshHostConsoleOverlayIfOpen();
if (mapData && canvas) drawLobbyMap();
+ if (typeof window.refreshCustomizeColorRow === 'function') window.refreshCustomizeColorRow();
});
socket.on('peer-ready', (data) => {
@@ -2985,6 +3163,12 @@
applyDetectiveReturnToLobbyB(data || {});
});
+ socket.on('lobby-evidence-sync', function (data) {
+ applyPersonalSuspectState(data || {});
+ applySuspectProgressVisual();
+ if (lobbyEvidenceSuspectIdx != null) renderLobbyEvidenceCards(lobbyEvidenceSuspectIdx);
+ });
+
socket.on('lobby-interact', (data) => {
if (!data || data.x == null || data.y == null) return;
lobbyInteractPulse = { x: data.x, y: data.y, until: Date.now() + 700 };
@@ -3636,6 +3820,16 @@
return ov;
}
+ function formatLobbyEvCardDescHtml(body) {
+ var text = String(body || '').trim();
+ var lines = text ? text.split(/\n+/).map(function (s) { return s.trim(); }).filter(Boolean) : [];
+ if (!lines.length) lines.push('—');
+ if (!lines.some(function (ln) { return ln.indexOf('เครื่องมือสืบ') >= 0; })) {
+ lines.push('เครื่องมือสืบเสาะเคยถือไว้ครอบครอง');
+ }
+ return lines.join('
');
+ }
+
function evLobbyEvidenceCardEl(c, linkName, cardIdx) {
var rar = String(c.rarity || 'common').toLowerCase();
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
@@ -3654,8 +3848,8 @@
'