diff --git a/www/html/Admin/api/player-coins-add.php b/www/html/Admin/api/player-coins-add.php
new file mode 100644
index 0000000..79e3b84
--- /dev/null
+++ b/www/html/Admin/api/player-coins-add.php
@@ -0,0 +1,67 @@
+ 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']]);
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index 2407bb0..a08d6d8 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -1003,6 +1003,9 @@
Ctrl + 1 / 2 / 3 — ในหน้าเลือกผู้ต้องสงสัย: เก็บหลักฐาน 1 ใบให้ suspect คนนั้นทันที (เทียบเท่าเล่นมินิเกมจบ 1 ครั้ง)
กดครบ 3 ปุ่ม → ปุ่ม "ชี้ตัวคนร้าย" โผล่
+ Ctrl + Q — ในเกมตอนคำถามพิเศษ (Special Quiz) เปิดอยู่: ตอบข้อปัจจุบันให้ถูกอัตโนมัติ
+ เซิร์ฟเวอร์เป็นผู้เติมคำตอบที่ถูก (กันโกง) — ใช้ทดสอบ flow รับการ์ดพิเศษได้เร็ว
+
diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json
index d9fea38..c2fd2f3 100644
--- a/www/html/Admin/private/store.json
+++ b/www/html/Admin/private/store.json
@@ -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"
}
]
}
\ No newline at end of file
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 81f7b18..4c0ffad 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -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 =
+ '![]()
' +
+ '
';
+ 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') {
diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js
index 8661f27..09b368a 100644
--- a/www/html/Game/public/js/room-lobby.js
+++ b/www/html/Game/public/js/room-lobby.js
@@ -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 = 'ลงติกแล้ว : ' + (voted || 0) + '/' + (total || 0) + '';
}
+ 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 =
+ '' +
+ '
CARD' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
';
+ 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) ? '(คุณ)' : (c.isBot ? 'บอท' : '');
+ b.innerHTML = '' + (c.nickname || '?') + '' + tag + '';
+ 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 = '' +
+ '
BAN CARD
' +
+ '
คุณถูกแบนรอบนี้
' +
+ '
รอเพื่อนเล่นมินิเกมจบ แล้วกลับมา LobbyB ด้วยกัน
' +
+ '
';
+ 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; }
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 460d332..d02d32f 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -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); }
+ }
@@ -3939,7 +3954,7 @@
-
+
v —