updateflow
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* สาธารณะ — บวกเหรียญให้บัญชี guest (ใช้กับการ์ดพิเศษ Fund ในเกม)
|
||||
* POST JSON: { "playerKey": "...", "amount": 10 }
|
||||
* จำกัด amount ต่อครั้งกันการยิงมั่ว
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_response(['ok' => false, 'error' => 'Use POST'], 405);
|
||||
}
|
||||
|
||||
$body = require_json_body();
|
||||
$key = trim((string)($body['playerKey'] ?? ''));
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key)) {
|
||||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||||
}
|
||||
|
||||
$amount = (int)($body['amount'] ?? 0);
|
||||
// กันยิงมั่ว: ครั้งละ 1–100 เหรียญ
|
||||
if ($amount < 1 || $amount > 100) {
|
||||
json_response(['ok' => false, 'error' => 'amount ต้องอยู่ระหว่าง 1–100'], 400);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
$accounts = $store['accounts'] ?? [];
|
||||
|
||||
foreach ($accounts as $i => $a) {
|
||||
if (($a['loginType'] ?? '') !== 'guest') {
|
||||
continue;
|
||||
}
|
||||
if (($a['providerUserId'] ?? '') !== $key) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($a['blocked'])) {
|
||||
json_response(['ok' => false, 'error' => 'บัญชีถูกระงับ'], 403);
|
||||
}
|
||||
$coins = max(0, (int)($a['coins'] ?? 0)) + $amount;
|
||||
$store['accounts'][$i]['coins'] = $coins;
|
||||
$store['accounts'][$i]['updatedAt'] = gmdate('c');
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||||
}
|
||||
json_response(['ok' => true, 'coins' => $coins, 'added' => $amount, 'accountId' => $a['id'] ?? null]);
|
||||
}
|
||||
|
||||
// ยังไม่มีบัญชี → สร้างใหม่พร้อมเหรียญ
|
||||
$new = [
|
||||
'id' => new_id(),
|
||||
'email' => '',
|
||||
'displayName' => 'Guest',
|
||||
'loginType' => 'guest',
|
||||
'providerUserId' => $key,
|
||||
'notes' => 'auto: player-coins-add',
|
||||
'blocked' => false,
|
||||
'coins' => $amount,
|
||||
'createdAt' => gmdate('c'),
|
||||
'updatedAt' => gmdate('c'),
|
||||
];
|
||||
$store['accounts'][] = $new;
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'สร้างบัญชีไม่สำเร็จ'], 500);
|
||||
}
|
||||
|
||||
json_response(['ok' => true, 'coins' => $amount, 'added' => $amount, 'accountId' => $new['id']]);
|
||||
@@ -1003,6 +1003,9 @@
|
||||
<li><kbd>Ctrl</kbd> + <kbd>1</kbd> / <kbd>2</kbd> / <kbd>3</kbd> — ในหน้าเลือกผู้ต้องสงสัย: เก็บหลักฐาน 1 ใบให้ suspect คนนั้นทันที (เทียบเท่าเล่นมินิเกมจบ 1 ครั้ง)<br>
|
||||
<span class="muted" style="font-size:13px;">กดครบ 3 ปุ่ม → ปุ่ม "ชี้ตัวคนร้าย" โผล่</span>
|
||||
</li>
|
||||
<li><kbd>Ctrl</kbd> + <kbd>Q</kbd> — ในเกมตอนคำถามพิเศษ (Special Quiz) เปิดอยู่: ตอบข้อปัจจุบันให้ถูกอัตโนมัติ<br>
|
||||
<span class="muted" style="font-size:13px;">เซิร์ฟเวอร์เป็นผู้เติมคำตอบที่ถูก (กันโกง) — ใช้ทดสอบ flow รับการ์ดพิเศษได้เร็ว</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p id="test-mode-msg" class="msg" role="status" style="margin-top:14px;"></p>
|
||||
|
||||
@@ -101,6 +101,18 @@
|
||||
"coins": 0,
|
||||
"createdAt": "2026-05-21T07:52:34+00:00",
|
||||
"updatedAt": "2026-05-21T07:52:34+00:00"
|
||||
},
|
||||
{
|
||||
"id": "4aded83523028d3837fc9fa0",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "testkey_phase2_demo",
|
||||
"notes": "auto: player-coins-add",
|
||||
"blocked": false,
|
||||
"coins": 10,
|
||||
"createdAt": "2026-06-01T15:32:12+00:00",
|
||||
"updatedAt": "2026-06-01T15:32:12+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -335,6 +335,15 @@
|
||||
if (hasAny) sessionStorage.setItem('justiceDetectiveMyEvidence', JSON.stringify(res.myPlayerEvidence));
|
||||
else sessionStorage.removeItem('justiceDetectiveMyEvidence');
|
||||
}
|
||||
/* โชว์การ์ดหลักฐานที่เพิ่งได้ ตอนกลับถึง LobbyB (เปิดแฟ้มอัตโนมัติ + ไฮไลต์ใบใหม่) */
|
||||
if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) {
|
||||
sessionStorage.setItem('justiceEvidenceReveal', JSON.stringify({
|
||||
suspectIndex: res.suspectIndex,
|
||||
awardedCard: (typeof res.awardedCard === 'number') ? res.awardedCard : null,
|
||||
freeEvidenceCount: res.freeEvidenceCount || 0,
|
||||
ts: Date.now(),
|
||||
}));
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
go();
|
||||
});
|
||||
@@ -11353,6 +11362,10 @@
|
||||
* server เป็นผู้ตัดสิน: สุ่มไอคอน 1/3, เปิดคำถามให้ทุกคนพร้อมกัน, ตอบถูกทุกคนทุกข้อ -> ได้การ์ด
|
||||
*/
|
||||
const SPECIAL_QUIZ_CHOICE_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
||||
/** Test Mode (Admin) เก็บใน localStorage — เปิด shortcut ทดสอบ เช่น Ctrl+Q ตอบถูก */
|
||||
function __isTestModeOnPlay() {
|
||||
try { return localStorage.getItem('justiceTestMode') === '1'; } catch (e) { return false; }
|
||||
}
|
||||
let specialQuizIcon = null; // { iconType, x, y } (พิกัด tile-center)
|
||||
let specialQuizCollideSent = false;
|
||||
let specialQuizAwardedCard = null; // การ์ดที่ได้รอบนี้ (จาก special-quiz-ended)
|
||||
@@ -11640,6 +11653,86 @@
|
||||
cancelAnimationFrame(specialQuizTimerRaf);
|
||||
}
|
||||
|
||||
/** การ์ดพิเศษถูก "ใช้" — โชว์ animation + ขยายเวลามินิเกมฝั่ง client (ถ้าเป็น Time Dilation) */
|
||||
function applySpecialCardClient(data) {
|
||||
const card = data.card || {};
|
||||
const addSec = Number(data.addSec) || 0;
|
||||
const coins = Number(data.coins) || 0;
|
||||
if (addSec > 0 && card.effectKey === 'time_dilation') {
|
||||
extendCurrentMinigameTimeClient(addSec);
|
||||
}
|
||||
if (coins > 0 && card.effectKey === 'fund') {
|
||||
addOwnCoinsViaApi(coins);
|
||||
}
|
||||
showSpecialCardUseAnimation(card, { addSec: addSec, coins: coins });
|
||||
}
|
||||
|
||||
/** Card 6 Fund — แต่ละ client บวกเหรียญของตัวเอง (อ่าน jdPlayerKey จาก localStorage) */
|
||||
function addOwnCoinsViaApi(amount) {
|
||||
let key = '';
|
||||
try { key = (localStorage.getItem('jdPlayerKey') || '').trim(); } catch (e) { key = ''; }
|
||||
if (!key || key.length < 8) return;
|
||||
const url = (typeof appPath === 'function' ? appPath('/Admin/api/player-coins-add.php') : '/Admin/api/player-coins-add.php');
|
||||
try {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ playerKey: key, amount: amount }),
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d && d.ok && typeof d.coins === 'number') {
|
||||
try { localStorage.setItem('jdCoins', String(d.coins)); } catch (e) { /* ignore */ }
|
||||
}
|
||||
}).catch(function () { /* ignore */ });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** +วินาที ให้มินิเกมที่กำลังเล่น (jump/shooter/quiz carry) — mng8a80o จัดการฝั่ง server */
|
||||
function extendCurrentMinigameTimeClient(sec) {
|
||||
const ms = Math.max(0, sec * 1000);
|
||||
if (!ms) return;
|
||||
if (isJumpSurvive() && jumpSurviveSessionStartMs) jumpSurviveSessionStartMs += ms;
|
||||
if (isSpaceShooter() && spaceShooterSessionStartMs) spaceShooterSessionStartMs += ms;
|
||||
if (isQuizCarry()) {
|
||||
if (quizCarryAnswerCloseAt > 0) quizCarryAnswerCloseAt += ms;
|
||||
if (quizCarryOptionRevealAt > 0) quizCarryOptionRevealAt += ms;
|
||||
}
|
||||
}
|
||||
|
||||
function showSpecialCardUseAnimation(card, opts) {
|
||||
opts = opts || {};
|
||||
let ov = document.getElementById('special-card-use-overlay');
|
||||
if (!ov) {
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'special-card-use-overlay';
|
||||
ov.className = 'is-hidden';
|
||||
ov.innerHTML =
|
||||
'<div class="scu-card"><img class="scu-img" alt="" />' +
|
||||
'<div class="scu-name"></div><div class="scu-effect"></div></div>';
|
||||
document.body.appendChild(ov);
|
||||
}
|
||||
const img = ov.querySelector('.scu-img');
|
||||
const nameEl = ov.querySelector('.scu-name');
|
||||
const fxEl = ov.querySelector('.scu-effect');
|
||||
if (img) img.src = card.imageUrl || (card.cardId ? '/Game/img/special-cards/card-' + card.cardId + '.png' : '');
|
||||
if (nameEl) nameEl.textContent = 'ใช้การ์ด: ' + (card.en || card.th || 'Special Card');
|
||||
if (fxEl) {
|
||||
let fx = card.desc || card.th || '';
|
||||
if (opts.addSec > 0) fx = '+' + opts.addSec + ' วินาที';
|
||||
else if (opts.coins > 0) fx = 'ทุกคน +' + opts.coins + ' COINS';
|
||||
fxEl.textContent = fx;
|
||||
}
|
||||
ov.classList.remove('is-hidden');
|
||||
ov.classList.remove('scu-play');
|
||||
void ov.offsetWidth;
|
||||
ov.classList.add('scu-play');
|
||||
clearTimeout(showSpecialCardUseAnimation._t);
|
||||
showSpecialCardUseAnimation._t = setTimeout(function () {
|
||||
ov.classList.add('is-hidden');
|
||||
ov.classList.remove('scu-play');
|
||||
}, 2600);
|
||||
}
|
||||
|
||||
function endSpecialQuiz(data) {
|
||||
cancelAnimationFrame(specialQuizTimerRaf);
|
||||
specialQuizActiveQuestion = null;
|
||||
@@ -15544,6 +15637,9 @@
|
||||
if (specialQuizForceDebug) {
|
||||
try { socket.emit('special-quiz-force', { on: true }); } catch (eF) { /* ignore */ }
|
||||
}
|
||||
if (__isTestModeOnPlay()) {
|
||||
try { socket.emit('special-quiz-testmode', { on: true }); } catch (eT) { /* ignore */ }
|
||||
}
|
||||
let botSlots = parseInt(res.botSlotCount, 10);
|
||||
if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) {
|
||||
botSlots = 6 - res.maxPlayers;
|
||||
@@ -15974,6 +16070,10 @@
|
||||
if (previewMode) return;
|
||||
endSpecialQuiz(data);
|
||||
});
|
||||
socket.on('special-card-applied', (data) => {
|
||||
if (previewMode || !data || !data.card) return;
|
||||
applySpecialCardClient(data);
|
||||
});
|
||||
|
||||
socket.on('gauntlet-sync', (data) => {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
@@ -19476,6 +19576,20 @@
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
/* Test Mode shortcut: Ctrl+Q = ตอบถูกใน special quiz (server เติมคำตอบให้) */
|
||||
if (e.ctrlKey && (e.code === 'KeyQ' || e.key === 'q' || e.key === 'Q')) {
|
||||
if (specialQuizActiveQuestion && !specialQuizAnswered && __isTestModeOnPlay()) {
|
||||
e.preventDefault();
|
||||
specialQuizAnswered = true;
|
||||
const status = document.getElementById('sq-ov-status');
|
||||
if (status) status.textContent = '[Test] ส่งคำตอบที่ถูกอัตโนมัติ — รอผู้เล่นคนอื่น...';
|
||||
if (specialQuizSelfId()) markSpecialQuizMemberAnswered(specialQuizSelfId());
|
||||
const choicesEl = document.getElementById('sq-ov-choices');
|
||||
if (choicesEl) Array.prototype.forEach.call(choicesEl.children, function (b) { b.disabled = true; });
|
||||
try { socket.emit('special-quiz-answer', { debugCorrect: true }, function () {}); } catch (eQ) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isMovementKey(e.code) && isChatFocused()) return;
|
||||
if (previewMode && editorEmbedReturn && mapData && !isChatFocused() && !isQuizQuestionMissionHudActivePlay() && !isStackTowerEmbedZoomLockedPlay()) {
|
||||
if (e.code === 'BracketLeft' || e.code === 'Minus' || e.code === 'NumpadSubtract') {
|
||||
|
||||
@@ -4356,6 +4356,28 @@
|
||||
'#trial-vote-counter{position:absolute;right:22px;top:14px;color:#7fe9ff;font:800 24px/1 Kanit,system-ui,sans-serif;display:none;z-index:62;text-shadow:0 0 12px rgba(34,211,238,.7)}' +
|
||||
'#trial-vote-counter b{color:#fff}' +
|
||||
'.suspect-pick--trial #trial-vote-counter{display:block}' +
|
||||
'#trial-vote-timer{position:absolute;right:22px;top:50px;color:#9ece6a;font:900 34px/1 Kanit,system-ui,sans-serif;display:none;z-index:62;text-shadow:0 0 14px rgba(158,206,106,.6)}' +
|
||||
'.suspect-pick--trial:not(.trial-revealed) #trial-vote-timer{display:block}' +
|
||||
'#trial-vote-timer.low{color:#f7768e;text-shadow:0 0 14px rgba(247,118,142,.7)}' +
|
||||
'#trial-card-notice{position:absolute;left:50%;top:9%;transform:translateX(-50%);z-index:63;background:linear-gradient(180deg,#1f2747,#141a33);border:1px solid rgba(255,214,102,.75);color:#ffe7b3;font:800 18px/1.3 Kanit;padding:10px 22px;border-radius:999px;box-shadow:0 10px 30px rgba(0,0,0,.5);display:none;text-align:center}' +
|
||||
'#pick-vote-overlay{position:fixed;inset:0;z-index:120;background:rgba(6,9,20,.86);display:none;align-items:center;justify-content:center;backdrop-filter:blur(3px)}' +
|
||||
'#pick-vote-overlay.is-open{display:flex}' +
|
||||
'.pv-shell{width:min(620px,92vw);background:linear-gradient(180deg,#161d38,#0e1326);border:1px solid rgba(124,154,255,.35);border-radius:18px;box-shadow:0 24px 60px rgba(0,0,0,.6);padding:22px 24px;text-align:center}' +
|
||||
'.pv-badge{display:inline-block;font:900 13px/1 Kanit;letter-spacing:1px;padding:6px 14px;border-radius:999px;background:rgba(255,214,102,.16);color:#ffd666;border:1px solid rgba(255,214,102,.5)}' +
|
||||
'.pv-title{margin:12px 0 2px;font:900 26px/1.2 Kanit;color:#fff}' +
|
||||
'.pv-sub{margin:0 0 6px;font:600 14px/1.4 Kanit;color:#9fb0d8}' +
|
||||
'.pv-timer{font:900 30px/1 Kanit;color:#9ece6a;margin:4px 0 12px;text-shadow:0 0 12px rgba(158,206,106,.5)}' +
|
||||
'.pv-timer.low{color:#f7768e;text-shadow:0 0 12px rgba(247,118,142,.6)}' +
|
||||
'.pv-list{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin:0 0 14px}' +
|
||||
'.pv-cand{position:relative;min-width:120px;padding:12px 14px;border-radius:14px;background:#1b2240;border:2px solid rgba(124,154,255,.25);color:#dfe6ff;font:800 16px/1.2 Kanit;cursor:pointer;transition:transform .12s,border-color .12s,background .12s}' +
|
||||
'.pv-cand:hover{transform:translateY(-2px);border-color:rgba(124,154,255,.6)}' +
|
||||
'.pv-cand.is-picked{border-color:#9ece6a;background:#1f3326;box-shadow:0 0 16px rgba(158,206,106,.35)}' +
|
||||
'.pv-cand .pv-me{font:700 11px/1 Kanit;color:#7fe9ff;display:block;margin-top:4px}' +
|
||||
'.pv-cand .pv-bot{font:700 11px/1 Kanit;color:#9fb0d8;display:block;margin-top:4px}' +
|
||||
'.pv-cand .pv-count{position:absolute;top:-8px;right:-8px;min-width:24px;height:24px;border-radius:999px;background:#f7768e;color:#fff;font:900 13px/24px Kanit;display:none}' +
|
||||
'.pv-cand .pv-count.show{display:block}' +
|
||||
'.pv-foot{font:700 14px/1.3 Kanit;color:#9fb0d8}' +
|
||||
'.pv-result{font:900 22px/1.3 Kanit;color:#ffd666;margin:6px 0}' +
|
||||
'.suspect-pick--trial .suspect-pick-title-img{display:none!important}' +
|
||||
'#trial-title{display:none;text-align:center;margin:0 0 4px}' +
|
||||
'#trial-title img{display:block;margin:0 auto;max-width:90vw}' +
|
||||
@@ -4430,6 +4452,41 @@
|
||||
if (el) el.innerHTML = 'ลงติกแล้ว : <b>' + (voted || 0) + '/' + (total || 0) + '</b>';
|
||||
}
|
||||
|
||||
var trialVoteCountdownRaf = 0;
|
||||
function ensureTrialVoteTimerEl() {
|
||||
var ov = document.getElementById('suspect-pick-overlay');
|
||||
var el = document.getElementById('trial-vote-timer');
|
||||
if (!el && ov) { el = document.createElement('div'); el.id = 'trial-vote-timer'; ov.appendChild(el); }
|
||||
return el;
|
||||
}
|
||||
function stopTrialVoteCountdown() {
|
||||
if (trialVoteCountdownRaf) { cancelAnimationFrame(trialVoteCountdownRaf); trialVoteCountdownRaf = 0; }
|
||||
}
|
||||
function startTrialVoteCountdown(endsAt) {
|
||||
stopTrialVoteCountdown();
|
||||
var el = ensureTrialVoteTimerEl();
|
||||
if (!el || !endsAt) { if (el) el.textContent = ''; return; }
|
||||
function tick() {
|
||||
if (trialMode !== 'voting') { stopTrialVoteCountdown(); return; }
|
||||
var ms = endsAt - Date.now();
|
||||
var s = Math.max(0, Math.ceil(ms / 1000));
|
||||
el.textContent = '⏱ ' + s;
|
||||
el.classList.toggle('low', s <= 5);
|
||||
if (ms > 0) trialVoteCountdownRaf = requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
function showTrialCardNotice(text) {
|
||||
var ov = document.getElementById('suspect-pick-overlay');
|
||||
if (!ov) return;
|
||||
var el = document.getElementById('trial-card-notice');
|
||||
if (!el) { el = document.createElement('div'); el.id = 'trial-card-notice'; ov.appendChild(el); }
|
||||
el.textContent = text;
|
||||
el.style.display = 'block';
|
||||
clearTimeout(showTrialCardNotice._t);
|
||||
showTrialCardNotice._t = setTimeout(function () { if (el) el.style.display = 'none'; }, 4200);
|
||||
}
|
||||
|
||||
function ensureTrialRevealBtn() {
|
||||
var ov = document.getElementById('suspect-pick-overlay');
|
||||
var btn = document.getElementById('trial-reveal-btn');
|
||||
@@ -4520,6 +4577,13 @@
|
||||
if (actions) actions.classList.remove('suspect-pick-actions--visible');
|
||||
var hint = document.getElementById('suspect-pick-hint');
|
||||
if (hint) { hint.classList.remove('is-hidden'); hint.textContent = 'คลิกการ์ดที่คิดว่าเป็นคนร้าย'; }
|
||||
/* ตัวจับเวลาโหวต + แจ้งการ์ดพิเศษ (Extension / Bail re-vote) */
|
||||
startTrialVoteCountdown((data && data.voteEndsAt) || 0);
|
||||
if (data && data.revote) {
|
||||
showTrialCardNotice('ใช้การ์ด Bail Coin — จับผิดตัว! โหวตใหม่อีกครั้ง');
|
||||
} else if (data && data.extensionCard) {
|
||||
showTrialCardNotice('ใช้การ์ด Extension — เพิ่มเวลาโหวต +' + ((data.extensionSec) || 10) + ' วินาที');
|
||||
}
|
||||
updateSuspectFloatingOpenBtn();
|
||||
scheduleSuspectPickScale();
|
||||
}
|
||||
@@ -4539,6 +4603,7 @@
|
||||
|
||||
function showTrialResult(data) {
|
||||
trialMode = 'revealed';
|
||||
stopTrialVoteCountdown();
|
||||
var culprit = (data && typeof data.culpritIndex === 'number') ? data.culpritIndex : 0;
|
||||
if (data && Array.isArray(data.counts)) trialVoteCounts = data.counts;
|
||||
var counts = trialVoteCounts || [0, 0, 0];
|
||||
@@ -5222,6 +5287,56 @@
|
||||
});
|
||||
}
|
||||
|
||||
function ensureEvidenceRevealStyle() {
|
||||
if (document.getElementById('evidence-reveal-style')) return;
|
||||
var st = document.createElement('style');
|
||||
st.id = 'evidence-reveal-style';
|
||||
st.textContent =
|
||||
'.ev-card--new{animation:evNewPulse 1.4s ease-out 2;box-shadow:0 0 0 3px #ffd666,0 0 28px rgba(255,214,102,.7)!important;border-radius:10px;}' +
|
||||
'@keyframes evNewPulse{0%{transform:scale(1)}30%{transform:scale(1.06)}100%{transform:scale(1)}}' +
|
||||
'#evidence-reveal-banner{position:absolute;top:14px;left:50%;transform:translateX(-50%);z-index:30;background:linear-gradient(180deg,#1f2747,#141a33);border:1px solid rgba(255,214,102,.7);color:#ffe7b3;font-weight:800;font-size:18px;padding:10px 22px;border-radius:999px;box-shadow:0 10px 30px rgba(0,0,0,.5);pointer-events:none;animation:evBannerIn .4s ease-out;}' +
|
||||
'@keyframes evBannerIn{from{opacity:0;transform:translate(-50%,-12px)}to{opacity:1;transform:translateX(-50%)}}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
|
||||
function showEvidenceRevealBanner(n) {
|
||||
var ov = document.getElementById('lobby-evidence-overlay');
|
||||
if (!ov) return;
|
||||
var old = document.getElementById('evidence-reveal-banner');
|
||||
if (old) old.remove();
|
||||
var b = document.createElement('div');
|
||||
b.id = 'evidence-reveal-banner';
|
||||
b.textContent = 'ได้รับหลักฐานใหม่ +' + Math.max(1, n) + ' ใบ';
|
||||
ov.appendChild(b);
|
||||
setTimeout(function () { if (b && b.parentNode) b.remove(); }, 4200);
|
||||
}
|
||||
|
||||
/** โชว์การ์ดหลักฐานที่เพิ่งได้ตอนกลับถึง LobbyB (เปิดแฟ้ม + ไฮไลต์ใบใหม่) */
|
||||
function showEvidenceRevealOnReturn() {
|
||||
var raw = null;
|
||||
try { raw = sessionStorage.getItem('justiceEvidenceReveal'); } catch (e) { raw = null; }
|
||||
if (!raw) return false;
|
||||
try { sessionStorage.removeItem('justiceEvidenceReveal'); } catch (e) { /* ignore */ }
|
||||
var info = null;
|
||||
try { info = JSON.parse(raw); } catch (e) { return false; }
|
||||
if (!info || typeof info.suspectIndex !== 'number' || info.suspectIndex < 0) return false;
|
||||
ensureEvidenceRevealStyle();
|
||||
setTimeout(function () {
|
||||
openLobbyEvidenceModal();
|
||||
syncLobbyEvidenceTabUi(info.suspectIndex);
|
||||
var root = document.getElementById('lobby-evidence-cards-root');
|
||||
var newN = 1 + (parseInt(info.freeEvidenceCount, 10) || 0);
|
||||
if (root && root.children.length) {
|
||||
var kids = root.children;
|
||||
for (var i = Math.max(0, kids.length - newN); i < kids.length; i++) {
|
||||
kids[i].classList.add('ev-card--new');
|
||||
}
|
||||
}
|
||||
showEvidenceRevealBanner(newN);
|
||||
}, 350);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyDetectiveReturnToLobbyB(data) {
|
||||
quizModeActive = false;
|
||||
quizPhaseLocal = null;
|
||||
@@ -5240,14 +5355,16 @@
|
||||
lobbyLevel: (window.__detectiveLobbyMeta && window.__detectiveLobbyMeta.level) || null,
|
||||
caseId: (window.__detectiveLobbyMeta && window.__detectiveLobbyMeta.caseId) || null,
|
||||
});
|
||||
const revealedEvidence = showEvidenceRevealOnReturn();
|
||||
if (reopenSuspect) {
|
||||
serverSuspectPhaseActive = true;
|
||||
/* ถ้ามีการโชว์การ์ดหลักฐานใหม่ ให้เลื่อนเปิดหน้าเลือกผู้ต้องสงสัยช้าลง (ผู้เล่นได้ดูการ์ดก่อน) */
|
||||
setTimeout(function () {
|
||||
openSuspectOverlay(pickIdx);
|
||||
updateSuspectFloatingOpenBtn();
|
||||
syncLobbyBUiChrome();
|
||||
updatePlayersHud();
|
||||
}, 500);
|
||||
}, revealedEvidence ? 1600 : 500);
|
||||
}
|
||||
syncLobbyBUiChrome();
|
||||
updatePlayersHud();
|
||||
@@ -5511,6 +5628,131 @@
|
||||
showTrialResult(data || {});
|
||||
});
|
||||
|
||||
/* ===== โหวตเลือกผู้เล่น (Silence / Ban) ===== */
|
||||
var pickVoteState = { open: false, purpose: '', picked: null, raf: 0 };
|
||||
|
||||
function pickVoteMeta(purpose) {
|
||||
if (purpose === 'ban') {
|
||||
return { badge: 'BAN CARD', title: 'โหวตห้ามเล่นมินิเกม', sub: 'เลือกผู้เล่น 1 คนที่จะถูกห้ามเล่นมินิเกมรอบนี้ (โหวตตัวเองได้ · เสมอ = สุ่ม)' };
|
||||
}
|
||||
return { badge: 'SILENCE CARD', title: 'โหวตปิดปาก', sub: 'เลือกผู้เล่น 1 คนที่จะโหวตชี้ตัวคนร้ายไม่ได้ในรอบนี้ (โหวตตัวเองได้ · เสมอ = สุ่ม)' };
|
||||
}
|
||||
|
||||
function ensurePickVoteOverlay() {
|
||||
var ov = document.getElementById('pick-vote-overlay');
|
||||
if (ov) return ov;
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'pick-vote-overlay';
|
||||
ov.innerHTML =
|
||||
'<div class="pv-shell">' +
|
||||
'<span class="pv-badge" id="pv-badge">CARD</span>' +
|
||||
'<h2 class="pv-title" id="pv-title"></h2>' +
|
||||
'<p class="pv-sub" id="pv-sub"></p>' +
|
||||
'<div class="pv-timer" id="pv-timer"></div>' +
|
||||
'<div class="pv-list" id="pv-list"></div>' +
|
||||
'<div class="pv-result" id="pv-result" style="display:none"></div>' +
|
||||
'<div class="pv-foot" id="pv-foot"></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
return ov;
|
||||
}
|
||||
|
||||
function stopPickVoteCountdown() {
|
||||
if (pickVoteState.raf) { cancelAnimationFrame(pickVoteState.raf); pickVoteState.raf = 0; }
|
||||
}
|
||||
|
||||
function startPickVoteCountdown(endsAt) {
|
||||
stopPickVoteCountdown();
|
||||
var el = document.getElementById('pv-timer');
|
||||
if (!el || !endsAt) { if (el) el.textContent = ''; return; }
|
||||
function tick() {
|
||||
if (!pickVoteState.open) { stopPickVoteCountdown(); return; }
|
||||
var ms = endsAt - Date.now();
|
||||
var s = Math.max(0, Math.ceil(ms / 1000));
|
||||
el.textContent = '⏱ ' + s;
|
||||
el.classList.toggle('low', s <= 5);
|
||||
if (ms > 0) pickVoteState.raf = requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
|
||||
function castPickVote(targetId) {
|
||||
if (!pickVoteState.open) return;
|
||||
pickVoteState.picked = targetId;
|
||||
var list = document.getElementById('pv-list');
|
||||
if (list) {
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pv-cand'), function (b) {
|
||||
b.classList.toggle('is-picked', b.getAttribute('data-id') === targetId);
|
||||
});
|
||||
}
|
||||
socket.emit('player-pick-vote', { targetId: targetId }, function (res) {
|
||||
if (!(res && res.ok)) appendLobbySystemChat('— โหวตไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('player-pick-vote-open', (data) => {
|
||||
var ov = ensurePickVoteOverlay();
|
||||
var meta = pickVoteMeta(data && data.purpose);
|
||||
pickVoteState.open = true;
|
||||
pickVoteState.purpose = (data && data.purpose) || '';
|
||||
pickVoteState.picked = null;
|
||||
document.getElementById('pv-badge').textContent = meta.badge;
|
||||
document.getElementById('pv-title').textContent = meta.title;
|
||||
document.getElementById('pv-sub').textContent = meta.sub;
|
||||
var resEl = document.getElementById('pv-result');
|
||||
if (resEl) { resEl.style.display = 'none'; resEl.textContent = ''; }
|
||||
document.getElementById('pv-foot').textContent = '';
|
||||
var list = document.getElementById('pv-list');
|
||||
list.innerHTML = '';
|
||||
var cands = (data && Array.isArray(data.candidates)) ? data.candidates : [];
|
||||
cands.forEach(function (c) {
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'pv-cand';
|
||||
b.setAttribute('data-id', c.id);
|
||||
var tag = (c.id === socket.id) ? '<span class="pv-me">(คุณ)</span>' : (c.isBot ? '<span class="pv-bot">บอท</span>' : '');
|
||||
b.innerHTML = '<span>' + (c.nickname || '?') + '</span>' + tag + '<span class="pv-count" data-id="' + c.id + '"></span>';
|
||||
b.addEventListener('click', function () { castPickVote(c.id); });
|
||||
list.appendChild(b);
|
||||
});
|
||||
ov.classList.add('is-open');
|
||||
startPickVoteCountdown(data && data.endsAt);
|
||||
});
|
||||
|
||||
socket.on('player-pick-vote-update', (data) => {
|
||||
if (!pickVoteState.open) return;
|
||||
var counts = (data && data.counts) || {};
|
||||
var list = document.getElementById('pv-list');
|
||||
if (list) {
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pv-count'), function (c) {
|
||||
var n = counts[c.getAttribute('data-id')] || 0;
|
||||
c.textContent = n;
|
||||
c.classList.toggle('show', n > 0);
|
||||
});
|
||||
}
|
||||
var foot = document.getElementById('pv-foot');
|
||||
if (foot) foot.textContent = 'โหวตแล้ว ' + ((data && data.voted) || 0) + '/' + ((data && data.total) || 0);
|
||||
});
|
||||
|
||||
socket.on('player-pick-vote-result', (data) => {
|
||||
stopPickVoteCountdown();
|
||||
pickVoteState.open = false;
|
||||
var resEl = document.getElementById('pv-result');
|
||||
var name = (data && data.targetName) ? data.targetName : '—';
|
||||
var word = (data && data.purpose === 'ban') ? 'ถูกห้ามเล่นมินิเกมรอบนี้' : 'ถูกปิดปาก (โหวตไม่ได้)';
|
||||
if (resEl) {
|
||||
resEl.style.display = 'block';
|
||||
resEl.textContent = '🎯 ' + name + ' ' + word;
|
||||
}
|
||||
var foot = document.getElementById('pv-foot');
|
||||
if (foot) foot.textContent = '';
|
||||
var amTarget = data && data.targetId === socket.id;
|
||||
setTimeout(function () {
|
||||
var ov = document.getElementById('pick-vote-overlay');
|
||||
if (ov) ov.classList.remove('is-open');
|
||||
}, amTarget ? 2600 : 1900);
|
||||
});
|
||||
|
||||
socket.on('suspect-investigation-start', (data) => {
|
||||
serverSuspectPhaseActive = false;
|
||||
closeSuspectOverlay();
|
||||
@@ -5860,6 +6102,12 @@
|
||||
sessionStorage.setItem('justicePlayLobbyAvatar:' + cidPlay, avEl.src);
|
||||
}
|
||||
} catch (eAv) { /* ignore */ }
|
||||
/* Card 3 Ban — ผู้ที่ถูกแบนรอบนี้ไม่เข้ามินิเกม (รออยู่ LobbyB) */
|
||||
if (data && data.bannedPlayerId && data.bannedPlayerId === socket.id) {
|
||||
appendLobbySystemChat('— คุณถูกแบนรอบนี้ (Ban Card) · รอเพื่อนเล่นมินิเกมจบ');
|
||||
showBannedNotice();
|
||||
return;
|
||||
}
|
||||
let q = 'play.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(getProfileDisplayName());
|
||||
if (mid) q += '&map=' + encodeURIComponent(mid);
|
||||
if (lobbyLevelStr) q += '&lobbyLevel=' + encodeURIComponent(lobbyLevelStr);
|
||||
@@ -5868,6 +6116,24 @@
|
||||
location.href = q;
|
||||
});
|
||||
|
||||
function showBannedNotice() {
|
||||
var ov = document.getElementById('banned-notice-overlay');
|
||||
if (!ov) {
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'banned-notice-overlay';
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:118;background:rgba(6,9,20,.82);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)';
|
||||
ov.innerHTML = '<div style="text-align:center;background:linear-gradient(180deg,#2a1430,#160a1d);border:1px solid rgba(247,118,142,.55);border-radius:18px;padding:26px 34px;box-shadow:0 24px 60px rgba(0,0,0,.6)">' +
|
||||
'<div style="font:900 15px/1 Kanit;letter-spacing:1px;color:#f7768e">BAN CARD</div>' +
|
||||
'<div style="font:900 28px/1.3 Kanit;color:#fff;margin:10px 0 6px">คุณถูกแบนรอบนี้</div>' +
|
||||
'<div style="font:600 15px/1.4 Kanit;color:#e6b8c4">รอเพื่อนเล่นมินิเกมจบ แล้วกลับมา LobbyB ด้วยกัน</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
}
|
||||
ov.style.display = 'flex';
|
||||
clearTimeout(showBannedNotice._t);
|
||||
showBannedNotice._t = setTimeout(function () { if (ov) ov.style.display = 'none'; }, 4200);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (moveCodes.includes(e.code) && !isChatFocused()) {
|
||||
if (suspectPickOverlayOpen) { e.preventDefault(); return; }
|
||||
|
||||
@@ -3916,6 +3916,21 @@
|
||||
#special-quiz-overlay .sq-ov-member--wrong .sq-ov-member-dot { background: #f7768e; color: #0e1226; }
|
||||
#special-quiz-overlay .sq-ov-member--wrong .sq-ov-member-ans { color: #f7768e; }
|
||||
#special-quiz-overlay .sq-ov-member--me { box-shadow: inset 0 0 0 1px rgba(224,175,104,0.6); }
|
||||
/* ใช้การ์ดพิเศษ — animation โชว์การ์ดบินเข้ากลางจอ */
|
||||
#special-card-use-overlay { position: fixed; inset: 0; z-index: 1300; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
||||
#special-card-use-overlay.is-hidden { display: none; }
|
||||
#special-card-use-overlay .scu-card { display: flex; flex-direction: column; align-items: center; gap: 10px; opacity: 0; transform: scale(0.5) translateY(40px); }
|
||||
#special-card-use-overlay.scu-play .scu-card { animation: scuPop 2.6s cubic-bezier(.18,.9,.28,1.2) forwards; }
|
||||
#special-card-use-overlay .scu-img { width: 168px; height: auto; border-radius: 12px; filter: drop-shadow(0 18px 40px rgba(0,0,0,0.6)) drop-shadow(0 0 22px rgba(224,175,104,0.55)); }
|
||||
#special-card-use-overlay .scu-name { font-size: 22px; font-weight: 800; color: #ffe7b3; text-shadow: 0 2px 10px rgba(0,0,0,0.7); letter-spacing: .02em; }
|
||||
#special-card-use-overlay .scu-effect { font-size: 26px; font-weight: 900; color: #9ece6a; text-shadow: 0 2px 12px rgba(0,0,0,0.7); }
|
||||
@keyframes scuPop {
|
||||
0% { opacity: 0; transform: scale(0.4) translateY(60px) rotate(-8deg); }
|
||||
14% { opacity: 1; transform: scale(1.12) translateY(0) rotate(2deg); }
|
||||
24% { transform: scale(1) translateY(0) rotate(0); }
|
||||
80% { opacity: 1; transform: scale(1) translateY(0); }
|
||||
100% { opacity: 0; transform: scale(0.94) translateY(-26px); }
|
||||
}
|
||||
</style>
|
||||
<div id="special-quiz-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="sq-overlay-title">
|
||||
<div class="sq-ov-backdrop" aria-hidden="true"></div>
|
||||
@@ -3939,7 +3954,7 @@
|
||||
<script src="/app-base.js?v=2"></script>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.0504"></script>
|
||||
<script src="js/play.js?v=0.0506"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1613,7 +1613,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.0255"></script>
|
||||
<script src="js/room-lobby.js?v=0.0258"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+365
-27
@@ -508,6 +508,9 @@ function maybeSpawnSpecialQuizForRun(space) {
|
||||
clearSpecialQuizTimers(space);
|
||||
space.specialQuiz = null;
|
||||
space.specialCardAwardedThisRun = null;
|
||||
space.timeDilationPendingSec = 0;
|
||||
/* หมายเหตุ: ไม่ล้าง pendingSpecialCard ที่นี่ — การ์ดที่ใช้ใน trial/lobby ต้องคงอยู่จนกว่าจะถูกใช้
|
||||
หรือถูกแทนด้วยการ์ดใบใหม่ (endSpecialQuizSession เขียนทับเมื่อได้ใบใหม่) */
|
||||
if (!space || !space.detectiveMinigameActive) return;
|
||||
const mapId = String(space.mapId || '').trim();
|
||||
if (SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.indexOf(mapId) < 0) return;
|
||||
@@ -683,6 +686,81 @@ function resolveSpecialQuizQuestion(sid, space, fromTimeout) {
|
||||
space.specialQuizTimers.push(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* นิยามการ์ดพิเศษ 7 ใบ — when = ช่วงที่การ์ดทำงาน:
|
||||
* minigame = ใช้ทันทีในมินิเกมที่เพิ่งเล่น (Time Dilation)
|
||||
* now = ใช้ทันทีตอนได้รับ (Fund coins)
|
||||
* after_game = ตอนจบมินิเกมก่อนกลับ LobbyB (Free Evidence)
|
||||
* pre_trial = ก่อนโหวตพิจารณาคดี (Silence)
|
||||
* trial_vote = ระหว่างโหวตพิจารณาคดี (Extension)
|
||||
* trial_revote= หลังเฉลยถ้าจับผิดตัว (Bail Coin)
|
||||
* lobby_next = ใน LobbyB ก่อนเลือกมินิเกมรอบหน้า (Ban)
|
||||
*/
|
||||
const SPECIAL_CARD_DEFS = {
|
||||
1: { key: 'time_dilation', when: 'minigame', addSec: 10, th: 'เพิ่มเวลาเล่นมินิเกม +10 วินาที' },
|
||||
2: { key: 'extension', when: 'trial_vote', addSec: 10, th: 'เพิ่มเวลาโหวตพิจารณาคดี +10 วินาที' },
|
||||
3: { key: 'ban', when: 'lobby_next', th: 'โหวตแบนผู้เล่น 1 คน ไม่ให้เล่นมินิเกมรอบหน้า' },
|
||||
4: { key: 'silence', when: 'pre_trial', th: 'โหวตปิดปากผู้เล่น 1 คน ห้ามโหวตในพิจารณาคดี' },
|
||||
5: { key: 'bail', when: 'trial_revote', th: 'จับผิดตัว ให้โหวตใหม่ได้ 1 ครั้ง' },
|
||||
6: { key: 'fund', when: 'now', coins: 10, th: 'ทุกคนรับ +10 COINS' },
|
||||
7: { key: 'free_evidence', when: 'after_game', evidence: 2, th: 'ทุกคนรับหลักฐานฟรี +2 ใบ' },
|
||||
};
|
||||
|
||||
function specialCardDef(cardId) {
|
||||
return SPECIAL_CARD_DEFS[Number(cardId)] || null;
|
||||
}
|
||||
|
||||
/** payload การ์ดสำหรับ client (รวม metadata การใช้งาน) */
|
||||
function specialCardClientPayload(card) {
|
||||
if (!card) return null;
|
||||
const def = specialCardDef(card.cardId) || {};
|
||||
return {
|
||||
cardId: card.cardId || null,
|
||||
th: card.th || def.th || '',
|
||||
en: card.en || '',
|
||||
rarity: card.rarity || 'common',
|
||||
imageUrl: card.imageUrl || (card.cardId ? specialCardImageUrl(card.cardId) : ''),
|
||||
txtImageUrl: card.txtImageUrl || (card.cardId ? specialCardTxtImageUrl(card.cardId) : ''),
|
||||
effectKey: def.key || '',
|
||||
effectWhen: def.when || '',
|
||||
addSec: def.addSec || 0,
|
||||
desc: def.th || '',
|
||||
};
|
||||
}
|
||||
|
||||
/** ใช้การ์ดที่ทำงาน "ทันที" (minigame / now) — ส่วนที่เหลือเข้าคิวรอ flow ที่ถูก */
|
||||
function applySpecialCardImmediate(sid, space, card) {
|
||||
const def = specialCardDef(card && card.cardId);
|
||||
if (!def) return;
|
||||
if (def.when === 'minigame' && def.key === 'time_dilation') {
|
||||
/* +10 วิให้รอบ quiz (mng8a80o) ตอน resume — มินิเกมอื่นฝั่ง client ขยายเอง */
|
||||
space.timeDilationPendingSec = (space.timeDilationPendingSec || 0) + (def.addSec || 10);
|
||||
io.to(sid).emit('special-card-applied', {
|
||||
card: specialCardClientPayload(card),
|
||||
addSec: def.addSec || 10,
|
||||
});
|
||||
consumePendingSpecialCard(space, card.cardId);
|
||||
return;
|
||||
}
|
||||
if (def.when === 'now' && def.key === 'fund') {
|
||||
/* +COINS — แต่ละ client บวกเหรียญของตัวเองผ่าน PHP (server ไม่รู้ playerKey) */
|
||||
io.to(sid).emit('special-card-applied', {
|
||||
card: specialCardClientPayload(card),
|
||||
coins: def.coins || 10,
|
||||
});
|
||||
consumePendingSpecialCard(space, card.cardId);
|
||||
return;
|
||||
}
|
||||
/* การ์ดที่เหลือ (after_game / pre_trial / trial_vote / trial_revote / lobby_next)
|
||||
— คงไว้ใน pendingSpecialCard เพื่อใช้ภายหลัง */
|
||||
}
|
||||
|
||||
function consumePendingSpecialCard(space, cardId) {
|
||||
if (space.pendingSpecialCard && space.pendingSpecialCard.cardId === Number(cardId)) {
|
||||
space.pendingSpecialCard.consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
function endSpecialQuizSession(sid, space, success) {
|
||||
const sq = space.specialQuiz;
|
||||
if (!sq) return;
|
||||
@@ -692,7 +770,9 @@ function endSpecialQuizSession(sid, space, success) {
|
||||
let cardOut = null;
|
||||
if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) {
|
||||
space.specialCardAwardedThisRun = sq.card;
|
||||
cardOut = sq.card;
|
||||
cardOut = specialCardClientPayload(sq.card);
|
||||
space.pendingSpecialCard = { cardId: Number(sq.card.cardId) || null, card: sq.card, consumed: false };
|
||||
applySpecialCardImmediate(sid, space, sq.card);
|
||||
}
|
||||
io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut });
|
||||
resumeQuizAfterSpecialQuiz(sid, space);
|
||||
@@ -707,8 +787,8 @@ function startSpecialQuizSession(sid, space) {
|
||||
.map(specialQuizNormalizeQuestion)
|
||||
.filter(Boolean);
|
||||
if (!all.length) return false;
|
||||
const cap = clampQuizRoundQuestionCount(settings.quizRoundQuestionCount, 10);
|
||||
const picked = specialQuizShuffle(all).slice(0, Math.max(1, Math.min(cap, all.length)));
|
||||
/* ควิซพิเศษ = สุ่มถามแค่ 1 ข้อ (รับการ์ดพิเศษ) */
|
||||
const picked = specialQuizShuffle(all).slice(0, 1);
|
||||
sq.triggered = true;
|
||||
sq.session = {
|
||||
questions: picked,
|
||||
@@ -2155,7 +2235,13 @@ function resumeQuizAfterSpecialQuiz(sid, space) {
|
||||
if (!sess || !sess.active) return;
|
||||
const md = sess.quizMapMd || getLobbyLayoutMapForSpace(space);
|
||||
if (!md) return;
|
||||
sess.phaseEndsAt = Date.now() + remaining;
|
||||
/* Time Dilation: +N วิให้รอบ quiz (mng8a80o) ตอน resume */
|
||||
let extraMs = 0;
|
||||
if (space.timeDilationPendingSec > 0) {
|
||||
extraMs = space.timeDilationPendingSec * 1000;
|
||||
space.timeDilationPendingSec = 0;
|
||||
}
|
||||
sess.phaseEndsAt = Date.now() + remaining + extraMs;
|
||||
if (phase === 'read') {
|
||||
sess.phase = 'read';
|
||||
const q = sess.questions[sess.qIndex];
|
||||
@@ -2389,10 +2475,26 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
}
|
||||
const ids = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()];
|
||||
const awarded = awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, ids);
|
||||
/* Card 7 Free Evidence — ทุกคนรับหลักฐานเพิ่ม (after_game) */
|
||||
let freeEvidenceCount = 0;
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (pend && Number(pend.cardId) === 7 && !pend.consumed) {
|
||||
const def = specialCardDef(7);
|
||||
freeEvidenceCount = (def && def.evidence) || 2;
|
||||
for (let n = 0; n < freeEvidenceCount; n++) {
|
||||
ids.forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx);
|
||||
if (extra != null && awarded[pid] == null) awarded[pid] = extra;
|
||||
});
|
||||
}
|
||||
pend.consumed = true;
|
||||
}
|
||||
space.detectiveAwardDoneForRun = true;
|
||||
space.lastDetectiveInvestigatedSuspect = suspectIdx;
|
||||
space.lastEvidenceGrant = { suspectIdx, awarded, freeEvidenceCount };
|
||||
emitLobbyEvidenceSync(sid, space);
|
||||
return { suspectIdx, awarded, alreadyGranted: false };
|
||||
return { suspectIdx, awarded, alreadyGranted: false, freeEvidenceCount };
|
||||
}
|
||||
|
||||
function emitLobbyEvidenceSync(sid, space) {
|
||||
@@ -2432,6 +2534,9 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
space.suspectPhaseActive = false;
|
||||
space.detectiveMinigameActive = true;
|
||||
space.detectiveAwardDoneForRun = false;
|
||||
/* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */
|
||||
space.bannedThisRunPlayerId = space.bannedPlayerId || null;
|
||||
space.bannedPlayerId = null;
|
||||
space.detectiveMinigameCardIndex = cardEntry.cardIndex;
|
||||
// จำว่ากำลังสืบผู้ต้องสงสัยคนไหน เพื่อเติมหลักฐาน (ติกถูก) ตอนเล่นจบ
|
||||
space.suspectActiveIndex = (typeof selectedIndex === 'number' && selectedIndex >= 0 && selectedIndex <= 2)
|
||||
@@ -2486,6 +2591,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
peersSnap,
|
||||
detectiveMinigame: true,
|
||||
minigameLabel: cardEntry.labelTh,
|
||||
bannedPlayerId: space.bannedThisRunPlayerId || null,
|
||||
});
|
||||
setTimeout(() => {
|
||||
const spNow = spaces.get(sid);
|
||||
@@ -2517,6 +2623,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
detectiveMinigame: true,
|
||||
detectiveReturnLobbyB: true,
|
||||
minigameLabel: cardEntry.labelTh,
|
||||
bannedPlayerId: space.bannedThisRunPlayerId || null,
|
||||
};
|
||||
if (md.gameType === 'gauntlet' && space.gauntletRun) {
|
||||
gameStartPayload.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
|
||||
@@ -2550,13 +2657,18 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
|
||||
} else if (awardOptions && Array.isArray(awardOptions.playerIds) && awardOptions.playerIds.length) {
|
||||
awardIds = awardOptions.playerIds.filter((id) => space.peers.has(id));
|
||||
}
|
||||
/* Card 3 Ban — ผู้ที่ถูกแบนรอบนี้ไม่ได้เล่น จึงไม่ได้รับหลักฐาน */
|
||||
if (space.bannedThisRunPlayerId) {
|
||||
awardIds = awardIds.filter((id) => id !== space.bannedThisRunPlayerId);
|
||||
}
|
||||
/* ไม่มอบการ์ดเมื่อกลับ LobbyB โดยไม่ระบุ (เช่น refresh room-lobby ระหว่างมินิเกม) */
|
||||
if (!space.detectiveAwardDoneForRun && awardIds.length) {
|
||||
awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, awardIds);
|
||||
space.detectiveAwardDoneForRun = true;
|
||||
/* ผ่าน grant…CurrentRun เพื่อให้ set lastEvidenceGrant + ใช้ Card 7 Free Evidence + sync แฟ้ม (สำหรับหน้าเปิดเผยหลักฐาน) */
|
||||
grantDetectiveEvidenceForCurrentRun(sid, space, awardIds);
|
||||
}
|
||||
}
|
||||
space.suspectActiveIndex = null;
|
||||
space.bannedThisRunPlayerId = null;
|
||||
const lb = maps.get(POST_CASE_LOBBY_SPACE_ID);
|
||||
if (!lb) {
|
||||
io.to(sid).emit('quiz-ended', { message: message || 'จบมินิเกม — ไม่พบ LobbyB', returnToLobbyB: true });
|
||||
@@ -2597,17 +2709,34 @@ function emitTrialVoteUpdate(sid, space) {
|
||||
io.to(sid).emit('trial-vote-update', {
|
||||
counts,
|
||||
voted: Object.keys(votes).length,
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
totalPlayers: trialEligibleVoterTotal(space),
|
||||
});
|
||||
}
|
||||
|
||||
function computeAndEmitTrialResult(sid, space) {
|
||||
space.trialPhase = 'revealed';
|
||||
/* กันบอทบางตัวยังไม่โหวต — โหวตให้หมดก่อนรวมผล */
|
||||
autoVoteBotsTrial(space);
|
||||
clearTrialVoteTimer(space);
|
||||
const counts = [0, 0, 0];
|
||||
const votes = space.trialVotes || {};
|
||||
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : 0;
|
||||
/* Card 5 Bail Coin — จับผิดตัวให้โหวตใหม่ได้ 1 ครั้ง */
|
||||
{
|
||||
const c2 = [0, 0, 0];
|
||||
Object.keys(votes).forEach((id) => { const v = votes[id]; if (v >= 0 && v <= 2) c2[v]++; });
|
||||
let mostVoted = 0; for (let k = 1; k <= 2; k++) { if (c2[k] > c2[mostVoted]) mostVoted = k; }
|
||||
const anyVotes = (c2[0] + c2[1] + c2[2]) > 0;
|
||||
const wrong = anyVotes && mostVoted !== culprit;
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (wrong && pend && Number(pend.cardId) === 5 && !pend.consumed && !space.trialBailUsed) {
|
||||
pend.consumed = true;
|
||||
space.trialBailUsed = true;
|
||||
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'bail' });
|
||||
openTrialVotingPhase(sid, space, { revote: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
space.trialPhase = 'revealed';
|
||||
const winners = [];
|
||||
if (!space.trialScores) space.trialScores = {};
|
||||
Object.keys(votes).forEach((id) => {
|
||||
@@ -2634,6 +2763,7 @@ function computeAndEmitTrialResult(sid, space) {
|
||||
winnerNames,
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
});
|
||||
space.silencedPlayerId = null;
|
||||
}
|
||||
|
||||
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (ปากคำ 3 รอบ ก่อนโหวต) ===== */
|
||||
@@ -2749,10 +2879,168 @@ function doTestimonyReveal(sid, space) {
|
||||
}
|
||||
|
||||
/** สุ่มโหวตชี้คนร้ายให้บอท — สมจริงๆ ก็คือเลือกใครก็ได้ใน 3 คน (มี bias ให้ตัวร้ายจริง 40% เพื่อให้สนุก) */
|
||||
/** เวลาโหวตพิจารณาคดี (มิลลิวิ) + เวลาที่การ์ด Extension เพิ่มให้ */
|
||||
const TRIAL_VOTE_MS = 30000;
|
||||
const TRIAL_VOTE_EXTENSION_MS = 10000;
|
||||
|
||||
function clearTrialVoteTimer(space) {
|
||||
if (space.trialVoteTimer) { clearTimeout(space.trialVoteTimer); space.trialVoteTimer = null; }
|
||||
}
|
||||
|
||||
/** เปิดเฟสโหวต — ตั้งตัวจับเวลา + ส่ง trial-open (ใช้ทั้งโหวตครั้งแรกและโหวตใหม่จาก Bail) */
|
||||
function openTrialVotingPhase(sid, space, opts) {
|
||||
opts = opts || {};
|
||||
space.testimonyActive = false;
|
||||
space.trialPhase = 'voting';
|
||||
space.trialVotes = {};
|
||||
autoVoteBotsTrial(space);
|
||||
clearTrialVoteTimer(space);
|
||||
/* Card 2 Extension — เพิ่มเวลาโหวต +10 วิ (ใช้ครั้งเดียว) */
|
||||
let extraMs = 0;
|
||||
let extensionCard = false;
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (pend && Number(pend.cardId) === 2 && !pend.consumed) {
|
||||
extraMs = TRIAL_VOTE_EXTENSION_MS;
|
||||
extensionCard = true;
|
||||
pend.consumed = true;
|
||||
}
|
||||
const durMs = TRIAL_VOTE_MS + extraMs;
|
||||
space.trialVoteEndsAt = Date.now() + durMs;
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const sock = io.sockets.sockets.get(pid);
|
||||
if (!sock) return;
|
||||
sock.emit('trial-open', {
|
||||
hostId: space.hostId,
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
myPlayerEvidence: clonePlayerEvidence(space, pid),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
|
||||
voteEndsAt: space.trialVoteEndsAt,
|
||||
voteDurationMs: durMs,
|
||||
extensionCard,
|
||||
extensionSec: extensionCard ? (TRIAL_VOTE_EXTENSION_MS / 1000) : 0,
|
||||
revote: !!opts.revote,
|
||||
});
|
||||
});
|
||||
if (extensionCard) {
|
||||
io.to(sid).emit('special-card-applied', {
|
||||
card: specialCardClientPayload(pend.card),
|
||||
addSec: TRIAL_VOTE_EXTENSION_MS / 1000,
|
||||
context: 'trial',
|
||||
});
|
||||
}
|
||||
emitTrialVoteUpdate(sid, space);
|
||||
space.trialVoteTimer = setTimeout(() => {
|
||||
const spNow = spaces.get(sid);
|
||||
if (!spNow || spNow.trialPhase !== 'voting') return;
|
||||
computeAndEmitTrialResult(sid, spNow);
|
||||
}, durMs + 120);
|
||||
}
|
||||
|
||||
/* ===== โหวตเลือกผู้เล่น 1 คน (ใช้ร่วม: Silence ปิดปาก / Ban ห้ามเล่น) ===== */
|
||||
const PICK_VOTE_MS = 15000;
|
||||
|
||||
function clearPickVoteTimer(space) {
|
||||
if (space.pickVoteTimer) { clearTimeout(space.pickVoteTimer); space.pickVoteTimer = null; }
|
||||
}
|
||||
|
||||
function pickVoteCandidates(space) {
|
||||
const arr = [];
|
||||
space.peers.forEach((p, id) => arr.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isBot: false }));
|
||||
testimonyBotIds(space).forEach((bid, i) => arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isBot: true }));
|
||||
return arr;
|
||||
}
|
||||
|
||||
function autoVotePickBots(space) {
|
||||
const pv = space.pickVote;
|
||||
if (!pv) return;
|
||||
testimonyBotIds(space).forEach((bid) => {
|
||||
if (pv.votes[bid] != null) return;
|
||||
const choices = pv.candidates.filter((c) => c.id !== bid);
|
||||
if (!choices.length) return;
|
||||
pv.votes[bid] = choices[Math.floor(Math.random() * choices.length)].id;
|
||||
});
|
||||
}
|
||||
|
||||
function emitPickVoteUpdate(sid, space) {
|
||||
const pv = space.pickVote;
|
||||
if (!pv) return;
|
||||
const counts = {};
|
||||
Object.keys(pv.votes).forEach((vid) => { const t = pv.votes[vid]; counts[t] = (counts[t] || 0) + 1; });
|
||||
io.to(sid).emit('player-pick-vote-update', {
|
||||
voted: Object.keys(pv.votes).length,
|
||||
total: pv.candidates.length,
|
||||
counts,
|
||||
});
|
||||
}
|
||||
|
||||
function startPlayerPickVote(sid, space, purpose, onResolve) {
|
||||
clearPickVoteTimer(space);
|
||||
const candidates = pickVoteCandidates(space);
|
||||
space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PICK_VOTE_MS, resolved: false, onResolve };
|
||||
autoVotePickBots(space);
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const sock = io.sockets.sockets.get(pid);
|
||||
if (!sock) return;
|
||||
sock.emit('player-pick-vote-open', {
|
||||
purpose,
|
||||
candidates: candidates.map((c) => ({ id: c.id, nickname: c.nickname, isBot: c.isBot })),
|
||||
endsAt: space.pickVote.endsAt,
|
||||
durationMs: PICK_VOTE_MS,
|
||||
});
|
||||
});
|
||||
emitPickVoteUpdate(sid, space);
|
||||
space.pickVoteTimer = setTimeout(() => {
|
||||
const sp = spaces.get(sid);
|
||||
if (!sp || !sp.pickVote || sp.pickVote.resolved) return;
|
||||
resolvePlayerPickVote(sid, sp);
|
||||
}, PICK_VOTE_MS + 120);
|
||||
maybeResolvePickVote(sid, space);
|
||||
}
|
||||
|
||||
function maybeResolvePickVote(sid, space) {
|
||||
const pv = space.pickVote;
|
||||
if (!pv || pv.resolved) return;
|
||||
const ids = pv.candidates.map((c) => c.id);
|
||||
if (ids.length > 0 && ids.every((id) => pv.votes[id] != null)) resolvePlayerPickVote(sid, space);
|
||||
}
|
||||
|
||||
function resolvePlayerPickVote(sid, space) {
|
||||
const pv = space.pickVote;
|
||||
if (!pv || pv.resolved) return;
|
||||
pv.resolved = true;
|
||||
clearPickVoteTimer(space);
|
||||
const counts = {};
|
||||
Object.keys(pv.votes).forEach((vid) => { const t = pv.votes[vid]; counts[t] = (counts[t] || 0) + 1; });
|
||||
let max = -1;
|
||||
Object.keys(counts).forEach((tid) => { if (counts[tid] > max) max = counts[tid]; });
|
||||
const maxIds = Object.keys(counts).filter((tid) => counts[tid] === max);
|
||||
const target = maxIds.length ? maxIds[Math.floor(Math.random() * maxIds.length)] : null;
|
||||
const tc = pv.candidates.find((c) => c.id === target);
|
||||
const targetName = tc ? tc.nickname : '';
|
||||
io.to(sid).emit('player-pick-vote-result', { purpose: pv.purpose, targetId: target, targetName, counts });
|
||||
const cb = pv.onResolve;
|
||||
space.pickVote = null;
|
||||
if (typeof cb === 'function') cb(target, targetName);
|
||||
}
|
||||
|
||||
function trialSilencedIsParticipant(space) {
|
||||
const s = space.silencedPlayerId;
|
||||
if (!s) return false;
|
||||
if (space.peers.has(s)) return true;
|
||||
return (typeof s === 'string' && s.indexOf(TESTIMONY_BOT_PREFIX) === 0);
|
||||
}
|
||||
|
||||
function trialEligibleVoterTotal(space) {
|
||||
let t = testimonyTotalParticipants(space);
|
||||
if (trialSilencedIsParticipant(space)) t = Math.max(0, t - 1);
|
||||
return t;
|
||||
}
|
||||
|
||||
function autoVoteBotsTrial(space) {
|
||||
if (!space.trialVotes) space.trialVotes = {};
|
||||
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : Math.floor(Math.random() * 3);
|
||||
testimonyBotIds(space).forEach((bid) => {
|
||||
if (bid === space.silencedPlayerId) return; /* บอทที่ถูกปิดปากไม่โหวต */
|
||||
if (space.trialVotes[bid] != null) return;
|
||||
/* 40% โหวตถูก, 60% โหวตสุ่ม */
|
||||
let pick;
|
||||
@@ -2768,22 +3056,22 @@ function autoVoteBotsTrial(space) {
|
||||
function advanceTestimony(sid, space) {
|
||||
space.testimonyRound = (typeof space.testimonyRound === 'number' ? space.testimonyRound : 0) + 1;
|
||||
if (space.testimonyRound >= 3) {
|
||||
space.testimonyActive = false;
|
||||
space.trialPhase = 'voting';
|
||||
space.trialVotes = {};
|
||||
/* บอทโหวตทันทีเมื่อเข้าหน้าโหวต */
|
||||
autoVoteBotsTrial(space);
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const sock = io.sockets.sockets.get(pid);
|
||||
if (!sock) return;
|
||||
sock.emit('trial-open', {
|
||||
hostId: space.hostId,
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
myPlayerEvidence: clonePlayerEvidence(space, pid),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
|
||||
space.trialBailUsed = false;
|
||||
space.silencedPlayerId = null;
|
||||
/* Card 4 Silence — โหวตเลือกผู้เล่น 1 คนให้ห้ามโหวต ก่อนเข้าโหวตจริง */
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (pend && Number(pend.cardId) === 4 && !pend.consumed) {
|
||||
pend.consumed = true;
|
||||
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'silence' });
|
||||
startPlayerPickVote(sid, space, 'silence', (targetId) => {
|
||||
const sp = spaces.get(sid);
|
||||
if (!sp) return;
|
||||
sp.silencedPlayerId = targetId || null;
|
||||
openTrialVotingPhase(sid, sp, { revote: false });
|
||||
});
|
||||
});
|
||||
emitTrialVoteUpdate(sid, space);
|
||||
return;
|
||||
}
|
||||
openTrialVotingPhase(sid, space, { revote: false });
|
||||
} else {
|
||||
emitTestimonyOpen(sid, space);
|
||||
}
|
||||
@@ -5335,6 +5623,11 @@ io.on('connection', (socket) => {
|
||||
console.log('[special-quiz] force flag =', space.forceSpecialQuiz, 'by', socket.id);
|
||||
});
|
||||
|
||||
/** Test Mode (จาก localStorage ฝั่ง client) — เปิด shortcut ทดสอบ เช่น Ctrl+Q ตอบถูก */
|
||||
socket.on('special-quiz-testmode', (data) => {
|
||||
socket.data.testMode = !!(data && data.on);
|
||||
});
|
||||
|
||||
socket.on('special-quiz-collide', (_data, cb) => {
|
||||
const reply = typeof cb === 'function' ? cb : () => {};
|
||||
const sid = socket.data.spaceId;
|
||||
@@ -5354,9 +5647,16 @@ io.on('connection', (socket) => {
|
||||
if (!space || !space.peers.has(socket.id)) return reply({ ok: false });
|
||||
const sess = space.specialQuiz && space.specialQuiz.session;
|
||||
if (!sess || sess.resolving) return reply({ ok: false });
|
||||
const choice = Number(data && data.choiceIndex);
|
||||
const q = sess.questions[sess.qIndex];
|
||||
if (!q || !Number.isInteger(choice) || choice < 0 || choice >= q.choices.length) {
|
||||
if (!q) return reply({ ok: false });
|
||||
/* Test Mode: Ctrl+Q เติมคำตอบที่ถูกให้ (เฉพาะ socket ที่เปิด test mode) */
|
||||
let choice;
|
||||
if (data && data.debugCorrect && socket.data.testMode) {
|
||||
choice = q.correctIndex;
|
||||
} else {
|
||||
choice = Number(data && data.choiceIndex);
|
||||
}
|
||||
if (!Number.isInteger(choice) || choice < 0 || choice >= q.choices.length) {
|
||||
return reply({ ok: false });
|
||||
}
|
||||
if (Number.isInteger(sess.answers[socket.id])) return reply({ ok: false, error: 'ตอบไปแล้ว' });
|
||||
@@ -5520,6 +5820,22 @@ io.on('connection', (socket) => {
|
||||
if (!cardEntry || !cardEntry.mapId) {
|
||||
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
|
||||
}
|
||||
/* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (pend && Number(pend.cardId) === 3 && !pend.consumed && !space.pickVote) {
|
||||
pend.consumed = true;
|
||||
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'ban' });
|
||||
startPlayerPickVote(sid, space, 'ban', (targetId) => {
|
||||
const sp = spaces.get(sid);
|
||||
if (!sp) return;
|
||||
sp.bannedPlayerId = targetId || null;
|
||||
const res = beginDetectiveSuspectMinigame(sid, sp, cardEntry, selectedIndex);
|
||||
if (!res || !res.ok) {
|
||||
io.to(sid).emit('quiz-ended', { message: (res && res.error) || 'เริ่มมินิเกมไม่สำเร็จ', returnToLobbyB: true });
|
||||
}
|
||||
});
|
||||
return reply({ ok: true, banVote: true });
|
||||
}
|
||||
const started = beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex);
|
||||
reply(started);
|
||||
});
|
||||
@@ -5539,6 +5855,7 @@ io.on('connection', (socket) => {
|
||||
alreadyOnLobbyB: true,
|
||||
suspectIndex: grant.suspectIdx,
|
||||
awardedCard: grant.awarded && grant.awarded[socket.id] != null ? grant.awarded[socket.id] : null,
|
||||
freeEvidenceCount: grant.freeEvidenceCount || 0,
|
||||
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
|
||||
});
|
||||
@@ -5547,8 +5864,12 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
// เล่นมินิเกม 1 ครั้ง = ได้การ์ด 1 ใบ (ไม่ขึ้นเกรด/คะแนน)
|
||||
returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB', { allPlayers: true });
|
||||
const g = space.lastEvidenceGrant || {};
|
||||
reply({
|
||||
ok: true,
|
||||
suspectIndex: typeof g.suspectIdx === 'number' ? g.suspectIdx : -1,
|
||||
awardedCard: (g.awarded && g.awarded[socket.id] != null) ? g.awarded[socket.id] : null,
|
||||
freeEvidenceCount: g.freeEvidenceCount || 0,
|
||||
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
|
||||
});
|
||||
@@ -5684,12 +6005,13 @@ io.on('connection', (socket) => {
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
|
||||
if (space.trialPhase !== 'voting') return reply({ ok: false, error: 'ยังไม่ถึงเวลาโหวต' });
|
||||
if (socket.id === space.silencedPlayerId) return reply({ ok: false, error: 'คุณถูกปิดปาก (Silence) — โหวตรอบนี้ไม่ได้', silenced: true });
|
||||
let idx = Math.floor(Number(data && data.index));
|
||||
if (Number.isNaN(idx) || idx < 0 || idx > 2) return reply({ ok: false, error: 'เลือกผู้ต้องสงสัยไม่ถูกต้อง' });
|
||||
space.trialVotes = space.trialVotes || {};
|
||||
space.trialVotes[socket.id] = idx;
|
||||
emitTrialVoteUpdate(sid, space);
|
||||
const totalTV = testimonyTotalParticipants(space);
|
||||
const totalTV = trialEligibleVoterTotal(space);
|
||||
if (Object.keys(space.trialVotes).length >= totalTV && totalTV > 0) {
|
||||
computeAndEmitTrialResult(sid, space);
|
||||
}
|
||||
@@ -5708,6 +6030,22 @@ io.on('connection', (socket) => {
|
||||
reply({ ok: true });
|
||||
});
|
||||
|
||||
/* โหวตเลือกผู้เล่น 1 คน (Silence/Ban) */
|
||||
socket.on('player-pick-vote', (data, cb) => {
|
||||
const reply = typeof cb === 'function' ? cb : () => {};
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
|
||||
const pv = space.pickVote;
|
||||
if (!pv || pv.resolved) return reply({ ok: false, error: 'ไม่มีการโหวตที่เปิดอยู่' });
|
||||
const tid = String((data && data.targetId) || '');
|
||||
if (!pv.candidates.some((c) => c.id === tid)) return reply({ ok: false, error: 'เป้าหมายไม่ถูกต้อง' });
|
||||
pv.votes[socket.id] = tid;
|
||||
reply({ ok: true, targetId: tid });
|
||||
emitPickVoteUpdate(sid, space);
|
||||
maybeResolvePickVote(sid, space);
|
||||
});
|
||||
|
||||
/** Host Console — ตั้งจำนวนรวม (คน+บอท) ไม่เกิน LOBBY_SLOT_TOTAL */
|
||||
socket.on('host-console-apply', (data, cb) => {
|
||||
const sid = socket.data.spaceId;
|
||||
|
||||
Reference in New Issue
Block a user