update flow design 1.2

This commit is contained in:
2026-05-27 16:31:13 +00:00
parent 27ca643995
commit 121eb695b9
10 changed files with 1289 additions and 271 deletions
+61 -17
View File
@@ -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;
}
@@ -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;
}
+104 -95
View File
@@ -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;
+47 -14
View File
@@ -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 = '' +
'<img class="customize-popup-tab-img" src="' + asset(tabImageName(i, active)) + '" alt="" decoding="async">' +
'<img class="customize-popup-tab-img" src="' + asset(tabImageName(i)) + '" alt="" decoding="async">' +
'<img class="customize-popup-tab-btn-line" src="' + asset('tab-line.png') + '" alt="" decoding="async">';
(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;
})();
+191 -26
View File
@@ -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.080.96 = ดกลางจอตองถงภายในแถบ FINISH อนหย (าเร ~0.56 ใกลกลหยขวาเส FINISH บนงานศลป)
* Map tuning (mno9kb07): finishStopViewportFrac 0.080.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();
}
/** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */
+400 -61
View File
@@ -158,6 +158,8 @@
let suspectCardMinigames = [];
/** หลักฐาน (ติกถูก) ที่เก็บได้ต่อผู้ต้องสงสัย 0–2 (เล่นมินิเกมจบ +1 ต่อช่อง สูงสุด 3) */
let suspectProgress = [0, 0, 0];
/** การ์ดหลักฐานที่ผู้เล่นคนนี้เก็บได้ — [suspectIdx][cardIdx 02] จากเซิร์ฟ */
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('<br>');
}
function evLobbyEvidenceCardEl(c, linkName, cardIdx) {
var rar = String(c.rarity || 'common').toLowerCase();
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
@@ -3654,8 +3848,8 @@
'<div class="en"></div>' +
'</div>' +
'</div>' +
'<div class="ev-card-art" aria-hidden="true"></div>' +
'<p class="ev-card-desc"></p>' +
'<div class="ev-card-art" aria-hidden="true">[การ์ดยังไม่มี PNG]</div>' +
'<div class="ev-card-desc"></div>' +
'<div class="ev-card-link-wrap">' +
'<div class="ev-card-link">' +
'<span class="label">Suspect Link</span>' +
@@ -3671,13 +3865,81 @@
card.querySelector('.icon').textContent = icon;
card.querySelector('.th').textContent = c.titleTh || '';
card.querySelector('.en').textContent = c.titleEn ? '(' + c.titleEn + ')' : '';
card.querySelector('.ev-card-desc').textContent = c.body || '';
card.querySelector('.ev-card-desc').innerHTML = formatLobbyEvCardDescHtml(c.body);
card.querySelector('.stars').textContent = stars + ' (' + LOBBY_EVIDENCE_RARITY[rar] + ')';
card.querySelector('.avatar').textContent = nm.charAt(0);
card.querySelector('.sname').textContent = nm;
return card;
}
var DETECTIVE_EVIDENCE_STORAGE_KEY = 'justiceDetectiveMyEvidence';
function normalizeEvidenceSlotList(cards) {
if (!Array.isArray(cards)) return [];
var n = Math.min(3, cards.length);
var out = [];
for (var i = 0; i < n; i++) out.push(i);
return out;
}
function getSuspectEvidenceCount(si) {
var i = Math.floor(Number(si));
if (Number.isNaN(i) || i < 0 || i > 2) return 0;
return Math.min(3, (myPlayerEvidence[i] || []).length);
}
function canInvestigateSuspect(si) {
return getSuspectEvidenceCount(si) < 3;
}
function persistMyPlayerEvidenceSession() {
try {
var hasAny = (myPlayerEvidence || []).some(function (r) { return r && r.length; });
if (hasAny) sessionStorage.setItem(DETECTIVE_EVIDENCE_STORAGE_KEY, JSON.stringify(myPlayerEvidence));
else sessionStorage.removeItem(DETECTIVE_EVIDENCE_STORAGE_KEY);
} catch (e) { /* ignore */ }
}
function applyMyPlayerEvidence(data) {
if (!Array.isArray(data)) return;
myPlayerEvidence = [0, 1, 2].map(function (si) {
var row = data[si];
if (!Array.isArray(row)) return [];
return normalizeEvidenceSlotList(row);
});
persistMyPlayerEvidenceSession();
}
/** ใช้เฉพาะตอน server ไม่ได้ส่ง myPlayerEvidence มาเลย (เช่น event ทั่วไป) — server เป็น source of truth */
function restoreMyPlayerEvidenceFromSession() {
try {
var raw = sessionStorage.getItem(DETECTIVE_EVIDENCE_STORAGE_KEY);
if (!raw) return false;
var parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return false;
applyMyPlayerEvidence(parsed);
return true;
} catch (e) { return false; }
}
function applyPersonalSuspectState(data) {
/* server เป็นความจริงเสมอ — ห้าม merge กับ session (กันเครื่องหมายถูกตกค้าง) */
if (data && Array.isArray(data.myPlayerEvidence)) {
applyMyPlayerEvidence(data.myPlayerEvidence);
} else if (!myPlayerEvidence || !myPlayerEvidence.some(function (r) { return r && r.length; })) {
restoreMyPlayerEvidenceFromSession();
}
if (data && Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
else if (myPlayerEvidence && myPlayerEvidence.some(function (row) { return row && row.length; })) {
setSuspectProgress([0, 1, 2].map(function (i) { return Math.min(3, (myPlayerEvidence[i] || []).length); }));
}
applySuspectProgressVisual();
if (typeof lobbyEvidenceSuspectIdx === 'number') renderLobbyEvidenceCards(lobbyEvidenceSuspectIdx);
}
/* ล้าง session เก่าเมื่อโหลดหน้านี้ — กันเครื่องหมายตกค้างข้ามคดี/รีเฟรช (server จะเติมให้เองถ้ามี) */
try { sessionStorage.removeItem(DETECTIVE_EVIDENCE_STORAGE_KEY); } catch (e) { /* ignore */ }
function renderLobbyEvidenceCards(suspectIdx) {
const root = document.getElementById('lobby-evidence-cards-root');
if (!root) return;
@@ -3687,9 +3949,17 @@
const s = suspects[si];
root.textContent = '';
if (!s || !s.cards) return;
s.cards.forEach((c, cardIdx) => {
root.appendChild(evLobbyEvidenceCardEl(c, s.linkName, cardIdx));
});
var count = getSuspectEvidenceCount(si);
for (var slot = 0; slot < count; slot++) {
if (!s.cards[slot]) continue;
root.appendChild(evLobbyEvidenceCardEl(s.cards[slot], s.linkName, slot));
}
if (!root.children.length) {
const empty = document.createElement('p');
empty.className = 'ev-empty-hint';
empty.textContent = 'ยังไม่มีหลักฐาน — เล่นมินิเกมสืบสวน 1 ครั้งเพื่อรับการ์ด 1 ใบ';
root.appendChild(empty);
}
}
let lobbyEvidenceSuspectIdx = 0;
@@ -4007,16 +4277,33 @@
if (suspectPickOverlayOpen) updateSuspectHostUi(); // อัปเดตปุ่มชี้ตัว (โชว์เมื่อสืบครบ)
}
/** วางติกถูกในช่อง "หลักฐาน" (.suspect-symbol-list) ตามจำนวนที่เก็บได้ */
/** วาช่องหลักฐานใต้การ์ดผู้ต้องสงสัย — โชว์ bg 3 ใบเสมอ; เก็บได้กี่ใบ เปลี่ยนเป็น symbol-card.png ซ้าย→ขวา */
function applySuspectProgressVisual() {
for (var i = 0; i <= 2; i++) {
var card = document.querySelector('.suspect-card[data-index="' + i + '"]');
if (!card) continue;
var slots = card.querySelectorAll('.suspect-symbol-list img');
var done = Math.max(0, Math.min(3, (suspectProgress && suspectProgress[i]) || 0));
for (var s = 0; s < slots.length; s++) {
var want = (s < done) ? SUSPECT_EVID_FULL : SUSPECT_EVID_EMPTY;
if (slots[s].getAttribute('src') !== want) slots[s].src = want;
var count = getSuspectEvidenceCount(i);
if (!count && suspectProgress && suspectProgress[i] > 0) {
count = Math.min(3, suspectProgress[i]);
myPlayerEvidence[i] = normalizeEvidenceSlotList(
(myPlayerEvidence[i] || []).length ? myPlayerEvidence[i] : new Array(count)
);
}
card.classList.toggle('suspect-card--evidence-full', count >= 3);
var list = card.querySelector('.suspect-symbol-list');
if (!list) continue;
list.classList.remove('suspect-symbol-list--mini-cards');
list.textContent = '';
for (var slot = 0; slot < 3; slot++) {
var sym = document.createElement('img');
var collected = slot < count;
sym.src = collected ? SUSPECT_EVID_FULL : SUSPECT_EVID_EMPTY;
sym.alt = '';
sym.width = 47;
sym.height = 61;
sym.decoding = 'async';
sym.setAttribute('title', collected ? ('หลักฐานชิ้นที่ ' + (slot + 1)) : 'ยังไม่มีหลักฐาน');
list.appendChild(sym);
}
}
}
@@ -4167,7 +4454,7 @@
myTrialVote = null;
trialVoteCounts = [0, 0, 0];
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (data && Array.isArray(data.suspectProgress)) suspectProgress = [0, 1, 2].map(function (i) { return Math.min(3, Math.max(0, Math.floor(Number(data.suspectProgress[i])) || 0)); });
applyPersonalSuspectState(data);
injectTrialStyle();
var ov = document.getElementById('suspect-pick-overlay');
if (!ov) return;
@@ -4313,7 +4600,7 @@
}
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select' };
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {} };
var SUSPECT_NAMES_EN = ['SOMCHAI', 'ARIN', 'JIRAPHA'];
function ensureTestimonyStyle() {
@@ -4324,7 +4611,7 @@
link.rel = 'stylesheet';
document.head.appendChild(link);
}
link.href = 'css/testimony-overlay.css?v=5';
link.href = 'css/testimony-overlay.css?v=8';
}
var testimonyScaleBound = false;
@@ -4338,8 +4625,12 @@
var h = wrap.clientHeight || window.innerHeight;
var scale = Math.min(w / dw, h / dh);
if (!Number.isFinite(scale) || scale <= 0) scale = 1;
stage.style.transform = 'scale(' + scale + ')';
stage.style.marginBottom = ((dh * scale) - dh) + 'px';
var scaledW = dw * scale;
var scaledH = dh * scale;
var tx = Math.max(0, (w - scaledW) / 2);
var ty = Math.max(0, (h - scaledH) / 2);
stage.style.transform = 'translate(' + tx + 'px, ' + ty + 'px) scale(' + scale + ')';
stage.style.marginBottom = '0';
}
function bindTestimonyScale() {
@@ -4459,6 +4750,8 @@
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, 2) }, function (res) {
if (res && res.ok) {
tmState.submitted = true;
if (!tmState.submittedPicksById) tmState.submittedPicksById = {};
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, 2);
es3SetStatus('ส่งหลักฐานแล้ว — รอผู้เล่นอื่น...');
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = true;
} else appendLobbySystemChat('— ส่งหลักฐานไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
@@ -4521,11 +4814,32 @@
return card;
}
function testimonyPicksForMember(picksMap, memberId) {
if (!picksMap || memberId == null || memberId === '') return [];
var sel = picksMap[memberId];
if (!sel) sel = picksMap[String(memberId)];
if (!sel && typeof memberId === 'string') {
Object.keys(picksMap).forEach(function (k) {
if (sel && sel.length) return;
if (k === memberId || String(k) === String(memberId)) sel = picksMap[k];
});
}
if (!sel) return [];
if (!Array.isArray(sel)) return [];
var out = [];
sel.forEach(function (ci) {
var idx = Math.floor(Number(ci));
if (idx >= 0 && out.indexOf(idx) === -1) out.push(idx);
});
return out;
}
function esRenderRevealPlayers(members, picks, suspectData) {
var root = document.getElementById('es-players');
if (!root) return;
root.textContent = '';
var sd = suspectData || { linkName: '?', cards: [] };
var cards = sd.cards || [];
(members || []).forEach(function (m) {
var col = document.createElement('div');
col.className = 'es-player';
@@ -4536,7 +4850,7 @@
nameEl.className = 'es-player-name';
nameEl.innerHTML = crown + nick;
col.appendChild(nameEl);
var sel = (picks && picks[m.id]) || [];
var sel = testimonyPicksForMember(picks, m.id);
if (!sel.length) {
var none = document.createElement('div');
none.className = 'es-rev-none';
@@ -4544,9 +4858,15 @@
col.appendChild(none);
} else {
sel.forEach(function (ci) {
var c = (sd.cards || [])[ci];
var c = cards[ci];
if (c) col.appendChild(esRevealCardEl(c, sd.linkName, ci));
});
if (!col.querySelector('.es-card')) {
var bad = document.createElement('div');
bad.className = 'es-rev-none';
bad.textContent = '(หลักฐานไม่ตรงรอบ)';
col.appendChild(bad);
}
}
root.appendChild(col);
});
@@ -4609,6 +4929,7 @@
tmState.members = (data && Array.isArray(data.members)) ? data.members : [];
tmState.picks = [];
tmState.submitted = false;
tmState.submittedPicksById = {};
tmState.mode = 'select';
var SC = '/Main-Lobby/IMAGE/Showcard/';
var CS = '/Main-Lobby/IMAGE/Choose%20a%20suspect/';
@@ -4651,6 +4972,7 @@
var ov = ensureTestimonyOverlay();
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
if (data && typeof data.round === 'number') tmState.round = data.round;
if (data && Array.isArray(data.members)) tmState.members = data.members;
var SC = '/Main-Lobby/IMAGE/Showcard/';
var CS = '/Main-Lobby/IMAGE/Choose%20a%20suspect/';
@@ -4667,7 +4989,7 @@
if (susName) susName.textContent = sd.linkName || '';
var rd = document.getElementById('esBtnReady');
if (rd) { rd.disabled = false; rd.classList.remove('is-active'); }
var picks = (data && data.picks) || {};
var picks = Object.assign({}, tmState.submittedPicksById || {}, (data && data.picks) || {});
esRenderRevealPlayers(tmState.members, picks, sd);
es3RenderMembers([], (data && data.ready) || [], true);
requestAnimationFrame(syncTestimonyScale);
@@ -4706,7 +5028,7 @@
const reopenSuspect = !!(data && data.suspectPhaseActive);
const pickIdx = (data && typeof data.suspectPickIndex === 'number') ? data.suspectPickIndex : suspectSelectedIndex;
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (data && Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
applyPersonalSuspectState(data);
applyRoomLobbyBTransition({
mapId: mid,
peersSnap: (data && data.peersSnap) ? data.peersSnap : [],
@@ -4826,13 +5148,24 @@
const canAccuse = isHost && allSuspectsInvestigated();
const actions = document.getElementById('suspect-pick-actions');
const accuseBtn = document.getElementById('suspect-btn-accuse');
const startBtn = document.getElementById('suspect-btn-start');
const hint = document.getElementById('suspect-pick-hint');
const canStartInvestigation = isHost && canInvestigateSuspect(suspectSelectedIndex);
if (startBtn) {
startBtn.disabled = !canStartInvestigation;
startBtn.classList.toggle('suspect-btn-start--disabled', !canStartInvestigation);
startBtn.setAttribute('aria-disabled', canStartInvestigation ? 'false' : 'true');
}
if (hint) {
hint.textContent = isHost
? (allSuspectsInvestigated()
? 'สืบครบทุกผู้ต้องสงสัยแล้ว — กด "ชี้ตัวคนร้าย" เพื่อเข้าสู่การพิจารณาคดี'
: 'เลือกการ์ดผู้ต้องสงสัยแล้วกดเริ่มสืบสวน (ต้องสืบให้ครบทั้ง 3 ก่อนชี้ตัวคนร้าย)')
: 'รอ Host เลือกการ์ดและกดเริ่มสืบสวน';
if (!isHost) {
hint.textContent = 'รอ Host เลือกการ์ดและกดเริ่มสืบสวน';
} else if (!canInvestigateSuspect(suspectSelectedIndex)) {
hint.textContent = 'สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่นเพื่อสืบต่อ';
} else if (allSuspectsInvestigated()) {
hint.textContent = 'สืบครบทุกผู้ต้องสงสัยแล้ว — กด "ชี้ตัวคนร้าย" เพื่อเข้าสู่การพิจารณาคดี';
} else {
hint.textContent = 'เลือกการ์ดผู้ต้องสงสัยแล้วกดเริ่มสืบสวน (สืบคนละ 3 ครั้ง · การ์ดเล็กเติมซ้ายไปขวา)';
}
hint.classList.toggle('is-hidden', false);
}
if (actions) {
@@ -4940,7 +5273,7 @@
serverSuspectPhaseActive = true;
if (data && data.hostId != null) hostId = data.hostId;
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (data && Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
applyPersonalSuspectState(data);
const idx = (data && typeof data.selectedIndex === 'number') ? data.selectedIndex : 0;
openSuspectOverlay(idx);
});
@@ -4987,6 +5320,7 @@
if (idx < 0 || idx > 2) return;
const prevIdx = suspectSelectedIndex;
applySuspectSelectionVisual(idx);
updateSuspectHostUi();
socket.emit('suspect-pick-select', { index: idx });
if (prevIdx !== idx) {
appendLobbySystemChat('— เลือกผู้ต้องสงสัยหมายเลข ' + (idx + 1));
@@ -4998,6 +5332,11 @@
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
const idx = suspectSelectedIndex;
if (idx < 0 || idx > 2) return;
if (!canInvestigateSuspect(idx)) {
appendLobbySystemChat('— สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่น');
updateSuspectHostUi();
return;
}
socket.emit('suspect-pick-start', {}, function (res) {
if (res && res.ok) return;
const err = (res && res.error) ? String(res.error) : (sourceLabel || 'เริ่มสืบสวนไม่สำเร็จ');
@@ -5102,7 +5441,7 @@
renderPeers();
ensureReadyControlEnabled();
if (data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
applyPersonalSuspectState(data);
if (lobbyBotSlotCount > 0) syncLobbyCaseBots();
appendLobbySystemChat('— ย้ายไป LobbyB แล้ว · ห้องนี้ไม่รับผู้เล่นใหม่');
tryTroublesomeLobbyGate();
@@ -5175,7 +5514,7 @@
closeSuspectOverlay();
closeLobbyPreplayWizard();
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (data && Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
applyPersonalSuspectState(data);
if (data && data.quizMode && data.stayInRoomLobby) {
const qMid = (data.mapId != null) ? String(data.mapId).trim() : '';
applyLobbyPeersSnapFromServer(data.peersSnap);
+1 -1
View File
@@ -3214,7 +3214,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0460"></script>
<script src="js/play.js?v=0.0465"></script>
<div class="version-tag">v —</div>
</body>
</html>
+24 -5
View File
@@ -9,10 +9,10 @@
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0174">
<link rel="stylesheet" href="css/profile-popup.css?v=6">
<link rel="stylesheet" href="css/customize-popup.css?v=40">
<link rel="stylesheet" href="css/customize-popup.css?v=42">
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
<link rel="stylesheet" href="css/testimony-overlay.css?v=5">
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=5">
<link rel="stylesheet" href="css/testimony-overlay.css?v=8">
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=12">
<style>
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
#mic-permission-overlay {
@@ -237,6 +237,22 @@
filter: none;
box-shadow: none;
}
.suspect-card--evidence-full:not(.suspect-card--selected) {
opacity: 0.72;
filter: grayscale(0.35) brightness(0.92);
}
.suspect-btn-start--disabled,
.suspect-btn-start:disabled {
opacity: 0.45;
cursor: not-allowed;
pointer-events: none;
filter: grayscale(0.6);
}
.suspect-btn-start--disabled:hover,
.suspect-btn-start:disabled:hover {
transform: none;
filter: grayscale(0.6);
}
.suspect-card-ui {
position: absolute;
left: 61%;
@@ -287,8 +303,11 @@
.suspect-symbol-list {
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 9px;
margin-left: 20px;
min-height: 61px;
min-width: 159px; /* 47*3 + 9*2 — กันแถวยุบเวลายังไม่มีการ์ด */
}
.suspect-symbol-list img {
width: 47px;
@@ -1556,8 +1575,8 @@
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/display-name.js?v=2"></script>
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=29" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/room-lobby.js?v=0.0227"></script>
<script src="js/customize-popup.js?v=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/room-lobby.js?v=0.0243"></script>
<div class="version-tag">v —</div>
</body>
</html>
+374 -39
View File
@@ -27,6 +27,86 @@ const DETECTIVE_MINIGAME_POOL = [
{ key: 'balloon_boss', mapId: 'mnq1eml7', labelTh: 'Minigame-7 ยิง Mega Virus' },
];
const LOBBY_SLOT_TOTAL = 6;
const LOBBY_THEME_COUNT = 8;
const LOBBY_SKIN_COUNT = 3;
/** ธีมสีเสื้อที่ถูกใช้แล้วในห้อง (ผู้เล่น + bot) */
function getTakenLobbyThemeIndices(space, excludePeerId) {
const taken = new Set();
if (!space) return taken;
space.peers.forEach((p, id) => {
if (excludePeerId && id === excludePeerId) return;
const ti = parseInt(p.lobbyColorThemeIndex, 10);
if (ti >= 1 && ti <= LOBBY_THEME_COUNT) taken.add(ti);
});
const bots = space.lobbyBotThemeBySlot;
if (bots && typeof bots === 'object') {
Object.keys(bots).forEach((k) => {
const ti = parseInt(bots[k] && bots[k].themeIndex, 10);
if (ti >= 1 && ti <= LOBBY_THEME_COUNT) taken.add(ti);
});
}
return taken;
}
function pickFreeLobbyThemeIndex(space, excludePeerId) {
const taken = getTakenLobbyThemeIndices(space, excludePeerId);
for (let i = 1; i <= LOBBY_THEME_COUNT; i++) {
if (!taken.has(i)) return i;
}
const n = (space && space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space);
return ((n - 1) % LOBBY_THEME_COUNT) + 1;
}
function pickLobbySkinIndexForSlot(slotIndex) {
const s = Number(slotIndex);
if (!Number.isFinite(s)) return 1;
return (Math.abs(Math.floor(s)) % LOBBY_SKIN_COUNT) + 1;
}
/** จัดสี bot ตามลำดับช่อง — ไม่ซ้ำกับผู้เล่น */
function syncLobbyBotThemeSlots(space) {
if (!space) return [];
const botCount = effectiveBotSlotCount(space);
if (!space.lobbyBotThemeBySlot || typeof space.lobbyBotThemeBySlot !== 'object') {
space.lobbyBotThemeBySlot = {};
}
const taken = getTakenLobbyThemeIndices(space);
const out = [];
for (let i = 0; i < botCount; i++) {
let slot = space.lobbyBotThemeBySlot[i];
let themeIndex = parseInt(slot && slot.themeIndex, 10);
if (!(themeIndex >= 1 && themeIndex <= LOBBY_THEME_COUNT) || taken.has(themeIndex)) {
themeIndex = pickFreeLobbyThemeIndex(space);
}
taken.add(themeIndex);
const skinToneIndex = (() => {
const si = parseInt(slot && slot.skinToneIndex, 10);
return (si >= 1 && si <= LOBBY_SKIN_COUNT) ? si : pickLobbySkinIndexForSlot(i);
})();
space.lobbyBotThemeBySlot[i] = { themeIndex, skinToneIndex };
out.push({ themeIndex, skinToneIndex });
}
Object.keys(space.lobbyBotThemeBySlot).forEach((k) => {
if (parseInt(k, 10) >= botCount) delete space.lobbyBotThemeBySlot[k];
});
return out;
}
function emitLobbyTintSync(spaceId, space) {
const peersTint = [];
space.peers.forEach((p, id) => {
peersTint.push({
id,
lobbyColorThemeIndex: p.lobbyColorThemeIndex || null,
lobbySkinToneIndex: p.lobbySkinToneIndex || null,
});
});
io.to(spaceId).emit('lobby-tint-sync', {
peers: peersTint,
lobbyBotThemes: syncLobbyBotThemeSlots(space),
});
}
/** จำนวน Bot จาก Create Room — ถ้าไม่เคยบันทึก ใช้ 6 − maxPlayers (เช่น 3 คน → 3 Bot) */
function effectiveBotSlotCount(space) {
@@ -1611,6 +1691,22 @@ function shuffleQuizQuestions(arr) {
return a;
}
function ensurePeerLobbyThemes(space) {
if (!space || !space.peers) return;
const seen = new Set();
space.peers.forEach((p, id) => {
let ti = parseInt(p.lobbyColorThemeIndex, 10);
if (!(ti >= 1 && ti <= LOBBY_THEME_COUNT) || seen.has(ti)) {
ti = pickFreeLobbyThemeIndex(space, id);
p.lobbyColorThemeIndex = ti;
}
seen.add(ti);
if (!parseInt(p.lobbySkinToneIndex, 10) || p.lobbySkinToneIndex < 1 || p.lobbySkinToneIndex > LOBBY_SKIN_COUNT) {
p.lobbySkinToneIndex = pickLobbySkinIndexForSlot(p.spawnJoinOrder || 0);
}
});
}
function buildPeersSnapForSpace(space, extraFields) {
return [...space.peers.values()].map((p) => {
const row = {
@@ -1621,6 +1717,8 @@ function buildPeersSnapForSpace(space, extraFields) {
nickname: p.nickname,
ready: !!p.ready,
characterId: p.characterId ?? null,
lobbyColorThemeIndex: p.lobbyColorThemeIndex || null,
lobbySkinToneIndex: p.lobbySkinToneIndex || null,
};
if (extraFields && extraFields.gauntlet) {
row.gauntletJumpTicks = p.gauntletJumpTicks || 0;
@@ -1667,8 +1765,8 @@ function assignDetectiveSuspectCardMinigames(space) {
gameType: (md && md.gameType) || '',
};
});
// ชุดการ์ดใหม่ = เริ่มนับหลักฐาน (ติกถูก) ใหม่ทั้งหมด
space.suspectProgress = [0, 0, 0];
// ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้
space.playerEvidence = {};
// สุ่มคนร้ายตัวจริง 1 ใน 3 ใบ (เก็บฝั่ง server ไม่ส่งให้ client จนกว่าจะเปิดเผย)
space.culpritIndex = Math.floor(Math.random() * 3);
space.trialPhase = null; // null | 'voting' | 'revealed'
@@ -1676,6 +1774,151 @@ function assignDetectiveSuspectCardMinigames(space) {
space.trialScores = space.trialScores || {};
}
function ensureSpacePlayerEvidence(space) {
if (!space.playerEvidence || typeof space.playerEvidence !== 'object') space.playerEvidence = {};
if (!space.evidenceByNick || typeof space.evidenceByNick !== 'object') space.evidenceByNick = {};
}
function normalizeEvidenceSlotList(cards) {
if (!Array.isArray(cards)) return [];
const n = Math.min(3, cards.length);
const out = [];
for (let i = 0; i < n; i++) out.push(i);
return out;
}
/** ดึง nickname normalized ของผู้เล่นบน space (จากแถว peers) — ใช้คีย์เก็บหลักฐานข้ามการ reconnect */
function peerNickKey(space, playerId) {
if (!space || !playerId) return '';
const p = space.peers && space.peers.get ? space.peers.get(playerId) : null;
if (!p || !p.nickname) return '';
return normalizeLobbyNickname(p.nickname);
}
/** สำเนาหลักฐาน — fallback ไป evidenceByNick ถ้าไม่มีตาม socket.id (กรณี reconnect socket ใหม่) */
function clonePlayerEvidence(space, playerId) {
ensureSpacePlayerEvidence(space);
let row = space.playerEvidence[playerId];
if (!row || !row.length) {
const nk = peerNickKey(space, playerId);
if (nk && space.evidenceByNick[nk]) {
row = space.evidenceByNick[nk];
space.playerEvidence[playerId] = row;
}
}
if (!row) row = [[], [], []];
return [0, 1, 2].map((i) => normalizeEvidenceSlotList(row[i]));
}
/** ซิงค์หลักฐานของผู้เล่นปัจจุบันลง evidenceByNick — เรียกหลังการ์ดเปลี่ยน */
function syncEvidenceByNick(space, playerId) {
ensureSpacePlayerEvidence(space);
const nk = peerNickKey(space, playerId);
if (!nk) return;
const row = space.playerEvidence[playerId];
if (Array.isArray(row)) space.evidenceByNick[nk] = row;
}
/** จำนวนหลักฐานที่เก็บได้ต่อผู้ต้องสงสัย (0–3) — ใช้แสดงติกบนการ์ด suspect */
function playerSuspectProgressFromEvidence(space, playerId) {
const row = clonePlayerEvidence(space, playerId);
return row.map((arr) => Math.min(3, arr.length));
}
/** มอบการ์ดหลักฐานชิ้นถัดไป — เรียงซ้าย→ขวา (0 แล้ว 1 แล้ว 2) ไม่สุ่มช่อง */
function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex) {
if (suspectIndex < 0 || suspectIndex > 2) return null;
ensureSpacePlayerEvidence(space);
/* เผื่อ rejoin: ดึงหลักฐานเก่าจาก nickname ถ้ามี ก่อนเพิ่มใบใหม่ */
if (!space.playerEvidence[playerId]) {
const nk = peerNickKey(space, playerId);
space.playerEvidence[playerId] = (nk && space.evidenceByNick[nk])
? space.evidenceByNick[nk]
: [[], [], []];
}
if (!Array.isArray(space.playerEvidence[playerId][suspectIndex])) {
space.playerEvidence[playerId][suspectIndex] = [];
}
const list = normalizeEvidenceSlotList(space.playerEvidence[playerId][suspectIndex]);
if (list.length >= 3) {
space.playerEvidence[playerId][suspectIndex] = list;
syncEvidenceByNick(space, playerId);
return null;
}
const pick = list.length;
list.push(pick);
space.playerEvidence[playerId][suspectIndex] = list;
syncEvidenceByNick(space, playerId);
return pick;
}
function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds) {
const out = {};
(playerIds || []).forEach((pid) => {
if (!space.peers.has(pid)) return;
const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex);
if (card != null) out[pid] = card;
});
return out;
}
function resolveDetectiveSuspectAwardIndex(space) {
if (typeof space.suspectActiveIndex === 'number' && space.suspectActiveIndex >= 0 && space.suspectActiveIndex <= 2) {
return space.suspectActiveIndex;
}
if (typeof space.lastDetectiveInvestigatedSuspect === 'number'
&& space.lastDetectiveInvestigatedSuspect >= 0 && space.lastDetectiveInvestigatedSuspect <= 2) {
return space.lastDetectiveInvestigatedSuspect;
}
if (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) {
return space.suspectPickIndex;
}
return -1;
}
/** มอบการ์ดหลักฐาน 1 ใบต่อผู้เล่นเมื่อจบมินิเกม (ครั้งเดียวต่อรอบ — ไม่ขึ้นเกรด) */
function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
const suspectIdx = resolveDetectiveSuspectAwardIndex(space);
if (suspectIdx < 0) return { suspectIdx: -1, awarded: {} };
if (space.detectiveAwardDoneForRun) {
return { suspectIdx, awarded: {}, alreadyGranted: true };
}
const ids = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()];
const awarded = awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, ids);
space.detectiveAwardDoneForRun = true;
space.lastDetectiveInvestigatedSuspect = suspectIdx;
emitLobbyEvidenceSync(sid, space);
return { suspectIdx, awarded, alreadyGranted: false };
}
function emitLobbyEvidenceSync(sid, space) {
if (!sid || !space) return;
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('lobby-evidence-sync', {
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
}
function emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload) {
const msg = message || 'จบมินิเกม — กลับ LobbyB';
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
const personal = {
...basePayload,
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
};
sock.emit('quiz-ended', { message: msg, returnToLobbyB: true });
sock.emit('detective-minigame-ended', personal);
sock.emit('game-start', personal);
});
}
/** เริ่มมินิเกมตามการ์ดที่เลือก (หลังกดเริ่มสืบสวน) */
function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
const mapId = cardEntry && cardEntry.mapId ? String(cardEntry.mapId).trim() : '';
@@ -1684,6 +1927,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
space.suspectPhaseActive = false;
space.detectiveMinigameActive = true;
space.detectiveAwardDoneForRun = false;
space.detectiveMinigameCardIndex = cardEntry.cardIndex;
// จำว่ากำลังสืบผู้ต้องสงสัยคนไหน เพื่อเติมหลักฐาน (ติกถูก) ตอนเล่นจบ
space.suspectActiveIndex = (typeof selectedIndex === 'number' && selectedIndex >= 0 && selectedIndex <= 2)
@@ -1777,7 +2021,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
return { ok: true, mapId, gameType: md.gameType };
}
function returnDetectiveSpaceToLobbyB(sid, space, message) {
function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
ensurePostCaseLobbyMapLoaded();
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
@@ -1786,11 +2030,22 @@ function returnDetectiveSpaceToLobbyB(sid, space, message) {
space.gauntletReturnMapId = null;
stopGauntletTicker(space);
space.gauntletRun = null;
// เล่นมินิเกมจบ → เติมหลักฐาน (ติกถูก) 1 ช่องให้ผู้ต้องสงสัยที่กำลังสืบ (สะสมได้สูงสุด 3)
if (!Array.isArray(space.suspectProgress) || space.suspectProgress.length !== 3) space.suspectProgress = [0, 0, 0];
const _ai = (typeof space.suspectActiveIndex === 'number') ? space.suspectActiveIndex : -1;
if (_ai >= 0 && _ai <= 2) {
space.suspectProgress[_ai] = Math.min(3, (space.suspectProgress[_ai] || 0) + 1);
const suspectIdx = resolveDetectiveSuspectAwardIndex(space);
if (suspectIdx >= 0) {
space.lastDetectiveInvestigatedSuspect = suspectIdx;
let awardIds = [];
if (awardOptions && awardOptions.allPlayers) {
awardIds = [...space.peers.keys()];
} else if (awardOptions && awardOptions.playerId && space.peers.has(awardOptions.playerId)) {
awardIds = [awardOptions.playerId];
} else if (awardOptions && Array.isArray(awardOptions.playerIds) && awardOptions.playerIds.length) {
awardIds = awardOptions.playerIds.filter((id) => space.peers.has(id));
}
/* ไม่มอบการ์ดเมื่อกลับ LobbyB โดยไม่ระบุ (เช่น refresh room-lobby ระหว่างมินิเกม) */
if (!space.detectiveAwardDoneForRun && awardIds.length) {
awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, awardIds);
space.detectiveAwardDoneForRun = true;
}
}
space.suspectActiveIndex = null;
const lb = maps.get(POST_CASE_LOBBY_SPACE_ID);
@@ -1813,19 +2068,16 @@ function returnDetectiveSpaceToLobbyB(sid, space, message) {
p.gauntletEliminated = false;
});
const peersSnap = buildPeersSnapForSpace(space);
const payload = {
const basePayload = {
stayInRoomLobby: true,
mapId: POST_CASE_LOBBY_SPACE_ID,
peersSnap,
suspectPhaseActive: true,
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
detectiveReturn: true,
};
io.to(sid).emit('quiz-ended', { message: message || 'จบมินิเกม — กลับ LobbyB', returnToLobbyB: true });
io.to(sid).emit('detective-minigame-ended', payload);
io.to(sid).emit('game-start', payload);
emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload);
}
/* ===== ขั้นพิจารณาคดี — โหวตชี้ตัวคนร้าย ===== */
@@ -1881,14 +2133,22 @@ function emitTestimonyOpen(sid, space) {
space.testimonySubmitted = {};
space.testimonyReadyNext = {};
space.testimonyRevealed = false;
io.to(sid).emit('testimony-open', {
const base = {
round: space.testimonyRound,
suspectIndex: space.testimonyRound,
members: buildTestimonyMembers(space),
totalPlayers: space.peers.size,
hostId: space.hostId,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
};
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('testimony-open', {
...base,
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
}
@@ -1915,10 +2175,15 @@ function advanceTestimony(sid, space) {
space.testimonyActive = false;
space.trialPhase = 'voting';
space.trialVotes = {};
io.to(sid).emit('trial-open', {
hostId: space.hostId,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('trial-open', {
hostId: space.hostId,
cardMinigames: space.suspectCardMinigames || [],
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
emitTrialVoteUpdate(sid, space);
} else {
@@ -1930,7 +2195,7 @@ function endQuizGame(sid, space, message) {
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
if (space.detectiveMinigameActive) {
returnDetectiveSpaceToLobbyB(sid, space, message);
returnDetectiveSpaceToLobbyB(sid, space, message, { allPlayers: true });
space.quizSession = null;
return;
}
@@ -3800,7 +4065,7 @@ function endGauntletGame(sid, space, reason) {
if (!md || md.gameType !== 'gauntlet') return;
if (space.detectiveMinigameActive) {
const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' : 'จบมินิเกม';
returnDetectiveSpaceToLobbyB(sid, space, msg);
returnDetectiveSpaceToLobbyB(sid, space, msg, { allPlayers: true });
return;
}
stopGauntletTicker(space);
@@ -4318,6 +4583,7 @@ io.on('connection', (socket) => {
socket.data.spaceId = spaceId;
if (!space.hostId) space.hostId = socket.id;
if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space);
ensurePeerLobbyThemes(space);
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
const spawnJoinOrder = space.peers.size;
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
@@ -4325,6 +4591,8 @@ io.on('connection', (socket) => {
const peer = {
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
spawnJoinOrder,
lobbyColorThemeIndex: pickFreeLobbyThemeIndex(space),
lobbySkinToneIndex: pickLobbySkinIndexForSlot(spawnJoinOrder),
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0,
balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
};
@@ -4383,8 +4651,10 @@ io.on('connection', (socket) => {
suspectPhaseActive: !!space.suspectPhaseActive && serverMapIsPostCaseLobbyB(space),
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
botSlotCount: effectiveBotSlotCount(space),
lobbyBotThemes: syncLobbyBotThemeSlots(space),
joinLocked: !!space.joinLocked,
};
if (mdJoin.gameType === 'quiz_carry' || spaceAllowsQuizCarryLobbyRelaxed(space)) {
@@ -4419,6 +4689,7 @@ io.on('connection', (socket) => {
joinCb.gauntletCrownRunHeld = !!space.gauntletRun.crownRunHeld;
}
if (typeof cb === 'function') cb(joinCb);
emitLobbyTintSync(spaceId, space);
socket.to(spaceId).emit('user-joined', peer);
if (space.voiceInits) {
for (const [peerId, initData] of Object.entries(space.voiceInits)) {
@@ -4427,6 +4698,34 @@ io.on('connection', (socket) => {
}
});
/** เปลี่ยนสีเสื้อ/ผิวใน lobby — ห้ามเลือกสีที่คนอื่นใช้แล้ว */
socket.on('lobby-tint-update', (data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
const peer = space && space.peers.get(socket.id);
if (!space || !peer) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
let themeIndex = Math.floor(Number(data && data.themeIndex));
let skinIndex = Math.floor(Number(data && data.skinToneIndex));
if (!Number.isFinite(themeIndex) || themeIndex < 1 || themeIndex > LOBBY_THEME_COUNT) {
return reply({ ok: false, error: 'ธีมสีไม่ถูกต้อง' });
}
if (!Number.isFinite(skinIndex) || skinIndex < 1 || skinIndex > LOBBY_SKIN_COUNT) {
skinIndex = peer.lobbySkinToneIndex || 1;
}
const taken = getTakenLobbyThemeIndices(space, socket.id);
if (taken.has(themeIndex)) return reply({ ok: false, error: 'สีเสื้อนี้มีคนใช้แล้ว' });
peer.lobbyColorThemeIndex = themeIndex;
peer.lobbySkinToneIndex = skinIndex;
emitLobbyTintSync(sid, space);
reply({
ok: true,
lobbyColorThemeIndex: themeIndex,
lobbySkinToneIndex: skinIndex,
lobbyBotThemes: space.lobbyBotThemeBySlot ? syncLobbyBotThemeSlots(space) : [],
});
});
/** เปลี่ยนชื่อที่แสดงใน lobby / เกม (ซิงก์ทุก client ในห้อง) */
socket.on('update-display-name', ({ nickname }, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
@@ -4493,12 +4792,20 @@ io.on('connection', (socket) => {
assignDetectiveSuspectCardMinigames(space);
}
space.suspectPhaseActive = true;
io.to(sid).emit('suspect-phase-open', {
const phaseBase = {
hostId: space.hostId,
selectedIndex: space.suspectPickIndex,
disruptorAccepted: accept,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
};
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('suspect-phase-open', {
...phaseBase,
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
socket.emit('troublesome-ack', { ok: true, accept });
});
@@ -4534,6 +4841,11 @@ io.on('connection', (socket) => {
assignDetectiveSuspectCardMinigames(space);
}
const selectedIndex = typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0;
const myEvidence = clonePlayerEvidence(space, socket.id);
const ownedCount = (myEvidence[selectedIndex] || []).length;
if (ownedCount >= 3) {
return reply({ ok: false, error: 'สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่น' });
}
const cardEntry = space.suspectCardMinigames[selectedIndex];
if (!cardEntry || !cardEntry.mapId) {
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
@@ -4550,11 +4862,26 @@ io.on('connection', (socket) => {
return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
}
if (!space.detectiveMinigameActive) {
if (serverMapIsPostCaseLobbyB(space)) return reply({ ok: true, alreadyOnLobbyB: true });
if (serverMapIsPostCaseLobbyB(space)) {
const grant = grantDetectiveEvidenceForCurrentRun(sid, space, [socket.id]);
return reply({
ok: true,
alreadyOnLobbyB: true,
suspectIndex: grant.suspectIdx,
awardedCard: grant.awarded && grant.awarded[socket.id] != null ? grant.awarded[socket.id] : null,
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
});
}
return reply({ ok: false, error: 'ไม่ได้อยู่ในมินิเกมสืบสวน' });
}
returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB');
reply({ ok: true });
// เล่นมินิเกม 1 ครั้ง = ได้การ์ด 1 ใบ (ไม่ขึ้นเกรด/คะแนน)
returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB', { allPlayers: true });
reply({
ok: true,
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
});
});
// Host เปิดขั้นพิจารณาคดี (ต้องสืบครบ 3 ผู้ต้องสงสัย)
@@ -4564,8 +4891,8 @@ io.on('connection', (socket) => {
const space = sid ? spaces.get(sid) : null;
if (!space || !serverMapIsPostCaseLobbyB(space)) return reply({ ok: false, error: 'ต้องอยู่โถง LobbyB' });
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host เปิดพิจารณาคดีได้' });
const prog = Array.isArray(space.suspectProgress) ? space.suspectProgress : [0, 0, 0];
if (![0, 1, 2].every((i) => (prog[i] || 0) >= 1)) return reply({ ok: false, error: 'ต้องสืบครบทั้ง 3 ผู้ต้องสงสัยก่อน' });
const prog = playerSuspectProgressFromEvidence(space, socket.id);
if (![0, 1, 2].every((i) => (prog[i] || 0) >= 1)) return reply({ ok: false, error: 'ต้องสืบครบทั้ง 3 ผู้ต้องสงสัยก่อน (เก็บหลักฐานอย่างน้อย 1 ใบต่อคน)' });
if (typeof space.culpritIndex !== 'number') space.culpritIndex = Math.floor(Math.random() * 3);
space.suspectPhaseActive = false;
// เริ่ม "ห้องสรุปหลักฐาน" (ไต่สวน 3 ปากคำ) ก่อนเข้าโหวต
@@ -4667,12 +4994,15 @@ io.on('connection', (socket) => {
total = humans + bots;
space.botSlotCount = bots;
space.maxPlayers = Math.max(humans, total - bots);
const lobbyBotThemes = syncLobbyBotThemeSlots(space);
io.to(sid).emit('host-console-settings', {
maxPlayers: space.maxPlayers,
botSlotCount: space.botSlotCount,
totalSlots: total
totalSlots: total,
lobbyBotThemes,
});
cb && cb({ ok: true, maxPlayers: space.maxPlayers, botSlotCount: space.botSlotCount, totalSlots: total });
emitLobbyTintSync(sid, space);
cb && cb({ ok: true, maxPlayers: space.maxPlayers, botSlotCount: space.botSlotCount, totalSlots: total, lobbyBotThemes });
});
socket.on('host-console-kick-peer', ({ targetId }, cb) => {
@@ -4788,14 +5118,19 @@ io.on('connection', (socket) => {
ready: !!p.ready,
characterId: p.characterId ?? null
}));
io.to(sid).emit('game-start', {
stayInRoomLobby: true,
mapId: POST_CASE_LOBBY_SPACE_ID,
lobbyLevel,
caseId,
peersSnap,
cardMinigames: space.suspectCardMinigames || [],
suspectProgress: space.suspectProgress || [0, 0, 0],
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('game-start', {
stayInRoomLobby: true,
mapId: POST_CASE_LOBBY_SPACE_ID,
lobbyLevel,
caseId,
peersSnap,
cardMinigames: space.suspectCardMinigames || [],
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
return reply({ ok: true });
}
+2 -2
View File
@@ -9,7 +9,7 @@
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0189">
<link rel="stylesheet" href="daily-popup.css?v=8">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=39">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=42">
<link rel="stylesheet" href="../Game/css/profile-popup.css?v=5">
</head>
<body class="lobby-page">
@@ -228,7 +228,7 @@
<script src="../app-base.js?v=2"></script>
<script src="../Game/js/display-name.js?v=2"></script>
<script src="daily-popup.js?v=25" data-daily-trigger="#btn-daily" data-daily-asset-base="IMAGE/Daily" data-daily-test-reset-seconds="0"></script>
<script src="../Game/js/customize-popup.js?v=26" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
<script src="../Game/js/customize-popup.js?v=31" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
<script src="../Game/js/profile-popup.js?v=6" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
<script src="lobby.js?v=0.0190"></script>
</body>