update flow design 1.2
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
/** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */
|
||||
|
||||
@@ -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('<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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user