From eb8c67740cf4214c2cf7d6defdead33d83d2d584 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Tue, 16 Jun 2026 03:41:31 +0000 Subject: [PATCH] update all flow --- www/html/Admin/admin.js | 33 ++++- www/html/Admin/index.html | 14 +- www/html/Admin/private/store.json | 8 +- www/html/Game/data/game-timing.json | 3 +- www/html/Game/public/js/room-lobby.js | 190 +++++++++++++++++++++++++- www/html/Game/public/room-lobby.html | 4 +- www/html/Game/server.js | 35 +++++ 7 files changed, 275 insertions(+), 12 deletions(-) diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 6009ae9..adc6857 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -4107,6 +4107,35 @@ .then(function () { if (btn) btn.disabled = false; }); } + function loadTroublesomeForcePanel() { + var chk = el('troublesome-force-offer'); + if (!chk) return; + gameTimingFetch('GET') + .then(function (data) { + chk.checked = !!(data && data.troublesomeForceOffer); + setMsg('troublesome-force-msg', '', ''); + }) + .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'โหลดไม่ได้', 'error'); }); + } + function saveTroublesomeForcePanel() { + var chk = el('troublesome-force-offer'); + if (!chk) return; + var on = !!chk.checked; + var btn = el('btn-troublesome-force-save'); + if (btn) btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.troublesomeForceOffer = on; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && typeof res === 'object') chk.checked = !!res.troublesomeForceOffer; + setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง > 4)', 'ok'); + }) + .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'บันทึกไม่ได้', 'error'); }) + .then(function () { if (btn) btn.disabled = false; }); + } + function saveJumpSurviveTimingPanel() { var inp = el('jump-survive-height-mult'); var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5; @@ -4625,7 +4654,7 @@ if (name === 'game-timing') loadGameTimingPanel(); if (name === 'stack-game') loadStackGamePanel(); if (name === 'highscore') loadHighscore(); - if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); } + if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); } } function boot() { @@ -4853,6 +4882,8 @@ if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel); var specialCardSaveBtn = el('btn-special-card-by-map-save'); if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel); + var troublesomeForceSaveBtn = el('btn-troublesome-force-save'); + if (troublesomeForceSaveBtn) troublesomeForceSaveBtn.addEventListener('click', saveTroublesomeForcePanel); syncTestModeUi(); /* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */ window.addEventListener('storage', function (e) { diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index a6ca6da..d61e198 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -1109,6 +1109,9 @@
  • Ctrl + Q — ในเกมตอนคำถามพิเศษ (Special Quiz) เปิดอยู่: ตอบข้อปัจจุบันให้ถูกอัตโนมัติ
    เซิร์ฟเวอร์เป็นผู้เติมคำตอบที่ถูก (กันโกง) — ใช้ทดสอบ flow รับการ์ดพิเศษได้เร็ว
  • +
  • Ctrl + Alt + 1 / 2 / 3 — ที่หน้าห้อง LobbyB: พรีวิว "หน้าผลตัดสินคดี" โดยไม่ต้องเล่นจบ
    + 1 = โหวตถูก (เรือนจำ → โพเดียมผู้ชนะ) · 2 = โหวตผิด (ตัวป่วนชนะ) · 3 = สลับ มี/ไม่มีตัวป่วน (ดูสถานะในแชต) · ตัวป่วนจำลองใช้ผู้เล่น/บอทคนแรก +
  • @@ -1161,11 +1164,20 @@

    + +

    บังคับเสนอบท "ตัวป่วน" (เทสต์)

    +

    ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมีคนจริงเกิน 4 คน · เปิดอันนี้เพื่อบังคับให้ขึ้นเสมอแม้เล่นคนเดียว (ไว้เทสต์)

    + +
    +

    - + diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 3fdb57e..a4d8afd 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -38,9 +38,9 @@ "providerUserId": "p_1775109142385_wq7wfy1p32j", "notes": "auto: player-coins", "blocked": false, - "coins": 210, + "coins": 230, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-14T15:14:18+00:00", + "updatedAt": "2026-06-15T13:38:26+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -54,11 +54,11 @@ ], "lockUntilMs": 0 }, - "score": 190, + "score": 200, "scoreByCase": { "1": 10, "10": 70, - "8": 90, + "8": 100, "11": 20 }, "lbName": "Q" diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 1af2c0a..405aa44 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -101,5 +101,6 @@ "mnptfts2": 5, "mnpz6rkp": 6, "mnq1eml7": 7 - } + }, + "troublesomeForceOffer": true } \ No newline at end of file diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index def50d2..3adebeb 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -4924,6 +4924,9 @@ if (titleEl) titleEl.style.display = 'block'; } var names = (data && data.winnerNames && data.winnerNames.length) ? data.winnerNames.join(', ') : 'ไม่มีใครตอบถูก'; + _lastTrialWinners = (data && Array.isArray(data.winners)) ? data.winners.slice() : []; + _lastDisruptorId = (data && data.disruptorId) ? data.disruptorId : null; + _lastHasDisruptor = !!(data && data.hasDisruptor && data.disruptorId); var iWon = !!(data && Array.isArray(data.winners) && data.winners.indexOf(socket.id) >= 0); var banner = ensureTrialBanner(); if (banner) { @@ -4943,6 +4946,129 @@ } var trialFinalTimer = null; + var winPodiumTimer = null; + var _lastTrialWinners = []; + var _lastDisruptorId = null; + var _lastHasDisruptor = false; + + /** id ผู้เล่น/บอททั้งหมดในห้อง (เรียง peers ก่อน bot) — ใช้กรณีตัวป่วน: โชว์ทุกคนยกเว้นตัวป่วน */ + function allOccupantIds() { + var out = []; + if (peers) peers.forEach(function (_p, id) { out.push(id); }); + if (lobbyBots) lobbyBots.forEach(function (_b, id) { out.push(id); }); + return out; + } + + /** ข้อมูลตัวละคร/ชื่อ/สี ของผู้เล่นหรือบอท ตาม id (ใช้วางบนโพเดียมผู้ชนะ) + สี = colorTheme/colorSkin (rgb/hex) สำหรับระบบ tint — ไม่ใช่ index */ + function resolveOccupantCharInfo(id) { + var isMe = !!(socket && id === socket.id); + var p = (peers && peers.get(id)) || (lobbyBots && lobbyBots.get(id)); + if (p) { + return { + characterId: p.characterId || (isMe ? getStoredCharacterId() : null), + name: (p.nickname || '').trim() || (isMe ? 'คุณ' : String(id).slice(0, 6)), + colorTheme: (isMe ? (myTintTheme || p.colorTheme) : p.colorTheme) || '', + colorSkin: (isMe ? (myTintSkin || p.colorSkin) : p.colorSkin) || '', + }; + } + if (isMe) { + return { characterId: getStoredCharacterId(), name: 'คุณ', colorTheme: myTintTheme || '', colorSkin: myTintSkin || '' }; + } + if (typeof id === 'string' && id.indexOf('__lobby_bot_') === 0) { + var slot = parseInt(id.slice('__lobby_bot_'.length), 10); + return { characterId: null, name: 'บอท ' + ((Number.isFinite(slot) ? slot : 0) + 1), colorTheme: '', colorSkin: '' }; + } + return { characterId: null, name: String(id).slice(0, 6), colorTheme: '', colorSkin: '' }; + } + + /** img ตัวละครเต็มตัว (down idle) — โหลด sprite ฐานก่อน แล้ว swap เป็นตัวที่ "ทาสีแล้ว" (tint) เมื่อพร้อม */ + function buildPodiumCharImg(characterId, colorTheme, colorSkin) { + var img = document.createElement('img'); + img.className = 'av'; + img.alt = ''; + img.decoding = 'async'; + img.style.filter = 'drop-shadow(0 6px 12px rgba(0,0,0,.5))'; + if (!characterId) { img.src = defaultAvatarImg.src; return img; } + var cands = characterSpriteUrlCandidates(characterId, 'down'); + var ci = 0; + img.src = cands[0]; + img.onerror = function () { + ci++; + if (ci < cands.length) { img.src = cands[ci]; } + else { img.onerror = null; img.src = defaultAvatarImg.src; } + }; + if ((colorTheme || colorSkin) && typeof preloadLobbyTintedCharacter === 'function') { + preloadLobbyTintedCharacter(characterId, colorTheme, colorSkin, function () { + var ckey = characterId + '|' + (colorTheme || '') + '|' + (colorSkin || '') + '|down'; + var anim = tintedCharCache[ckey]; + var tinted = anim && anim.fallback; + if (tinted && tinted.complete && tinted.naturalWidth) { img.onerror = null; img.src = tinted.src; } + }); + } + return img; + } + + /** หน้า2 ของผลชนะ — โพเดียมผู้ชนะบน win-bg + มงกุฎ · memberIds = รายชื่อที่จะโชว์ (ดีฟอลต์ = ผู้โหวตถูก) */ + function renderWinPodium(memberIds) { + var R = '/Main-Lobby/IMAGE/Result/'; + var ov = document.getElementById('trial-final'); + if (!ov) return; + ov.classList.add('trial-final--podium'); + ov.style.backgroundImage = 'url(' + R + 'win-bg.png)'; + var culpEl = ov.querySelector('#trial-final-culprit'); + if (culpEl) culpEl.style.display = 'none'; + var txtEl = ov.querySelector('#trial-final-txt'); + if (txtEl) txtEl.style.display = 'none'; + var congrats = ov.querySelector('#trial-final-congrats'); + if (!congrats) { congrats = document.createElement('img'); congrats.id = 'trial-final-congrats'; congrats.alt = 'ยินดีด้วย ทีมนักสืบ ชนะ'; ov.appendChild(congrats); } + congrats.src = R + 'txt-congrats.png'; + var pod = ov.querySelector('#trial-final-podium'); + if (!pod) { pod = document.createElement('div'); pod.id = 'trial-final-podium'; ov.appendChild(pod); } + pod.innerHTML = ''; + var left = document.createElement('div'); left.className = 'tf-pod-side tf-pod-left'; + var right = document.createElement('div'); right.className = 'tf-pod-side tf-pod-right'; + var ids = (Array.isArray(memberIds) && memberIds.length) ? memberIds.slice(0, 6) + : ((_lastTrialWinners && _lastTrialWinners.length) ? _lastTrialWinners.slice(0, 6) : []); + if (!ids.length && socket && socket.id) ids = [socket.id]; + ids.forEach(function (id, i) { + var info = resolveOccupantCharInfo(id); + var cell = document.createElement('div'); cell.className = 'tf-pod-char'; + if (i === 0) { var cr = document.createElement('div'); cr.className = 'crown'; cr.textContent = '👑'; cell.appendChild(cr); } + var nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = info.name || ''; + cell.appendChild(nm); + cell.appendChild(buildPodiumCharImg(info.characterId, info.colorTheme, info.colorSkin)); + (i % 2 === 0 ? left : right).appendChild(cell); + }); + pod.appendChild(left); + pod.appendChild(right); + var home = ov.querySelector('#trial-final-home'); + if (home) home.style.display = ''; + } + + /** หน้าผลโหวตผิด "ตัวป่วนชนะ" (กรณีมีตัวป่วน) — โชว์เฉพาะตัวป่วน 1 ตัว บน troll-win-bg */ + function renderTrollWinDisruptor(disruptorId) { + var R = '/Main-Lobby/IMAGE/Result/'; + var ov = document.getElementById('trial-final'); + if (!ov) return; + ov.classList.remove('trial-final--podium'); + ov.classList.add('trial-final--trollwin'); + ov.style.backgroundImage = 'url(' + R + 'troll-win-bg.png)'; + var culpEl = ov.querySelector('#trial-final-culprit'); + if (culpEl) culpEl.style.display = 'none'; + var txtEl = ov.querySelector('#trial-final-txt'); + if (txtEl) { txtEl.src = R + 'txt-troll-win.png'; txtEl.className = ''; txtEl.style.display = 'block'; } + var dWrap = ov.querySelector('#trial-final-disruptor'); + if (!dWrap) { dWrap = document.createElement('div'); dWrap.id = 'trial-final-disruptor'; ov.appendChild(dWrap); } + dWrap.innerHTML = ''; + var info = resolveOccupantCharInfo(disruptorId); + var nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = info.name || ''; + dWrap.appendChild(nm); + dWrap.appendChild(buildPodiumCharImg(info.characterId, info.colorTheme, info.colorSkin)); + var home = ov.querySelector('#trial-final-home'); + if (home) home.style.display = ''; + } + function injectFinalResultStyle() { if (document.getElementById('trial-final-style')) return; var R = '/Main-Lobby/IMAGE/Result/'; @@ -4957,7 +5083,25 @@ '#trial-final-txt{position:absolute;left:50%;top:7%;transform:translateX(-50%);width:780px;max-width:90vw;height:auto;z-index:2;display:none}' + /* ชนะ: ข้อความ "ยินดีด้วย ชนะ" วางช่วงล่างเหนือปุ่มกลับ (เลี่ยงป้าย "เรือนจำไซเบอร์" บนสุด) */ '#trial-final-txt.final-txt-congrats{top:auto;bottom:21%;width:420px;filter:drop-shadow(0 2px 10px rgba(0,0,0,.7))}' + - '#trial-final-home{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);width:300px;height:104px;max-width:70vw;background:url(' + R + 'btn-gohome.png) center/contain no-repeat;border:none;cursor:pointer;font-size:0;color:transparent;z-index:3;filter:drop-shadow(0 4px 14px rgba(0,0,0,.6))}'; + '#trial-final-home{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);width:300px;height:104px;max-width:70vw;background:url(' + R + 'btn-gohome.png) center/contain no-repeat;border:none;cursor:pointer;font-size:0;color:transparent;z-index:5;filter:drop-shadow(0 4px 14px rgba(0,0,0,.6))}' + + /* ===== หน้า2 (ชนะ): โพเดียมผู้ชนะ win-bg + ตัวละคร + มงกุฎ ===== */ + '#trial-final.trial-final--podium{background-image:url(' + R + 'win-bg.png)!important}' + + '#trial-final-congrats{position:absolute;left:50%;top:5%;transform:translateX(-50%);width:560px;max-width:82vw;height:auto;z-index:4;display:none;filter:drop-shadow(0 3px 14px rgba(0,0,0,.6))}' + + '#trial-final.trial-final--podium #trial-final-congrats{display:block}' + + '#trial-final-podium{position:absolute;left:0;right:0;bottom:9%;height:58%;display:none;z-index:3;pointer-events:none}' + + '#trial-final.trial-final--podium #trial-final-podium{display:block}' + + '.tf-pod-side{position:absolute;bottom:0;display:flex;align-items:flex-end;justify-content:center;gap:min(3vw,52px)}' + + '.tf-pod-left{left:2%;right:56%}' + + '.tf-pod-right{left:56%;right:2%}' + + '.tf-pod-char{position:relative;display:flex;flex-direction:column;align-items:center;width:min(17vw,230px)}' + + '.tf-pod-char .crown{font-size:min(7vw,62px);line-height:1;margin-bottom:-2px;filter:drop-shadow(0 0 10px rgba(255,200,60,.95))}' + + '.tf-pod-char .nm{margin-bottom:8px;color:#eafcff;font:800 clamp(14px,1.7vw,26px)/1 Kanit,system-ui,sans-serif;text-shadow:0 0 10px rgba(0,0,0,.85),0 2px 4px rgba(0,0,0,.9);white-space:nowrap}' + + '.tf-pod-char img.av{width:100%;height:auto;image-rendering:auto}' + + /* ===== ตัวป่วนชนะ (โหวตผิด) — โชว์ตัวป่วนเดี่ยวกลางเวที ===== */ + '#trial-final-disruptor{position:absolute;left:50%;bottom:17%;transform:translateX(-50%);display:none;flex-direction:column;align-items:center;width:min(21vw,280px);z-index:3;pointer-events:none}' + + '#trial-final.trial-final--trollwin #trial-final-disruptor{display:flex}' + + '#trial-final-disruptor .nm{margin-bottom:10px;color:#ffd6e6;font:800 clamp(15px,2vw,30px)/1 Kanit,system-ui,sans-serif;text-shadow:0 0 12px rgba(0,0,0,.9),0 2px 5px rgba(0,0,0,.95);white-space:nowrap}' + + '#trial-final-disruptor img.av{width:100%;height:auto}'; document.head.appendChild(st); } function showFinalResult(correct, culprit, anyVotes) { @@ -4975,17 +5119,32 @@ } var culpEl = ov.querySelector('#trial-final-culprit'); var txtEl = ov.querySelector('#trial-final-txt'); + var homeEl = ov.querySelector('#trial-final-home'); + /* รีเซ็ตสถานะหน้าก่อน (เผื่อรอบก่อนเป็นหน้าโพเดียม/ตัวป่วน) */ + if (winPodiumTimer) { clearTimeout(winPodiumTimer); winPodiumTimer = null; } + ov.classList.remove('trial-final--podium', 'trial-final--trollwin'); + var dWrap0 = ov.querySelector('#trial-final-disruptor'); if (dWrap0) dWrap0.innerHTML = ''; + if (homeEl) homeEl.style.display = ''; if (correct) { - /* ชนะ — จับคนร้ายจริง: เรือนจำไซเบอร์ + คนร้ายในคุก + ข้อความ "ยินดีด้วย ทีมนักสืบ ชนะ!" */ + /* ชนะ — 2 หน้า: (หน้า1) เรือนจำไซเบอร์ + คนร้ายในคุก → (หน้า2) โพเดียมผู้ชนะ + ตัวละคร + มีตัวป่วน → โพเดียมโชว์ "ทุกคนยกเว้นตัวป่วน" · ไม่มี → โชว์ผู้โหวตถูก (ดีฟอลต์) */ ov.style.backgroundImage = 'url(' + R + 'cyber-prison.png)'; var culpIdx = (typeof culprit === 'number') ? culprit : 0; if (culpEl) { culpEl.src = getCulpritPrisonImageUrl(culpIdx); culpEl.style.display = 'block'; } - if (txtEl) { txtEl.src = R + 'txt-congrats.png'; txtEl.className = 'final-txt-congrats'; txtEl.style.display = 'block'; } + if (txtEl) txtEl.style.display = 'none'; /* หน้า1 ไม่มีข้อความ congrats (อยู่หน้า2) */ + if (homeEl) homeEl.style.display = 'none'; /* หน้า1 ยังกลับไม่ได้ — auto เข้าหน้า2 */ + var podMembers = _lastHasDisruptor + ? allOccupantIds().filter(function (id) { return id !== _lastDisruptorId; }) + : null; + winPodiumTimer = setTimeout(function () { renderWinPodium(podMembers); }, 3500); + } else if (anyVotes && _lastHasDisruptor && _lastDisruptorId) { + /* โหวตผิด + มีตัวป่วน → "ตัวป่วนชนะ" โชว์เฉพาะตัวป่วน */ + renderTrollWinDisruptor(_lastDisruptorId); } else if (anyVotes) { - /* จับแพะ — โหวตผิดคน: "อุปส์..ตัวป่วนชนะ ความจริงถูกบิดเบือน!" (troll-win) */ + /* จับแพะ — โหวตผิดคน (ไม่มีตัวป่วน): "ตัวป่วนชนะ ความจริงถูกบิดเบือน!" (troll-win เปล่า) */ ov.style.backgroundImage = 'url(' + R + 'troll-win-bg.png)'; if (txtEl) { txtEl.src = R + 'txt-troll-win.png'; txtEl.className = ''; txtEl.style.display = 'block'; } if (culpEl) culpEl.style.display = 'none'; @@ -4999,6 +5158,29 @@ try { closeSuspectOverlay(); } catch (e) { /* ignore */ } } + /* ===== Key ลัด (เฉพาะ Test Mode) — พรีวิวหน้าผลจบโดยไม่ต้องเล่นจบจริง ===== + Ctrl+Alt+1 = โหวตถูก · Ctrl+Alt+2 = โหวตผิด · Ctrl+Alt+3 = สลับมี/ไม่มีตัวป่วน */ + var _testDisruptorOn = true; + function __previewTrialFinal(correct, anyVotes) { + var occ = allOccupantIds(); + var disruptor = (_testDisruptorOn && occ.length) ? occ[0] : null; + _lastDisruptorId = disruptor; + _lastHasDisruptor = !!disruptor; + _lastTrialWinners = occ.slice(0, Math.min(4, occ.length)); /* ผู้โหวตถูก (ใช้กรณีไม่มีตัวป่วน) */ + showFinalResult(correct, 0, anyVotes); + } + document.addEventListener('keydown', function (e) { + if (!e.ctrlKey || !e.altKey || e.repeat) return; + if (!__isJusticeTestModeOn()) return; + if (e.code === 'Digit1') { e.preventDefault(); __previewTrialFinal(true, true); } + else if (e.code === 'Digit2') { e.preventDefault(); __previewTrialFinal(false, true); } + else if (e.code === 'Digit3') { + e.preventDefault(); + _testDisruptorOn = !_testDisruptorOn; + try { appendLobbySystemChat('— [Test] ตัวป่วน: ' + (_testDisruptorOn ? 'มี' : 'ไม่มี') + ' (Ctrl+Alt+1 ถูก / Ctrl+Alt+2 ผิด)'); } catch (eC) { /* ignore */ } + } + }); + /* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */ var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2, hostId: '', submittedIds: [], readyIds: [], iReadied: false }; diff --git a/www/html/Game/public/room-lobby.html b/www/html/Game/public/room-lobby.html index 5eb4825..05ed1ba 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -426,6 +426,8 @@ } #suspect-pick-close:hover { transform: scale(1.07); filter: brightness(1.12); } #suspect-pick-close img { display: block; width: min(52px, 11vw); height: auto; } + /* หน้าโหวตชี้ตัวคนร้าย (trial) — ปิดไม่ได้ ต้องโหวต → ซ่อนปุ่มปิด */ + .suspect-pick--trial #suspect-pick-close { display: none !important; } /* ปุ่มบนโถง — เปิด overlay ใหม่เมื่อเฟสยัง active NOTE: ต้องอยู่ "หลัง" #testimony-overlay (z 9000) และ #suspect-pick-overlay (z 10045) เพื่อไม่ทับ overlay สำคัญ — ตั้งค่าต่ำกว่า 9000 (สูงพอเหนือ HUD โถง) */ @@ -1613,7 +1615,7 @@ - +
    v —
    diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 8ed3a65..6cb1c95 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -1953,6 +1953,8 @@ function defaultGameTiming() { forcedMinigameKeys: ['', '', ''], /** โหมดเทสต์: บังคับการ์ดพิเศษต่อเกม { mapId: cardId } */ testSpecialCardByMap: {}, + /** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง > 4) */ + troublesomeForceOffer: false, }; } @@ -2341,6 +2343,7 @@ function loadGameTiming() { j.forcedMinigameKeys != null ? j.forcedMinigameKeys : j.forcedMinigameKey, ), testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap), + troublesomeForceOffer: !!j.troublesomeForceOffer, }; } } catch (e) { console.error('loadGameTiming', e.message); } @@ -2554,6 +2557,9 @@ function saveGameTimingToFile(d) { testSpecialCardByMap: Object.prototype.hasOwnProperty.call(d, 'testSpecialCardByMap') ? normalizeTestSpecialCardByMap(d.testSpecialCardByMap) : normalizeTestSpecialCardByMap(prev.testSpecialCardByMap), + troublesomeForceOffer: Object.prototype.hasOwnProperty.call(d, 'troublesomeForceOffer') + ? !!d.troublesomeForceOffer + : !!prev.troublesomeForceOffer, }; fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8'); runtimeGameTiming = out; @@ -3537,12 +3543,23 @@ function computeAndEmitTrialResult(sid, space) { } return String(id).slice(0, 6); }); + /* ตัวป่วน (ถ้ามีคนรับบท) — หา socket.id ปัจจุบันจาก playerKey/nickname (id เดิมตอนเสนออาจเปลี่ยนแล้ว) */ + let disruptorId = null; + if (space.troublesomeAccept && (space.disruptorPlayerKey || space.disruptorNickname)) { + for (const [pid, p] of space.peers) { + const keyMatch = space.disruptorPlayerKey && p.playerKey === space.disruptorPlayerKey; + const nickMatch = space.disruptorNickname && normalizeLobbyNickname(p.nickname) === space.disruptorNickname; + if (keyMatch || nickMatch) { disruptorId = pid; break; } + } + } io.to(sid).emit('trial-result', { culpritIndex: culprit, counts, winners, winnerNames, cardMinigames: space.suspectCardMinigames || [], + disruptorId, + hasDisruptor: !!disruptorId, }); space.silencedPlayerId = null; } @@ -6717,6 +6734,12 @@ function attemptTroublesomeRoll(sid, force) { const peerIds = [...space.peers.keys()]; if (peerIds.length === 0) return; if (!force && space.troublesomeEligible.size < peerIds.length) return; + /* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง > 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */ + const forceOffer = !!(runtimeGameTiming && runtimeGameTiming.troublesomeForceOffer); + if (peerIds.length <= 4 && !forceOffer) { + space.troublesomeOfferSent = true; /* มาร์คว่าผ่านรอบนี้ไปแล้ว — ไม่มีตัวป่วน */ + return; + } space.troublesomeOfferSent = true; if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer); @@ -7479,6 +7502,15 @@ io.on('connection', (socket) => { if (space.troublesomeResponseReceived) return; space.troublesomeResponseReceived = true; space.troublesomeAccept = accept; + /* เก็บตัวตนตัวป่วนแบบถาวร (playerKey/nickname) — socket.id เปลี่ยนหลังเล่นมินิเกม ใช้ id ตรง ๆ ไม่ได้ */ + if (accept) { + const dPeer = space.peers.get(socket.id); + space.disruptorPlayerKey = (dPeer && dPeer.playerKey) ? dPeer.playerKey : ''; + space.disruptorNickname = dPeer ? normalizeLobbyNickname(dPeer.nickname) : ''; + } else { + space.disruptorPlayerKey = ''; + space.disruptorNickname = ''; + } let idx = Math.floor(Number(space.suspectPickIndex)); if (Number.isNaN(idx) || idx < 0 || idx > 2) idx = 0; space.suspectPickIndex = idx; @@ -7895,6 +7927,9 @@ io.on('connection', (socket) => { space.troublesomeOfferSent = false; space.troublesomeTargetId = null; space.troublesomeResponseReceived = false; + space.troublesomeAccept = false; + space.disruptorPlayerKey = ''; + space.disruptorNickname = ''; space.suspectPhaseActive = false; space.suspectPickIndex = 0; space.detectiveLobbyLevel = Number(lobbyLevel) || 1;