diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 5ca1a85..dc4455c 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -3860,6 +3860,44 @@ bindEmbedPanelFullscreen('btn-map-editor-fullscreen', 'tab-panel-map-editor'); bindEmbedPanelFullscreen('btn-character-fullscreen', 'tab-panel-characters'); + /* Test Mode toggle — เก็บใน localStorage (share ระหว่าง Admin กับ Game ภายใต้ origin เดียวกัน) */ + var TEST_MODE_KEY = 'justiceTestMode'; + function isTestModeOn() { + try { return localStorage.getItem(TEST_MODE_KEY) === '1'; } catch (e) { return false; } + } + function setTestMode(on) { + try { localStorage.setItem(TEST_MODE_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ } + } + function syncTestModeUi() { + var chk = el('test-mode-toggle'); + var lbl = el('test-mode-state-label'); + var msg = el('test-mode-msg'); + var on = isTestModeOn(); + if (chk) chk.checked = on; + if (lbl) { + lbl.textContent = on ? 'เปิดอยู่ (ON)' : 'ปิดอยู่ (OFF)'; + lbl.style.color = on ? '#4ade80' : '#94a3b8'; + } + if (msg) { + msg.textContent = on + ? '✓ Test Mode เปิด — Shortcuts ใช้งานได้ในแท็บอื่นของเบราว์เซอร์นี้' + : '○ Test Mode ปิด — Shortcuts ไม่ทำงาน'; + msg.style.color = on ? '#4ade80' : '#94a3b8'; + } + } + var testModeChk = el('test-mode-toggle'); + if (testModeChk) { + testModeChk.addEventListener('change', function () { + setTestMode(!!testModeChk.checked); + syncTestModeUi(); + }); + } + syncTestModeUi(); + /* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */ + window.addEventListener('storage', function (e) { + if (e && e.key === TEST_MODE_KEY) syncTestModeUi(); + }); + var btnGameTimingSave = el('btn-game-timing-save'); if (btnGameTimingSave) { btnGameTimingSave.addEventListener('click', function () { diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index a88c53d..d94bbed 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -80,6 +80,7 @@ + + + - + diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 0753a8e..34c1052 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -2718,21 +2718,24 @@ : !!playQuizEverWrong[sid]; } - /** วาด "ผี" แทน avatar ทั้งตัวในแมป mng8a80o เมื่อตอบผิด — คุมผีเลย */ - function drawQuizWrongGhostOverlayPlay(ax, ay, entId, nameLabel) { + /** วาด "ผี" แทน avatar ทั้งตัวในแมป mng8a80o เมื่อตอบผิด — คุมผีเลย + * args ทุกตัวเป็น "screen pixels" แล้ว (คำนวณจาก worldToScreen ที่ caller) + * - sxCenter = พิกัด X กึ่งกลาง avatar บนจอ (px) + * - syBottom = พิกัด Y เท้า avatar บนจอ (px) + * - heightPx = ความสูง avatar บนจอ (px) ใช้คุมขนาดผีให้พอดี + */ + function drawQuizWrongGhostOverlayPlay(sxCenter, syBottom, heightPx, entId, nameLabel) { const img = getQuizWrongGhostImg(); if (!img || !img.complete || !img.naturalWidth) return; - const tile = (typeof tileSize === 'number' && tileSize > 0) ? tileSize : 32; - /* ผีขนาดเท่าตัวละครเดิม: avatar ปกติ ~1 tile กว้าง, ~1.6 tile สูง → ผีกว้าง ~1.4, สูงตามสัดส่วน */ - const wPx = tile * 1.4; - const ratio = img.naturalHeight / Math.max(1, img.naturalWidth); - const hPx = wPx * ratio; + /* ผีสูงเท่า avatar ตามจริง — คงสัดส่วนภาพ */ + const hPx = Math.max(24, Number(heightPx) || 0); + const ratio = img.naturalWidth / Math.max(1, img.naturalHeight); + const wPx = hPx * ratio; const sid = String(entId == null ? '' : entId); - const tBob = Math.sin(performance.now() / 360 + (sid ? sid.charCodeAt(0) || 0 : 0)) * (tile * 0.06); - /* วาดให้ "เท้าผี" ตรงกับเท้า avatar (avatar สูง 1.6 tile จากจุดบนซ้าย ay) */ - const groundY = ay + tile * 1.6; - const drawX = ax + (tile - wPx) / 2; - const drawY = groundY - hPx + tBob; + const tBob = Math.sin(performance.now() / 360 + (sid ? sid.charCodeAt(0) || 0 : 0)) * (hPx * 0.04); + /* วาดให้ "เท้าผี" ตรงกับเท้า avatar */ + const drawX = sxCenter - wPx / 2; + const drawY = syBottom - hPx + tBob; ctx.save(); ctx.globalAlpha = 0.98; ctx.drawImage(img, drawX, drawY, wPx, hPx); @@ -2745,13 +2748,24 @@ ctx.fillStyle = 'rgba(255,255,255,0.95)'; ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 3; - const nx = ax + tile / 2; - const ny = groundY + 14; - ctx.strokeText(nameLabel, nx, ny); - ctx.fillText(nameLabel, nx, ny); + const ny = syBottom + 14; + ctx.strokeText(nameLabel, sxCenter, ny); + ctx.fillText(nameLabel, sxCenter, ny); ctx.restore(); } } + /** ช่วยแปลง tile coords → screen pos สำหรับ ghost (ใช้เฉพาะที่ caller ใน render scope) + * รับ helpers ที่เป็น scope-local (worldToScreen, zDraw) เพราะถูกเรียกในรอบ draw เท่านั้น */ + function _quizGhostScreenAnchorPlay(axTile, ayTile, w2s, zDrawIn) { + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const cxWorld = (axTile + cw * 0.5) * tileSize; + const cyBottomWorld = (ayTile + ch) * tileSize; + const [sx, sy] = w2s(cxWorld, cyBottomWorld); + /* ความสูง = สูง avatar บนจอ (สอดคล้องกับ drawAvatar: boxH = tileSize * zDraw * ch * 1.35) */ + const sizeH = tileSize * zDrawIn * Math.max(ch, 1) * 1.35; + return { sx, sy, sizeH }; + } /** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */ const QUIZ_TF_POINTS_PER_CORRECT = 10; const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png'; @@ -18503,7 +18517,8 @@ if (botOut) ctx.globalAlpha = 0.4; /* ตอบผิดในแมป mng8a80o → แทน avatar ด้วยผีเลย */ if (ghostActiveO) { - drawQuizWrongGhostOverlayPlay(axO, ayO, id, peerName); + const ga = _quizGhostScreenAnchorPlay(axO, ayO, worldToScreen, zDraw); + drawQuizWrongGhostOverlayPlay(ga.sx, ga.sy, ga.sizeH, id, peerName); } else { drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); } @@ -18518,7 +18533,8 @@ if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; if (ghostActiveMe) { - drawQuizWrongGhostOverlayPlay(axMe, ayMe, myId, me.nickname + meTag); + const gam = _quizGhostScreenAnchorPlay(axMe, ayMe, worldToScreen, zDraw); + drawQuizWrongGhostOverlayPlay(gam.sx, gam.sy, gam.sizeH, myId, me.nickname + meTag); } else { drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); } diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index 01df7d7..8661f27 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -5598,9 +5598,13 @@ }); }); - /* ลบปุ่ม X ปิดออกตาม design — ใช้ปุ่ม "เริ่มสืบสวน/ชี้ตัวคนร้าย" เท่านั้น */ - const __sustClose = document.getElementById('suspect-pick-close'); - if (__sustClose && __sustClose.parentNode) __sustClose.parentNode.removeChild(__sustClose); + /* ปุ่ม X ปิด suspect overlay — ปิดได้แล้วเปิดใหม่ผ่าน #btn-suspect-reopen + (โหมด trial: ไม่ให้ปิด — กันหลุดโฟกัสตอนโหวต) */ + document.getElementById('suspect-pick-close')?.addEventListener('click', () => { + if (!suspectPickOverlayOpen) return; + if (trialMode === 'voting' || trialMode === 'revealed') return; + closeSuspectOverlay(); + }); document.getElementById('btn-suspect-reopen')?.addEventListener('click', () => { if (!serverSuspectPhaseActive || suspectPickOverlayOpen) return; @@ -5611,11 +5615,18 @@ * Hotkey ลัดสำหรับเทสต์ (เฉพาะตอน suspect overlay เปิด): Ctrl+1 / Ctrl+2 / Ctrl+3 * → "เล่นเสร็จ 1 รอบ" ของ suspect คนนั้น (เก็บหลักฐาน 1 ใบให้ทันที) * กดครบ 3 ปุ่ม → "ชี้ตัวคนร้าย" โผล่อัตโนมัติ + * + * เปิด/ปิดได้จาก Admin Panel → tab "Test Mode" (localStorage: justiceTestMode = "1"|"0") * โหมดโหวต/เปิดเผยผล: ปิด hotkey ป้องกันการกระทบเกม */ + function __isJusticeTestModeOn() { + try { return localStorage.getItem('justiceTestMode') === '1'; } catch (e) { return false; } + } document.addEventListener('keydown', (ev) => { if (!ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey) return; if (!suspectPickOverlayOpen) return; + /* Test Mode ปิดอยู่ — ข้าม (ตั้งใน Admin Panel → Test Mode) */ + if (!__isJusticeTestModeOn()) return; /* ขณะอยู่ใน input/textarea/ที่แก้ไขได้ — ข้าม กันชน UX */ const ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return; diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index b5a565f..2f369cd 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 9c8edcb..c193466 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -426,10 +426,12 @@ } #suspect-pick-close:hover { transform: scale(1.07); filter: brightness(1.12); } #suspect-pick-close img { display: block; width: min(52px, 11vw); height: auto; } - /* ปุ่มบนโถง — เปิด overlay ใหม่เมื่อเฟสยัง active */ + /* ปุ่มบนโถง — เปิด overlay ใหม่เมื่อเฟสยัง active + NOTE: ต้องอยู่ "หลัง" #testimony-overlay (z 9000) และ #suspect-pick-overlay (z 10045) + เพื่อไม่ทับ overlay สำคัญ — ตั้งค่าต่ำกว่า 9000 (สูงพอเหนือ HUD โถง) */ #btn-suspect-reopen { position: fixed; - z-index: 10044; + z-index: 5500; left: max(12px, env(safe-area-inset-left)); top: 50%; transform: translateY(-50%); @@ -1121,6 +1123,9 @@