update all flow

This commit is contained in:
2026-06-16 03:41:31 +00:00
parent 31f5211574
commit eb8c67740c
7 changed files with 275 additions and 12 deletions
+32 -1
View File
@@ -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) {
+13 -1
View File
@@ -1109,6 +1109,9 @@
<li><kbd>Ctrl</kbd> + <kbd>Q</kbd> — ในเกมตอนคำถามพิเศษ (Special Quiz) เปิดอยู่: ตอบข้อปัจจุบันให้ถูกอัตโนมัติ<br>
<span class="muted" style="font-size:13px;">เซิร์ฟเวอร์เป็นผู้เติมคำตอบที่ถูก (กันโกง) — ใช้ทดสอบ flow รับการ์ดพิเศษได้เร็ว</span>
</li>
<li><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>1</kbd> / <kbd>2</kbd> / <kbd>3</kbd> — ที่หน้าห้อง LobbyB: พรีวิว "หน้าผลตัดสินคดี" โดยไม่ต้องเล่นจบ<br>
<span class="muted" style="font-size:13px;"><kbd>1</kbd> = โหวตถูก (เรือนจำ → โพเดียมผู้ชนะ) · <kbd>2</kbd> = โหวตผิด (ตัวป่วนชนะ) · <kbd>3</kbd> = สลับ มี/ไม่มีตัวป่วน (ดูสถานะในแชต) · ตัวป่วนจำลองใช้ผู้เล่น/บอทคนแรก</span>
</li>
</ul>
<p id="test-mode-msg" class="msg" role="status" style="margin-top:14px;"></p>
@@ -1161,11 +1164,20 @@
<div id="special-card-by-map-rows" style="display:flex;flex-direction:column;gap:8px;max-width:640px;"></div>
<div style="margin-top:12px;"><button type="button" class="btn" id="btn-special-card-by-map-save">บันทึก</button></div>
<p id="special-card-by-map-msg" class="msg" role="status"></p>
<h3 class="admin-subheading" style="margin-top:28px;">บังคับเสนอบท "ตัวป่วน" (เทสต์)</h3>
<p class="muted">ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมี<strong>คนจริงเกิน 4 คน</strong> · เปิดอันนี้เพื่อ<strong>บังคับให้ขึ้นเสมอ</strong>แม้เล่นคนเดียว (ไว้เทสต์)</p>
<label style="display:flex;align-items:center;gap:10px;color:#cfd6ea;font-size:14px;cursor:pointer;">
<input type="checkbox" id="troublesome-force-offer" style="width:18px;height:18px;cursor:pointer;">
บังคับเสนอบทตัวป่วน (ข้ามเงื่อนไข > 4 คน)
</label>
<div style="margin-top:12px;"><button type="button" class="btn" id="btn-troublesome-force-save">บันทึก</button></div>
<p id="troublesome-force-msg" class="msg" role="status"></p>
</section>
</div><!-- /.admin-content -->
</div>
</main>
</div>
<script src="admin.js?v=82"></script>
<script src="admin.js?v=83"></script>
</body>
</html>
+4 -4
View File
@@ -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"
+2 -1
View File
@@ -101,5 +101,6 @@
"mnptfts2": 5,
"mnpz6rkp": 6,
"mnq1eml7": 7
}
},
"troublesomeForceOffer": true
}
+186 -4
View File
@@ -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 };
+3 -1
View File
@@ -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 @@
<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.0278"></script>
<script src="js/room-lobby.js?v=0.0282"></script>
<div class="version-tag">v —</div>
</body>
</html>
+35
View File
@@ -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;