From 20b66b7332a7f1f4c3fdffab6e0bafa473b95b66 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Mon, 1 Jun 2026 16:09:38 +0000 Subject: [PATCH] updateflow --- www/html/Admin/api/player-coins-add.php | 67 ++++ www/html/Admin/index.html | 3 + www/html/Admin/private/store.json | 12 + www/html/Game/public/js/play.js | 114 +++++++ www/html/Game/public/js/room-lobby.js | 268 +++++++++++++++- www/html/Game/public/play.html | 17 +- www/html/Game/public/room-lobby.html | 2 +- www/html/Game/server.js | 392 ++++++++++++++++++++++-- 8 files changed, 845 insertions(+), 30 deletions(-) create mode 100644 www/html/Admin/api/player-coins-add.php 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); } + }