เก็บ flow

This commit is contained in:
2026-05-28 08:56:20 +00:00
parent 3c632dc754
commit 69b70dc422
6 changed files with 441 additions and 102 deletions
+73 -31
View File
@@ -263,6 +263,19 @@
width: 100%;
height: 100%;
object-fit: fill;
z-index: 1;
}
/* card-back.png — ชั้นกลางทับ card-bg ทันเฟรม (โผล่เฉพาะตอน is-ready) */
#testimony-overlay .es3-ps-card .card-back {
position: absolute;
inset: 6px;
width: calc(100% - 12px);
height: calc(100% - 12px);
object-fit: contain;
z-index: 2;
pointer-events: none;
filter: drop-shadow(0 0 6px rgba(83, 224, 255, 0.35));
}
#testimony-overlay .es3-ps-card .status-icon {
@@ -272,7 +285,7 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
z-index: 3;
}
#testimony-overlay .es3-ps-card .loading-icon {
@@ -528,29 +541,9 @@
cursor: not-allowed;
}
/* โซนคลิกทับปุ่ม "ส่งหลักฐาน" ใน tab-01.png — ไม่วาด btn-send ซ้ำ (เดิม 920×944 ทับผิดจุด) */
#testimony-overlay .es3-btn-submit {
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 {
/* ปุ่มหลัก bottom-right — สลับรูประหว่าง btn-send.png ↔ btn-open-card.png */
#testimony-overlay .es3-btn-action {
position: absolute;
left: calc(1329 / var(--es3-w) * 100%);
top: calc(944 / var(--es3-h) * 100%);
width: calc(400 / var(--es3-w) * 100%);
@@ -558,15 +551,42 @@
max-width: 400px;
max-height: 120px;
z-index: 5;
cursor: pointer;
user-select: none;
display: block;
transition: filter 0.15s ease, transform 0.1s ease;
}
#testimony-overlay .es3-btn-open img {
#testimony-overlay .es3-btn-action img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
pointer-events: none;
}
#testimony-overlay .es3-btn-open.is-hidden {
#testimony-overlay .es3-btn-action:hover {
filter: brightness(1.1) drop-shadow(0 0 8px rgba(109, 240, 255, 0.6));
}
#testimony-overlay .es3-btn-action:active {
transform: scale(0.97);
filter: brightness(0.9);
}
#testimony-overlay .es3-btn-action.is-disabled {
cursor: not-allowed;
filter: grayscale(0.7) opacity(0.45);
pointer-events: none;
}
#testimony-overlay .es3-btn-action.is-hidden {
display: none !important;
}
/* legacy hidden — ไม่ใช้แล้วแต่กันโค้ดเก่าอ้างอิง */
#testimony-overlay .es3-btn-submit-legacy,
#testimony-overlay .es3-btn-open-legacy {
display: none !important;
}
@@ -943,19 +963,41 @@
transition: opacity 0.2s;
}
#testimony-overlay .es-reveal-wrap .es-ready .ready-active {
#testimony-overlay .es-reveal-wrap .es-ready .ready-active,
#testimony-overlay .es-reveal-wrap .es-ready .ready-start-vote {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.2s;
}
#testimony-overlay .es-reveal-wrap .es-ready.is-active .ready-default,
#testimony-overlay .es-reveal-wrap .es-ready:disabled .ready-default {
/* state: ready (กดแล้ว / disabled) → แสดง btn-ready-active.png */
#testimony-overlay .es-reveal-wrap .es-ready.is-active:not(.is-start-vote) .ready-default,
#testimony-overlay .es-reveal-wrap .es-ready:disabled:not(.is-start-vote) .ready-default {
opacity: 0;
}
#testimony-overlay .es-reveal-wrap .es-ready.is-active .ready-active,
#testimony-overlay .es-reveal-wrap .es-ready:disabled .ready-active {
#testimony-overlay .es-reveal-wrap .es-ready.is-active:not(.is-start-vote) .ready-active,
#testimony-overlay .es-reveal-wrap .es-ready:disabled:not(.is-start-vote) .ready-active {
opacity: 1;
}
/* state: start-vote (host เมื่อทุกคน READY แล้ว) → แสดง btn-start-vote.png */
#testimony-overlay .es-reveal-wrap .es-ready.is-start-vote .ready-default,
#testimony-overlay .es-reveal-wrap .es-ready.is-start-vote .ready-active {
opacity: 0;
}
#testimony-overlay .es-reveal-wrap .es-ready.is-start-vote .ready-start-vote {
opacity: 1;
}
#testimony-overlay .es-reveal-wrap .es-ready.is-start-vote {
filter: drop-shadow(0 0 12px rgba(83, 224, 255, 0.5));
}
#testimony-overlay .es-reveal-wrap .es-ready.is-start-vote:hover {
filter: drop-shadow(0 0 18px rgba(83, 224, 255, 0.85)) brightness(1.1);
transform: scale(1.02);
transition: filter 0.15s ease, transform 0.1s ease;
}
+12 -5
View File
@@ -813,8 +813,8 @@
const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
let lineInArt = Number(raw.finishLineInFinishArtFrac);
if (!Number.isFinite(lineInArt)) {
/* mno9kb07: จบที่ "แถบ FINISH" สีเหลือง (กลาง-ซ้ายของ finish art) — ไม่ใช่ตู้เซฟ */
lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.22 : 0.12;
/* mno9kb07: จบที่ "แถบ FINISH" สีเหลือง — หยุดทันทีหลังผ่านแถบ (ขอบขวาของแถบ FINISH) */
lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.34 : 0.12;
}
lineInArt = Math.max(0, Math.min(0.92, lineInArt));
const finishLineWorldX = g.finishStart + g.w4Safe * lineInArt;
@@ -831,8 +831,8 @@
legacy = Math.max(0.08, Math.min(0.96, legacy));
return Math.max(0.12, Math.min(0.62, 0.14 + legacy * 0.42));
}
/* mno9kb07: หยุดเลื่อนที่ ~0.42 (เส้น FINISH เลื่อนมาอยู่กลางจอ) */
return gauntletCrownRunwayBgMapEligiblePlay() ? 0.42 : 0.34;
/* mno9kb07: หยุดเลื่อนเร็วขึ้น (เส้น FINISH หยุดที่ ~0.55 จากซ้าย — ขอบขวาของแถบ FINISH ใกล้กลางจอ) */
return gauntletCrownRunwayBgMapEligiblePlay() ? 0.55 : 0.34;
}
function gauntletCrownRunwayObstacleFadeStartViewportFracPlay() {
@@ -16462,6 +16462,13 @@
others.forEach((o, id) => {
if (!isPreviewBotId(id)) return;
o.botIsWalking = false;
/* บอทที่ตอบผิดไปแล้วในแมป mng8a80o → เป็นผีเดินมั่ว ไม่ต้องไปยืนรอที่โซนตอบ */
const ghostWanderBot = isQuizQuestionMissionUiMapPlay() && isQuizWrongGhostActiveForEnt(id);
if (ghostWanderBot) {
/* ข้าม path-following → ตกไป wander logic ด้านล่าง */
o.botPath = [];
o.botAnswerWander = true;
}
if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) {
stepPreviewBotAlongPath(o, w, h);
return;
@@ -16469,7 +16476,7 @@
if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) {
return;
}
/* ก่อนตอบ / พัก / บอทสับสน: เดินทุกเฟรมตามทิศ (เหมือนผู้เล่นกดค้าง) — เดิมรอเป็นจังหวะแล้วก้าวทีละครั้งเลยดูกระตุก */
/* ก่อนตอบ / พัก / บอทสับสน / ผี (ตอบผิดไปแล้ว): เดินทุกเฟรมตามทิศ */
if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
const d = pickRandomPreviewBotWanderDir();
o.botWanderDx = d[0];
+247 -50
View File
@@ -1840,6 +1840,12 @@
preplayOverlay.addEventListener('click', function (ev) {
var startBtn = ev.target.closest('.lobby-case-start');
if (!startBtn || !preplayOverlay.contains(startBtn)) return;
/* กันกดซ้ำ — ถ้าปุ่มไหน loading อยู่ (รวมปุ่มอื่นในกลุ่ม) → ไม่รับ */
if (startBtn.classList.contains('is-loading') || startBtn.disabled) return;
var allCaseBtns = preplayOverlay.querySelectorAll('.lobby-case-start');
var anyLoading = false;
allCaseBtns.forEach(function (b) { if (b.classList.contains('is-loading')) anyLoading = true; });
if (anyLoading) return;
var cid = (startBtn.getAttribute('data-case') || '').trim();
var level = (preplaySelectedLevel && String(preplaySelectedLevel).trim())
|| (preplayOverlay.dataset.preplayLevel || '').trim();
@@ -1848,9 +1854,19 @@
try { alert('กรุณาเลือกระดับความท้าทายก่อน (กลับไปขั้นตอนก่อนหน้าแล้วเลือกระดับ)'); } catch (e0) { /* ignore */ }
return;
}
/* loading state: lock ทุกปุ่มเริ่มคดี + ใส่ class is-loading */
allCaseBtns.forEach(function (b) { b.disabled = true; });
startBtn.classList.add('is-loading');
function clearLoadingState() {
allCaseBtns.forEach(function (b) {
b.disabled = false;
b.classList.remove('is-loading');
});
}
var acked = false;
var t = setTimeout(function () {
if (!acked) {
clearLoadingState();
try { alert('ไม่ได้รับตอบจากเซิร์ฟเวอร์ — ลองใหม่หรือรีเฟรชหน้า'); } catch (eT) { /* ignore */ }
}
}, 12000);
@@ -1863,8 +1879,10 @@
acked = true;
clearTimeout(t);
if (res && res.ok === false && res.error) {
clearLoadingState();
try { alert(res.error); } catch (e2) { /* ignore */ }
}
/* ถ้าสำเร็จ — คงสถานะ loading ไว้จนกว่าเซิร์ฟจะส่ง event นำเข้าฉาก (กันกดซ้ำระหว่างรอเปลี่ยน scene) */
});
});
@@ -3949,6 +3967,8 @@
setSuspectProgress([0, 1, 2].map(function (i) { return Math.min(3, (myPlayerEvidence[i] || []).length); }));
}
applySuspectProgressVisual();
/* อัปเดต UI host เร็วทันที — ปุ่ม "ชี้ตัวคนร้าย" ต้องโผล่ทันทีเมื่อสืบครบ */
if (suspectPickOverlayOpen) updateSuspectHostUi();
if (typeof lobbyEvidenceSuspectIdx === 'number') renderLobbyEvidenceCards(lobbyEvidenceSuspectIdx);
}
@@ -4615,7 +4635,7 @@
}
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2 };
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2, hostId: '', submittedIds: [], readyIds: [], iReadied: false };
/** สล็อตหลักฐานที่ผู้เล่นเก็บได้สำหรับ suspect ตามรอบ (เช่น [0,2]) */
function tmOwnedSlotsForRound(round) {
@@ -4736,10 +4756,13 @@
'<div class="layer es3-counter es3-mode-select" id="es3-count">เลือกหลักฐาน 0/2</div>' +
'<div class="layer es3-cards es3-mode-select" id="es3-cards"></div>' +
'<div class="layer es3-status-text es3-mode-select" id="es3-status">Status : กำลังรอ..ผู้เล่นเลือกหลักฐาน</div>' +
'<button type="button" class="layer es3-btn-submit btn es3-mode-select" id="es3BtnSubmit" disabled aria-label="ส่งหลักฐาน"></button>' +
'<div class="layer es3-btn-open btn es3-mode-select" id="es3BtnOpen" role="button" tabindex="0" aria-label="เปิดหลักฐานทั้งหมด">' +
'<img src="' + SC + 'btn-open-card.png" alt="">' +
/* ปุ่มหลักที่ bottom-right — สลับรูปตามสถานะ: btn-send (ทุกคน) → btn-open-card (host เมื่อทุกคนส่งครบ) */
'<div class="layer es3-btn-action es3-mode-select" id="es3BtnAction" role="button" tabindex="0" aria-label="ส่งหลักฐาน" data-state="send">' +
'<img id="es3BtnActionImg" src="' + SC + 'btn-send.png" alt="">' +
'</div>' +
/* legacy hidden — เก็บไว้กันโค้ดเก่าอ้างอิง id (ไม่แสดงผล) */
'<button type="button" class="es3-btn-submit-legacy" id="es3BtnSubmit" hidden disabled aria-hidden="true"></button>' +
'<div class="es3-btn-open-legacy" id="es3BtnOpen" hidden aria-hidden="true"></div>' +
'<div class="es-reveal-wrap" id="es-reveal-wrap" hidden>' +
'<div class="layer es-bg" aria-hidden="true"><img src="' + SC + 'bg-showcard.png" alt=""></div>' +
'<div class="layer es-popup"><img src="' + SC + 'popup-0.png" alt=""></div>' +
@@ -4753,9 +4776,10 @@
'<p class="es-subtitle" id="es-popup-sub">[วิเคราะห์พยานหลักฐานกับทีม ก่อนไปปากคำถัดไป]</p>' +
'</div>' +
'<div class="layer es-players" id="es-players"></div>' +
'<button type="button" class="layer es-ready btn" id="esBtnReady" aria-label="READY">' +
'<button type="button" class="layer es-ready btn" id="esBtnReady" aria-label="READY" data-state="ready">' +
'<img class="ready-default" src="' + SC + 'btn-ready.png" alt="">' +
'<img class="ready-active" src="' + SC + 'btn-ready-active.png" alt="">' +
'<img class="ready-start-vote" src="' + SC + 'btn-start-vote.png" alt="">' +
'</button>' +
'</div>' +
'<button type="button" class="es3-mic btn" id="es3-mic" aria-label="ไมค์">🎤</button>' +
@@ -4773,8 +4797,8 @@
else { if (tmState.picks.length >= maxPicks) return; tmState.picks.push(idx); card.classList.add('is-selected'); }
es3UpdateCount();
});
var submitEl = ov.querySelector('#es3BtnSubmit');
if (submitEl) submitEl.addEventListener('click', function () {
/* ปุ่มเดียวจัดการทั้ง submit (ทุกคน) และ reveal (host เมื่อทุกคนส่งครบ) */
function doSubmitMyPicks() {
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
if (tmState.picks.length !== req || tmState.submitted) return;
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) {
@@ -4782,43 +4806,57 @@
tmState.submitted = true;
if (!tmState.submittedPicksById) tmState.submittedPicksById = {};
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req);
if (tmState.submittedIds.indexOf(socket.id) < 0) tmState.submittedIds.push(socket.id);
es3SetStatus('ส่งหลักฐานแล้ว — รอผู้เล่นอื่น...');
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = true;
} else appendLobbySystemChat('— ส่งหลักฐานไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
es3RenderMembers(tmState.submittedIds, [], false);
es3UpdateActionButton();
} else {
appendLobbySystemChat('— ส่งหลักฐานไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
}
});
});
var openEl = ov.querySelector('#es3BtnOpen');
function autoSubmitThenReveal() {
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
var doReveal = function () {
socket.emit('testimony-reveal', {}, function (res) { if (!(res && res.ok)) appendLobbySystemChat('— เปิดเผยไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : '')); });
};
/* ถ้ายังไม่ submit แต่เลือกครบตาม required แล้ว — submit ให้ก่อน reveal */
if (!tmState.submitted && tmState.ownedSlots.length > 0 && tmState.picks.length === req) {
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) {
if (res && res.ok) {
tmState.submitted = true;
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req);
}
doReveal();
});
} else {
doReveal();
}
}
if (openEl) {
openEl.addEventListener('click', autoSubmitThenReveal);
openEl.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); autoSubmitThenReveal(); }
function doRevealAll() {
socket.emit('testimony-reveal', {}, function (res) {
if (!(res && res.ok)) appendLobbySystemChat('— เปิดเผยไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
});
}
function onActionBtnTrigger() {
var btn = document.getElementById('es3BtnAction');
if (!btn || btn.classList.contains('is-disabled')) return;
var state = btn.getAttribute('data-state');
if (state === 'open') doRevealAll();
else doSubmitMyPicks();
}
var actionEl = ov.querySelector('#es3BtnAction');
if (actionEl) {
actionEl.addEventListener('click', onActionBtnTrigger);
actionEl.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onActionBtnTrigger(); }
});
}
var readyEl = ov.querySelector('#esBtnReady');
if (readyEl) readyEl.addEventListener('click', function () {
if (this.disabled) return;
var state = this.getAttribute('data-state') || 'ready';
if (state === 'start-vote') {
/* host กดเริ่มพิจารณาคดี — ไปปากคำถัดไป/หน้าโหวต */
this.disabled = true;
socket.emit('testimony-start-vote', {}, function (res) {
if (!(res && res.ok)) {
var rd = document.getElementById('esBtnReady');
if (rd) rd.disabled = false;
appendLobbySystemChat('— เริ่มพิจารณาคดีไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
}
});
return;
}
/* state === 'ready' — กดยืนยัน READY ของตัวเอง */
this.classList.add('is-active');
this.disabled = true;
tmState.iReadied = true;
socket.emit('testimony-ready-next', {}, function (res) {
if (!(res && res.ok)) {
tmState.iReadied = false;
var rd = document.getElementById('esBtnReady');
if (rd) { rd.disabled = false; rd.classList.remove('is-active'); }
appendLobbySystemChat('— READY ไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
@@ -4836,7 +4874,48 @@
function es3UpdateCount() {
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
var el = document.getElementById('es3-count'); if (el) el.textContent = 'เลือกหลักฐาน ' + tmState.picks.length + '/' + req;
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = (tmState.picks.length !== req || tmState.submitted);
es3UpdateActionButton();
}
/* + bottom-right
- state="send" btn-send.png (กคนเห; กดสงหลกฐานของตวเอง)
- state="open" btn-open-card.png (เฉพาะ host เห/กดได; กดเปดเผยทงหมด) */
function es3UpdateActionButton() {
var btn = document.getElementById('es3BtnAction');
var img = document.getElementById('es3BtnActionImg');
if (!btn || !img) return;
var SC = '/Main-Lobby/IMAGE/Showcard/';
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
var totalMembers = (tmState.members || []).length;
var submittedCount = (tmState.submittedIds || []).length;
var allDone = totalMembers > 0 && submittedCount >= totalMembers;
var isHost = !!(tmState.hostId && tmState.hostId === socket.id);
if (allDone) {
btn.setAttribute('data-state', 'open');
btn.setAttribute('aria-label', 'เปิดหลักฐานทั้งหมด');
img.src = SC + 'btn-open-card.png';
/* host เท่านั้นที่เห็น/กดได้ */
if (isHost) {
btn.classList.remove('is-hidden', 'is-disabled');
btn.style.display = '';
btn.removeAttribute('aria-hidden');
} else {
btn.classList.add('is-hidden');
btn.style.display = 'none';
btn.setAttribute('aria-hidden', 'true');
}
} else {
btn.setAttribute('data-state', 'send');
btn.setAttribute('aria-label', 'ส่งหลักฐาน');
img.src = SC + 'btn-send.png';
btn.classList.remove('is-hidden');
btn.style.display = '';
btn.removeAttribute('aria-hidden');
/* ใช้ได้เฉพาะเมื่อเลือกครบและยังไม่เคยส่ง */
var canSend = (tmState.picks.length === req && !tmState.submitted && tmState.ownedSlots.length > 0);
btn.classList.toggle('is-disabled', !canSend);
}
}
function esRevealCardEl(c, linkName, cardIdx) {
@@ -4946,6 +5025,7 @@
'<div class="es3-ps-name">' + crown + nick + '</div>' +
'<div class="es3-ps-card is-ready">' +
'<img class="card-bg" src="' + SC + 'card-bg.png" alt="">' +
'<img class="card-back" src="' + SC + 'card-back.png" alt="">' +
'<img class="status-icon" src="' + SC + 'ready-icon.png" alt="พร้อม">' +
'</div>';
} else {
@@ -4971,6 +5051,10 @@
tmState.picks = [];
tmState.submitted = false;
tmState.submittedPicksById = {};
tmState.submittedIds = (data && Array.isArray(data.submitted)) ? data.submitted.slice() : [];
tmState.hostId = (data && data.hostId) ? data.hostId : '';
tmState.readyIds = [];
tmState.iReadied = false;
tmState.mode = 'select';
tmState.ownedSlots = tmOwnedSlotsForRound(tmState.round);
/* ต้องเลือกหลักฐาน = min(2, จำนวนที่เก็บได้) — ถ้าได้ 1 ใบ ส่ง 1 ใบพอ */
@@ -4989,12 +5073,6 @@
if (readyBtn) { readyBtn.disabled = false; readyBtn.classList.remove('is-active'); }
var tabImg = document.querySelector('#es3-bottom-box img');
if (tabImg) tabImg.src = SC + 'tab-0' + (tmState.round + 1) + '.png';
var btnOpen = document.getElementById('es3BtnOpen');
if (btnOpen) {
var isHost = (data && data.hostId) === socket.id;
btnOpen.classList.toggle('is-hidden', !isHost);
btnOpen.style.display = isHost ? '' : 'none';
}
es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน');
var cardsRoot = document.getElementById('es3-cards'); cardsRoot.textContent = '';
/* แสดงเฉพาะการ์ดที่ผู้เล่นเก็บได้จริง (ตามสล็อตจาก myPlayerEvidence) */
@@ -5008,7 +5086,9 @@
if (res && res.ok) {
tmState.submitted = true;
tmState.submittedPicksById[socket.id] = [];
if (tmState.submittedIds.indexOf(socket.id) < 0) tmState.submittedIds.push(socket.id);
es3SetStatus('ไม่มีหลักฐาน — ข้ามอัตโนมัติ');
es3UpdateActionButton();
}
});
} else {
@@ -5018,7 +5098,8 @@
});
}
es3UpdateCount();
es3RenderMembers([], [], false);
es3RenderMembers(tmState.submittedIds || [], [], false);
es3UpdateActionButton();
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
requestAnimationFrame(syncTestimonyScale);
@@ -5026,8 +5107,19 @@
function testimonyStatusUpdate(data) {
if (!data) return;
es3RenderMembers(data.submitted || [], [], false);
if (data.submitted && data.submitted.length) es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน');
tmState.submittedIds = Array.isArray(data.submitted) ? data.submitted.slice() : [];
if (data.hostId) tmState.hostId = data.hostId;
es3RenderMembers(tmState.submittedIds, [], false);
es3UpdateActionButton();
/* อัปเดต status text */
var total = (tmState.members || []).length;
if (total > 0 && tmState.submittedIds.length >= total) {
es3SetStatus('ทุกคนเลือกหลักฐานแล้ว — รอ host เปิดเผยผล');
} else if (tmState.submittedIds.length > 0) {
es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน (' + tmState.submittedIds.length + '/' + total + ')');
} else {
es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน');
}
}
function testimonyReveal(data) {
@@ -5051,15 +5143,65 @@
var susName = document.getElementById('es-popup-sus-name');
if (susName) susName.textContent = sd.linkName || '';
var rd = document.getElementById('esBtnReady');
if (rd) { rd.disabled = false; rd.classList.remove('is-active'); }
if (rd) { rd.disabled = false; rd.classList.remove('is-active', 'is-start-vote'); rd.setAttribute('data-state', 'ready'); }
tmState.readyIds = (data && Array.isArray(data.ready)) ? data.ready.slice() : [];
tmState.iReadied = false;
if (data && data.hostId) tmState.hostId = data.hostId;
var picks = Object.assign({}, tmState.submittedPicksById || {}, (data && data.picks) || {});
esRenderRevealPlayers(tmState.members, picks, sd);
es3RenderMembers([], (data && data.ready) || [], true);
es3RenderMembers([], tmState.readyIds, true);
esUpdateRevealActionButton();
requestAnimationFrame(syncTestimonyScale);
}
function testimonyReadyUpdate(data) {
es3RenderMembers([], (data && data.ready) || [], true);
var readyArr = (data && data.ready) || [];
tmState.readyIds = readyArr.slice();
if (data && data.hostId) tmState.hostId = data.hostId;
es3RenderMembers([], readyArr, true);
esUpdateRevealActionButton();
}
/* reveal mode:
- อนผเลนกด READY: btn-ready.png (กคน)
- หลงกด READY แตงรอคนอ: btn-ready-active.png (disabled)
- กคน READY แล: host เห btn-start-vote.png (กดได); คนอ disabled */
function esUpdateRevealActionButton() {
var btn = document.getElementById('esBtnReady');
if (!btn) return;
var isHost = !!(tmState.hostId && tmState.hostId === socket.id);
var total = (tmState.members || []).length;
var readyCount = (tmState.readyIds || []).length;
var allReady = total > 0 && readyCount >= total;
var meReadied = !!tmState.iReadied || ((tmState.readyIds || []).indexOf(socket.id) >= 0);
btn.classList.remove('is-start-vote', 'is-active');
btn.disabled = false;
if (allReady && isHost) {
/* host เห็นปุ่มเริ่มพิจารณาคดี */
btn.setAttribute('data-state', 'start-vote');
btn.setAttribute('aria-label', 'เริ่มพิจารณาคดี');
btn.classList.add('is-start-vote');
btn.disabled = false;
} else if (allReady && !isHost) {
/* non-host: รอ host */
btn.setAttribute('data-state', 'ready');
btn.setAttribute('aria-label', 'รอ host เริ่มพิจารณาคดี');
btn.classList.add('is-active');
btn.disabled = true;
} else if (meReadied) {
/* ยังรอคนอื่น */
btn.setAttribute('data-state', 'ready');
btn.setAttribute('aria-label', 'รอผู้เล่นอื่น');
btn.classList.add('is-active');
btn.disabled = true;
} else {
/* ยังไม่ได้กด */
btn.setAttribute('data-state', 'ready');
btn.setAttribute('aria-label', 'READY');
btn.disabled = false;
}
}
function hideTestimonyOverlay() {
@@ -5395,13 +5537,34 @@
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
const idx = suspectSelectedIndex;
if (idx < 0 || idx > 2) return;
const startBtn = document.getElementById('suspect-btn-start');
/* กันกดซ้ำ */
if (startBtn && (startBtn.classList.contains('is-loading') || startBtn.disabled)) return;
if (!canInvestigateSuspect(idx)) {
appendLobbySystemChat('— สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่น');
updateSuspectHostUi();
return;
}
if (startBtn) {
startBtn.classList.add('is-loading');
startBtn.disabled = true;
}
const clearLoading = () => {
const b = document.getElementById('suspect-btn-start');
if (b) { b.classList.remove('is-loading'); b.disabled = false; }
};
let acked = false;
const timeoutT = setTimeout(() => {
if (!acked) {
clearLoading();
appendLobbySystemChat('— เซิร์ฟเวอร์ไม่ตอบ ลองอีกครั้ง');
}
}, 12000);
socket.emit('suspect-pick-start', {}, function (res) {
if (res && res.ok) return;
acked = true;
clearTimeout(timeoutT);
if (res && res.ok) return; /* คง loading ต่อจนเข้า play scene */
clearLoading();
const err = (res && res.error) ? String(res.error) : (sourceLabel || 'เริ่มสืบสวนไม่สำเร็จ');
appendLobbySystemChat('— ' + err);
try { console.warn('suspect-pick-start', res); } catch (e2) { /* ignore */ }
@@ -5435,16 +5598,50 @@
});
});
document.getElementById('suspect-pick-close')?.addEventListener('click', () => {
if (!suspectPickOverlayOpen) return;
closeSuspectOverlay();
});
/* ลบปุ่ม X ปิดออกตาม design — ใช้ปุ่ม "เริ่มสืบสวน/ชี้ตัวคนร้าย" เท่านั้น */
const __sustClose = document.getElementById('suspect-pick-close');
if (__sustClose && __sustClose.parentNode) __sustClose.parentNode.removeChild(__sustClose);
document.getElementById('btn-suspect-reopen')?.addEventListener('click', () => {
if (!serverSuspectPhaseActive || suspectPickOverlayOpen) return;
openSuspectOverlay(suspectSelectedIndex);
});
/**
* Hotkey ดสำหรบเทสต (เฉพาะตอน suspect overlay เป): Ctrl+1 / Ctrl+2 / Ctrl+3
* "เล่นเสร็จ 1 รอบ" ของ suspect คนน (เกบหลกฐาน 1 ใบใหนท)
* กดครบ 3 "ชี้ตัวคนร้าย" โผลตโนม
* โหมดโหวต/เปดเผยผล: hotkey องกนการกระทบเกม
*/
document.addEventListener('keydown', (ev) => {
if (!ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey) return;
if (!suspectPickOverlayOpen) return;
/* ขณะอยู่ใน input/textarea/ที่แก้ไขได้ — ข้าม กันชน UX */
const ae = document.activeElement;
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return;
let idx = -1;
if (ev.key === '1' || ev.code === 'Digit1' || ev.code === 'Numpad1') idx = 0;
else if (ev.key === '2' || ev.code === 'Digit2' || ev.code === 'Numpad2') idx = 1;
else if (ev.key === '3' || ev.code === 'Digit3' || ev.code === 'Numpad3') idx = 2;
if (idx < 0) return;
/* โหมด trial — ไม่ให้ hotkey ใช้ (กันชนกับการโหวต) */
if (trialMode === 'voting' || trialMode === 'revealed') return;
ev.preventDefault();
/* แสดงสถานะกำลังโกง (toast) */
appendLobbySystemChat('— [DEBUG] เก็บหลักฐานให้ผู้ต้องสงสัย ' + (idx + 1) + '...');
socket.emit('detective-debug-award-evidence', { suspectIndex: idx }, function (res) {
if (res && res.ok) {
appendLobbySystemChat('— [DEBUG] เก็บหลักฐาน suspect ' + (idx + 1) + ' สำเร็จ');
/* อัปเดต state ฝั่ง client ทันที (server จะ broadcast lobby-evidence-sync มาด้วย แต่ apply ทันทีเพื่อ UX ลื่น) */
if (Array.isArray(res.myPlayerEvidence)) applyMyPlayerEvidence(res.myPlayerEvidence);
if (Array.isArray(res.suspectProgress)) setSuspectProgress(res.suspectProgress);
if (suspectPickOverlayOpen) updateSuspectHostUi();
} else {
appendLobbySystemChat('— [DEBUG] ล้มเหลว: ' + ((res && res.error) || 'unknown'));
}
});
});
function applyRoomLobbyBTransition(data) {
const mid = (data && data.mapId) ? String(data.mapId).trim() : '';
if (!mid) return;
+1 -1
View File
@@ -3214,7 +3214,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0472"></script>
<script src="js/play.js?v=0.0474"></script>
<div class="version-tag">v —</div>
</body>
</html>
+37 -5
View File
@@ -11,7 +11,7 @@
<link rel="stylesheet" href="css/profile-popup.css?v=6">
<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=11">
<link rel="stylesheet" href="css/testimony-overlay.css?v=14">
<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%; }
@@ -253,6 +253,41 @@
transform: none;
filter: grayscale(0.6);
}
/* loading state — กันกดซ้ำ + แสดง spinner */
.suspect-btn-start.is-loading,
.lobby-case-start.is-loading {
position: relative;
cursor: wait !important;
pointer-events: none;
}
.suspect-btn-start.is-loading::after,
.lobby-case-start.is-loading::after {
content: '';
position: absolute;
inset: 0;
margin: auto;
width: 38px;
height: 38px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.18);
border-top-color: #6ff0ff;
animation: zd-spin 0.8s linear infinite;
pointer-events: none;
z-index: 5;
}
.suspect-btn-start.is-loading > *,
.lobby-case-start.is-loading > * {
opacity: 0.32;
filter: blur(0.8px);
}
.lobby-case-start:disabled:not(.is-loading) {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
@keyframes zd-spin {
to { transform: rotate(360deg); }
}
.suspect-card-ui {
position: absolute;
left: 61%;
@@ -1086,9 +1121,6 @@
</div>
<div id="suspect-pick-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="suspect-pick-sr-title" aria-hidden="true">
<h2 id="suspect-pick-sr-title" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);">เลือกผู้ต้องสงสัย</h2>
<button type="button" id="suspect-pick-close" class="suspect-pick-close" title="ปิดหน้าต่าง (กดปุ่มด้านล่างเพื่อเปิดอีกครั้ง)" aria-label="ปิดหน้าเลือกผู้ต้องสงสัย">
<img src="/Main-Lobby/IMAGE/Choose%20a%20suspect/btn-close_1.png" alt="" width="56" height="56" decoding="async">
</button>
<div id="suspect-pick-scale-wrap" class="suspect-pick-scale-wrap">
<div class="suspect-pick-inner">
<img class="suspect-pick-title-img" src="/Main-Lobby/IMAGE/Choose%20a%20suspect/txt-suspect.png" alt="เลือกผู้ต้องสงสัย" width="1149" height="198" decoding="async">
@@ -1576,7 +1608,7 @@
<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=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/room-lobby.js?v=0.0248"></script>
<script src="js/room-lobby.js?v=0.0253"></script>
<div class="version-tag">v —</div>
</body>
</html>
+71 -10
View File
@@ -2192,6 +2192,7 @@ function emitTestimonyOpen(sid, space) {
space.testimonyRevealed = false;
/* บอทส่งหลักฐานทันทีเมื่อเปิด round */
autoSubmitBotTestimonyPicks(space);
const submittedIds = Object.keys(space.testimonySubmitted || {});
const base = {
round: space.testimonyRound,
suspectIndex: space.testimonyRound,
@@ -2199,6 +2200,7 @@ function emitTestimonyOpen(sid, space) {
totalPlayers: testimonyTotalParticipants(space),
hostId: space.hostId,
cardMinigames: space.suspectCardMinigames || [],
submitted: submittedIds,
};
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
@@ -2209,7 +2211,8 @@ function emitTestimonyOpen(sid, space) {
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
/* ถ้ามีแต่บอท (ไม่มีผู้เล่นจริง) ก็เปิด reveal เลย — แต่ปกติจะมีคนกด reveal เอง */
/* sync สถานะปุ่ม (เผื่อบอทส่งครบแล้ว — host ต้องเห็นปุ่ม "เปิดหลักฐานทั้งหมด" ทันที) */
emitTestimonyStatus(sid, space);
}
function emitTestimonyStatus(sid, space) {
@@ -4674,9 +4677,25 @@ io.on('connection', (socket) => {
const spawnJoinOrder = space.peers.size;
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 3));
/* ใช้สีที่ client เลือกไว้ใน Main-Lobby ถ้าผ่านการตรวจสอบ + ยังไม่ถูกใช้ในห้องนี้ */
/* ใช้สีที่ client เลือกไว้ใน Main-Lobby — คนจริงสำคัญกว่าบอท */
let chosenThemeIdx = parseInt(desiredLobbyColorThemeIndex, 10);
if (!(chosenThemeIdx >= 1 && chosenThemeIdx <= LOBBY_THEME_COUNT) || getTakenLobbyThemeIndices(space).has(chosenThemeIdx)) {
if (chosenThemeIdx >= 1 && chosenThemeIdx <= LOBBY_THEME_COUNT) {
/* ถ้ามี "ผู้เล่นจริงคนอื่น" ถือสีนี้แล้ว → fallback สุ่มใหม่ */
let takenByHuman = false;
space.peers.forEach((p) => {
const ti = parseInt(p.lobbyColorThemeIndex, 10);
if (ti === chosenThemeIdx) takenByHuman = true;
});
if (takenByHuman) {
chosenThemeIdx = pickFreeLobbyThemeIndex(space);
} else if (space.lobbyBotThemeBySlot && typeof space.lobbyBotThemeBySlot === 'object') {
/* ถ้าบอทใช้สีนี้ → ดันบอทไปสีอื่น (เคลียร์ slot.themeIndex; syncLobbyBotThemeSlots ใน joinCb จะหาสีใหม่ที่ไม่ชนกับ peer ใหม่) */
Object.keys(space.lobbyBotThemeBySlot).forEach((k) => {
const slot = space.lobbyBotThemeBySlot[k];
if (slot && parseInt(slot.themeIndex, 10) === chosenThemeIdx) slot.themeIndex = 0;
});
}
} else {
chosenThemeIdx = pickFreeLobbyThemeIndex(space);
}
let chosenSkinIdx = parseInt(desiredLobbySkinToneIndex, 10);
@@ -4979,6 +4998,30 @@ io.on('connection', (socket) => {
});
});
/**
* DEBUG/TEST hotkey เกบหลกฐาน 1 ใบใหองสงส (suspectIndex 02) นท
* (ใชสำหรบเทสต Ctrl+1/2/3 เทยบเทาเลนมเกมจบ 1 คร)
*/
socket.on('detective-debug-award-evidence', (data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
if (!serverMapIsPostCaseLobbyB(space)) return reply({ ok: false, error: 'ต้องอยู่โถง LobbyB' });
const idx = Math.floor(Number(data && data.suspectIndex));
if (!(idx >= 0 && idx <= 2)) return reply({ ok: false, error: 'suspectIndex ต้องเป็น 02' });
const card = awardRandomEvidenceCardToPlayer(space, socket.id, idx);
if (card == null) return reply({ ok: false, error: 'เก็บครบ 3 ใบแล้วสำหรับ suspect นี้' });
emitLobbyEvidenceSync(sid, space);
reply({
ok: true,
suspectIndex: idx,
awardedCard: card,
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
});
});
// Host เปิดขั้นพิจารณาคดี (ต้องสืบครบ 3 ผู้ต้องสงสัย)
socket.on('suspect-accuse-open', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
@@ -5018,8 +5061,7 @@ io.on('connection', (socket) => {
if (!space.testimonySubmitted) space.testimonySubmitted = {};
space.testimonySubmitted[socket.id] = [];
emitTestimonyStatus(sid, space);
const totalP0 = testimonyTotalParticipants(space);
if (Object.keys(space.testimonySubmitted).length >= totalP0 && totalP0 > 0) doTestimonyReveal(sid, space);
/* ไม่ auto-reveal — รอ host กดปุ่ม "เปิดหลักฐานทั้งหมด" เองตาม design */
return reply({ ok: true, skipped: true });
}
if (picks.length !== required) {
@@ -5028,8 +5070,8 @@ io.on('connection', (socket) => {
if (!space.testimonySubmitted) space.testimonySubmitted = {};
space.testimonySubmitted[socket.id] = picks.slice(0, required);
emitTestimonyStatus(sid, space);
const totalP = testimonyTotalParticipants(space);
if (Object.keys(space.testimonySubmitted).length >= totalP && totalP > 0) doTestimonyReveal(sid, space);
/* auto-reveal host "" design
(UX: host องเหนปมเปลยนรปจาก btn-send btn-open-card แลวคอยกดเอง) */
reply({ ok: true });
});
@@ -5045,7 +5087,7 @@ io.on('connection', (socket) => {
reply({ ok: true });
});
// ผู้เล่นกด READY ไปปากคำถัดไป (หลังเปิดเผยผล)
// ผู้เล่นกด READY ไปปากคำถัดไป (หลังเปิดเผยผล) — ไม่ auto-advance รอ host กด "เริ่มพิจารณาคดี"
socket.on('testimony-ready-next', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
@@ -5055,8 +5097,27 @@ io.on('connection', (socket) => {
if (!space.testimonyReadyNext) space.testimonyReadyNext = {};
space.testimonyReadyNext[socket.id] = true;
const totalRN = testimonyTotalParticipants(space);
io.to(sid).emit('testimony-ready-update', { ready: Object.keys(space.testimonyReadyNext), totalPlayers: totalRN });
if (Object.keys(space.testimonyReadyNext).length >= totalRN && totalRN > 0) advanceTestimony(sid, space);
io.to(sid).emit('testimony-ready-update', {
ready: Object.keys(space.testimonyReadyNext),
totalPlayers: totalRN,
hostId: space.hostId,
});
/* ไม่ auto-advance — รอ host กดปุ่ม "เริ่มพิจารณาคดี" (btn-start-vote.png) เอง */
reply({ ok: true });
});
// Host กดปุ่ม "เริ่มพิจารณาคดี" (btn-start-vote) — ไปปากคำถัดไป/เข้าโหวต
socket.on('testimony-start-vote', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host' });
if (!space.testimonyActive || !space.testimonyRevealed) return reply({ ok: false, error: 'ยังไม่ถึงเวลา' });
/* host อาจกดก่อนทุกคน READY — บังคับให้ทุกคน (รวมบอท) ready ก่อน advance เพื่อให้ flow consistent */
if (!space.testimonyReadyNext) space.testimonyReadyNext = {};
testimonyBotIds(space).forEach((bid) => { space.testimonyReadyNext[bid] = true; });
advanceTestimony(sid, space);
reply({ ok: true });
});