diff --git a/www/html/Game/public/css/testimony-overlay.css b/www/html/Game/public/css/testimony-overlay.css
index e46ccef..bc2dac3 100644
--- a/www/html/Game/public/css/testimony-overlay.css
+++ b/www/html/Game/public/css/testimony-overlay.css
@@ -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;
+}
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 22da66f..0753a8e 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -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];
diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js
index e77d9ec..01df7d7 100644
--- a/www/html/Game/public/js/room-lobby.js
+++ b/www/html/Game/public/js/room-lobby.js
@@ -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 @@
'
เลือกหลักฐาน 0/2
' +
'' +
'Status : กำลังรอ..ผู้เล่นเลือกหลักฐาน
' +
- '' +
- '' +
- '

' +
+ /* ปุ่มหลักที่ bottom-right — สลับรูปตามสถานะ: btn-send (ทุกคน) → btn-open-card (host เมื่อทุกคนส่งครบ) */
+ '
' +
+ '

' +
'
' +
+ /* legacy hidden — เก็บไว้กันโค้ดเก่าอ้างอิง id (ไม่แสดงผล) */
+ '
' +
+ '
' +
'
' +
'
' +
'' +
@@ -4753,9 +4776,10 @@
'' +
'
' +
'
' +
- '
' +
'🎤' +
@@ -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 @@
'' + crown + nick + '
' +
'';
} 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;
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index bd45c05..b5a565f 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -3214,7 +3214,7 @@
-
+
v —