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 (ไม่แสดงผล) */ + '' + + '' + '' + '
' + - '' + '
' + '' + @@ -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 —
diff --git a/www/html/Game/public/room-lobby.html b/www/html/Game/public/room-lobby.html index 9e852f7..9c8edcb 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -11,7 +11,7 @@ - +