diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js
index c165dfc..6009ae9 100644
--- a/www/html/Admin/admin.js
+++ b/www/html/Admin/admin.js
@@ -4019,6 +4019,94 @@
});
}
+ var SPECIAL_CARD_GAMES = [
+ { id: 'mng8a80o', label: 'Minigame-1 จริงหรือไม่' },
+ { id: 'mno9kb07', label: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' },
+ { id: 'mnn93hpi', label: 'Minigame-3 Tower block' },
+ { id: 'mnorwqx1', label: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' },
+ { id: 'mnptfts2', label: 'Minigame-5 กระโดดขึ้นแท่น' },
+ { id: 'mnpz6rkp', label: 'Minigame-6 ยิงอุกาบาต Violent Crime' },
+ { id: 'mnq1eml7', label: 'Minigame-7 ยิง Mega Virus' }
+ ];
+ var SPECIAL_CARD_OPTS = [
+ { v: '', t: '— ไม่บังคับ —' },
+ { v: '1', t: '1 · เพิ่มเวลามินิเกม +10s' },
+ { v: '2', t: '2 · เพิ่มเวลาโหวต +10s' },
+ { v: '3', t: '3 · แบนผู้เล่น 1 คน' },
+ { v: '4', t: '4 · ปิดปากผู้เล่น 1 คน' },
+ { v: '5', t: '5 · จับผิดตัว โหวตใหม่' },
+ { v: '6', t: '6 · ทุกคน +10 COINS' },
+ { v: '7', t: '7 · ทุกคนหลักฐานฟรี +1' }
+ ];
+ var specialCardRowsBuilt = false;
+ function buildSpecialCardByMapRows() {
+ if (specialCardRowsBuilt) return;
+ var wrap = el('special-card-by-map-rows');
+ if (!wrap) return;
+ wrap.innerHTML = '';
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var row = document.createElement('div');
+ row.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;';
+ var name = document.createElement('span');
+ name.style.cssText = 'min-width:240px;color:#cfd6ea;font-size:14px;';
+ name.textContent = g.label;
+ var sel = document.createElement('select');
+ sel.id = 'tsc-' + g.id;
+ sel.style.cssText = 'padding:6px 8px;min-width:300px;font-size:14px;';
+ SPECIAL_CARD_OPTS.forEach(function (o) {
+ var op = document.createElement('option');
+ op.value = o.v;
+ op.textContent = o.t;
+ sel.appendChild(op);
+ });
+ row.appendChild(name);
+ row.appendChild(sel);
+ wrap.appendChild(row);
+ });
+ specialCardRowsBuilt = true;
+ }
+ function loadSpecialCardByMapPanel() {
+ buildSpecialCardByMapRows();
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var m = (data && data.testSpecialCardByMap) || {};
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ if (sel) sel.value = (m[g.id] != null && m[g.id] !== '') ? String(m[g.id]) : '';
+ });
+ setMsg('special-card-by-map-msg', '', '');
+ })
+ .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'โหลดไม่ได้', 'error'); });
+ }
+ function saveSpecialCardByMapPanel() {
+ var map = {};
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ var v = sel ? parseInt(String(sel.value || ''), 10) : 0;
+ if (v >= 1 && v <= 7) map[g.id] = v;
+ });
+ var btn = el('btn-special-card-by-map-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.testSpecialCardByMap = map;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && res.testSpecialCardByMap) {
+ var m = res.testSpecialCardByMap;
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ if (sel) sel.value = (m[g.id] != null) ? String(m[g.id]) : '';
+ });
+ }
+ var any = Object.keys(map).length > 0;
+ setMsg('special-card-by-map-msg', any ? 'บันทึกแล้ว — เกมที่ตั้งจะขึ้นคำถามพิเศษ + การ์ดนั้นแน่นอน' : 'ล้างการบังคับแล้ว (กลับเป็นสุ่มปกติ)', 'ok');
+ })
+ .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'บันทึกไม่ได้', 'error'); })
+ .then(function () { if (btn) btn.disabled = false; });
+ }
+
function saveJumpSurviveTimingPanel() {
var inp = el('jump-survive-height-mult');
var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
@@ -4537,7 +4625,7 @@
if (name === 'game-timing') loadGameTimingPanel();
if (name === 'stack-game') loadStackGamePanel();
if (name === 'highscore') loadHighscore();
- if (name === 'test-mode') loadForcedMinigamePanel();
+ if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); }
}
function boot() {
@@ -4763,6 +4851,8 @@
}
var forcedMgSaveBtn = el('btn-forced-minigame-save');
if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel);
+ var specialCardSaveBtn = el('btn-special-card-by-map-save');
+ if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel);
syncTestModeUi();
/* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */
window.addEventListener('storage', function (e) {
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index b1dbc34..a6ca6da 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -1155,6 +1155,12 @@
+
+ บังคับการ์ดพิเศษ → เกม (เทสต์)
+ เลือกว่าจะให้ "การ์ดพิเศษใบไหน" ขึ้นใน "เกมไหน" → เกมที่ตั้งไว้คำถามพิเศษจะขึ้นแน่นอนและให้การ์ดใบนั้น · ต้องมีคำถามพิเศษของ Level×Case นั้นด้วย · "— ไม่บังคับ —" = ปกติ (สุ่ม 1/3)
+
+
+
diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json
index 6d707e1..a63026d 100644
--- a/www/html/Admin/private/store.json
+++ b/www/html/Admin/private/store.json
@@ -82,13 +82,16 @@
"providerUserId": "p_1777015659667_xqfjh247bgp",
"notes": "auto: player-coins",
"blocked": false,
- "coins": 90,
+ "coins": 320,
"createdAt": "2026-04-24T07:28:02+00:00",
- "updatedAt": "2026-06-14T04:39:10+00:00",
- "score": 90,
+ "updatedAt": "2026-06-14T10:44:27+00:00",
+ "score": 320,
"scoreByCase": {
- "8": 60,
- "5": 30
+ "8": 210,
+ "5": 30,
+ "7": 60,
+ "2": 10,
+ "9": 10
},
"lbName": "ผู้เล่น"
},
diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json
index fa15f0a..219cb2f 100644
--- a/www/html/Game/data/game-timing.json
+++ b/www/html/Game/data/game-timing.json
@@ -89,8 +89,17 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
- "quiz",
+ "jump_survive",
"quiz_carry",
- "balloon_boss"
- ]
+ "stack"
+ ],
+ "testSpecialCardByMap": {
+ "mng8a80o": 1,
+ "mno9kb07": 2,
+ "mnn93hpi": 3,
+ "mnorwqx1": 4,
+ "mnptfts2": 5,
+ "mnpz6rkp": 6,
+ "mnq1eml7": 7
+ }
}
\ No newline at end of file
diff --git a/www/html/Game/public/css/room-lobby.css b/www/html/Game/public/css/room-lobby.css
index c059194..74f3640 100644
--- a/www/html/Game/public/css/room-lobby.css
+++ b/www/html/Game/public/css/room-lobby.css
@@ -1836,3 +1836,161 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
body.room-lobby-page .room-lobby-top-right-actions {
display: none !important;
}
+
+/* ---- Player Pick Vote overlay (Card Ban / Silence) — โหวตเลือกผู้เล่น ----
+ สร้างด้วย JS ใน room-lobby.js (#pick-vote-overlay) แต่เดิมไม่มี CSS เลย → div จมหลัง UI lobby มองไม่เห็น */
+#pick-vote-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 12000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4vh 4vw;
+ background: rgba(4, 8, 16, 0.74);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition: opacity 0.22s ease, visibility 0.22s ease;
+ font-family: 'Kanit', sans-serif;
+}
+#pick-vote-overlay.is-open {
+ opacity: 1;
+ visibility: visible;
+ pointer-events: auto;
+}
+#pick-vote-overlay .pv-shell {
+ width: min(460px, 92vw);
+ max-height: 88vh;
+ overflow-y: auto;
+ box-sizing: border-box;
+ padding: 1.4rem 1.5rem 1.6rem;
+ border-radius: 18px;
+ text-align: center;
+ color: #e8f6ff;
+ background: linear-gradient(165deg, rgba(16, 30, 50, 0.97), rgba(8, 16, 30, 0.97));
+ border: 1px solid rgba(83, 224, 255, 0.55);
+ box-shadow: 0 0 28px rgba(83, 224, 255, 0.35), inset 0 0 24px rgba(80, 180, 240, 0.14);
+ transform: translateY(12px) scale(0.97);
+ transition: transform 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.2);
+}
+#pick-vote-overlay.is-open .pv-shell {
+ transform: translateY(0) scale(1);
+}
+#pick-vote-overlay .pv-badge {
+ display: inline-block;
+ padding: 0.2rem 0.85rem;
+ border-radius: 999px;
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ color: #04121e;
+ background: linear-gradient(90deg, #53e0ff, #8ad8ff);
+ box-shadow: 0 0 14px rgba(83, 224, 255, 0.55);
+}
+#pick-vote-overlay .pv-title {
+ margin: 0.7rem 0 0.3rem;
+ font-size: 1.32rem;
+ font-weight: 700;
+ text-shadow: 0 0 12px rgba(83, 224, 255, 0.45);
+}
+#pick-vote-overlay .pv-sub {
+ margin: 0 0 0.7rem;
+ font-size: 0.86rem;
+ line-height: 1.35;
+ color: #a9c7da;
+}
+#pick-vote-overlay .pv-timer {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #53e0ff;
+ margin-bottom: 0.6rem;
+ min-height: 1.4em;
+}
+#pick-vote-overlay .pv-timer.low {
+ color: #ff7a7a;
+ animation: pvPulse 0.7s ease-in-out infinite;
+}
+@keyframes pvPulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.12); }
+}
+#pick-vote-overlay .pv-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin-bottom: 0.6rem;
+}
+#pick-vote-overlay .pv-cand {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0.7rem 1rem;
+ border-radius: 12px;
+ font: 600 1rem 'Kanit', sans-serif;
+ color: #e8f6ff;
+ cursor: pointer;
+ background: rgba(28, 48, 74, 0.85);
+ border: 1px solid rgba(83, 224, 255, 0.3);
+ transition: transform 0.12s ease, border-color 0.15s ease, background 0.15s ease;
+}
+#pick-vote-overlay .pv-cand:hover {
+ transform: translateY(-1px);
+ border-color: rgba(83, 224, 255, 0.7);
+ background: rgba(36, 62, 94, 0.95);
+}
+#pick-vote-overlay .pv-cand.is-picked {
+ border-color: #53e0ff;
+ background: linear-gradient(90deg, rgba(83, 224, 255, 0.28), rgba(83, 224, 255, 0.14));
+ box-shadow: 0 0 14px rgba(83, 224, 255, 0.5);
+}
+#pick-vote-overlay .pv-me {
+ font-size: 0.78rem;
+ color: #8ad8ff;
+}
+#pick-vote-overlay .pv-bot {
+ font-size: 0.72rem;
+ color: #04121e;
+ background: #7fd4a8;
+ padding: 0.05rem 0.45rem;
+ border-radius: 999px;
+}
+#pick-vote-overlay .pv-count {
+ position: absolute;
+ right: 0.7rem;
+ min-width: 1.5em;
+ height: 1.5em;
+ line-height: 1.5em;
+ border-radius: 999px;
+ font-size: 0.82rem;
+ font-weight: 700;
+ color: #04121e;
+ background: #53e0ff;
+ opacity: 0;
+ transform: scale(0.6);
+ transition: opacity 0.15s ease, transform 0.15s ease;
+}
+#pick-vote-overlay .pv-count.show {
+ opacity: 1;
+ transform: scale(1);
+}
+#pick-vote-overlay .pv-result {
+ margin: 0.4rem 0;
+ padding: 0.7rem;
+ border-radius: 12px;
+ font-size: 1.05rem;
+ font-weight: 700;
+ color: #fff;
+ background: rgba(83, 224, 255, 0.16);
+ border: 1px solid rgba(83, 224, 255, 0.5);
+}
+#pick-vote-overlay .pv-foot {
+ font-size: 0.82rem;
+ color: #a9c7da;
+ min-height: 1.2em;
+}
diff --git a/www/html/Game/public/img/Jumper/popup-result-card-bg.png b/www/html/Game/public/img/Jumper/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/Jumper/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/MegaVirus/popup-result-card-bg.png b/www/html/Game/public/img/MegaVirus/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/MegaVirus/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/QUESTION/popup-result-card-bg.png b/www/html/Game/public/img/QUESTION/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/QUESTION/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/TowerBlock/popup-result-card-bg.png b/www/html/Game/public/img/TowerBlock/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/TowerBlock/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/ViolentCrime/popup-result-card-bg.png b/www/html/Game/public/img/ViolentCrime/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/ViolentCrime/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/gauntlet-assets/popup-result-card-bg.png b/www/html/Game/public/img/gauntlet-assets/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/gauntlet-assets/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/img/quiz-carry/popup-result-card-bg.png b/www/html/Game/public/img/quiz-carry/popup-result-card-bg.png
new file mode 100644
index 0000000..b86b6be
Binary files /dev/null and b/www/html/Game/public/img/quiz-carry/popup-result-card-bg.png differ
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 908eb24..208880a 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -34,6 +34,27 @@
}
let nick = (params.get('nick') || '').trim() || readStoredDisplayName() || 'ผู้เล่น';
const previewMode = params.get('preview') === '1';
+ /* Card 3 Ban — ผู้ถูกแบนรอบนี้: เข้ามาดูเพื่อนเล่นได้ แต่ควบคุมตัวละคร/ทำแอ็กชันไม่ได้ (รอบหน้าเล่นปกติ) */
+ const playBannedSpectator = params.get('banned') === '1';
+ /* shield โปร่งใสคลุมทั้งจอ (z 200) กิน pointer/touch ทุกอย่าง → ปุ่มเกม(jump z59/drop z60/joystick/shoot)+canvas กดไม่ได้
+ แต่ต่ำกว่า modal (z 10000+) เลย special quiz / สรุปผล ยังกดได้ปกติ + ป้ายบอกสถานะด้านบน */
+ function setupBannedSpectatorUi() {
+ if (!playBannedSpectator || document.getElementById('banned-spectator-shield')) return;
+ const shield = document.createElement('div');
+ shield.id = 'banned-spectator-shield';
+ shield.style.cssText = 'position:fixed;inset:0;z-index:200;background:transparent;pointer-events:auto;cursor:not-allowed;touch-action:none;-webkit-tap-highlight-color:transparent';
+ ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'dblclick', 'touchstart', 'touchend', 'touchmove', 'wheel', 'contextmenu'].forEach(function (ev) {
+ shield.addEventListener(ev, function (e) { e.stopPropagation(); e.preventDefault(); }, { passive: false });
+ });
+ document.body.appendChild(shield);
+ const banner = document.createElement('div');
+ banner.id = 'banned-spectator-banner';
+ banner.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:201;pointer-events:none;background:linear-gradient(180deg,#2a1430,#160a1d);border:1px solid rgba(247,118,142,.6);border-radius:14px;padding:10px 20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,.55)';
+ banner.innerHTML = 'BAN CARD
'
+ + 'คุณถูกแบนรอบนี้ · กำลังดูเพื่อนเล่น
'
+ + 'รอบหน้ากลับมาเล่นได้ตามปกติ
';
+ document.body.appendChild(banner);
+ }
const specialQuizForceDebug = params.get('sqForce') === '1';
const forceDefaultCharacter = params.get('defaultChar') === '1';
const editorEmbedReturn = params.get('editorEmbed') === '1';
@@ -442,7 +463,7 @@
var img = document.createElement('img');
img.src = c.imageUrl;
img.alt = EVIDENCE_RARITY_LABEL[rar] || 'หลักฐาน';
- img.style.cssText = 'display:block;width:min(34vw,260px);height:auto;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6);cursor:zoom-in';
+ img.style.cssText = 'display:block;width:min(51vw,390px);height:auto;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6);cursor:zoom-in';
img.title = 'แตะเพื่อดูใหญ่';
img.addEventListener('click', function () { openEvidenceCardLightboxPlay(c.imageUrl, EVIDENCE_RARITY_LABEL[rar] || '', rar); });
img.onerror = function () { this.onerror = null; this.style.width = '200px'; this.style.height = '300px'; this.style.background = '#1b2240'; this.removeAttribute('src'); };
@@ -10538,27 +10559,26 @@
function quizCarryPreviewBotLobbyWanderStep(o, w, h) {
const now = Date.now();
o.botIsWalking = false;
- if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
+ /* เดินแบบมีจุดหมาย (เหมือน lobby) — สุ่มจุดเป้า เดินเข้าหาแบบทแยงลื่น พักบ้าง · เลิกใช้สุ่มทิศแกนที่ดูกระตุก */
+ const arrived = (typeof o.botWanderTX === 'number' && typeof o.botWanderTY === 'number')
+ && Math.hypot(o.botWanderTX - o.x, o.botWanderTY - o.y) < 0.5;
+ const timedOut = typeof o.botWanderRetargetAt === 'number' && now >= o.botWanderRetargetAt;
+ if (typeof o.botWanderTX !== 'number' || typeof o.botWanderTY !== 'number' || arrived || timedOut) {
+ o.botWanderTX = 1 + Math.random() * Math.max(1, w - 2);
+ o.botWanderTY = 1 + Math.random() * Math.max(1, h - 2);
+ o.botWanderRetargetAt = now + 3000 + Math.floor(Math.random() * 4500);
+ o.botWanderPauseUntil = (arrived && Math.random() < 0.45) ? now + 500 + Math.floor(Math.random() * 1600) : 0;
}
- if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600;
- if (now >= o.botWanderNextTurn) {
- o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200);
- if (Math.random() < 0.55) {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
- }
- }
- const accX = o.botWanderDx;
- const accY = o.botWanderDy;
- if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up';
- else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left';
+ if (o.botWanderPauseUntil && now < o.botWanderPauseUntil) return;
+ let dx = o.botWanderTX - o.x;
+ let dy = o.botWanderTY - o.y;
+ const dist = Math.hypot(dx, dy) || 1;
+ dx /= dist; dy /= dist;
+ if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up';
+ else o.direction = dx > 0 ? 'right' : 'left';
const step = MOVE_SPEED * quizCarryWalkSpeedMultActive;
- const nx = o.x + accX * step;
- const ny = o.y + accY * step;
+ const nx = o.x + dx * step;
+ const ny = o.y + dy * step;
const ox = o.x;
const oy = o.y;
if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) {
@@ -10570,10 +10590,8 @@
} else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) {
o.y = ny;
} else {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
- o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
+ o.botWanderTX = undefined;
+ o.botWanderTY = undefined;
}
}
if (!Number.isFinite(o.x)) o.x = 0.5;
@@ -11168,6 +11186,8 @@
lastStackPreviewActorId = '__human__';
lastStackPreviewActorUntil = Date.now() + 900;
}
+ /* MG3 stack: เดินไปแตะไอคอนไม่ได้ → "หยอดบล็อก" (บล็อกร่วงผ่านไอคอน) = ทริกคำถามพิเศษเลย */
+ if (specialQuizIcon) sendSpecialQuizCollide();
return true;
}
@@ -11181,6 +11201,8 @@
o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800));
lastStackPreviewActorId = botId;
lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800));
+ /* บอทหยอดบล็อกก็ทริกคำถามพิเศษ (กันไม่มีคนหยอดไปโดน) */
+ if (specialQuizIcon) sendSpecialQuizCollide();
}
/** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */
@@ -12722,6 +12744,8 @@
try { quizCarryAnswerCloseAt = shift(quizCarryAnswerCloseAt); } catch (e) {}
try { if (typeof jumpSurviveSessionStartMs === 'number' && jumpSurviveSessionStartMs > 0) jumpSurviveSessionStartMs += deltaMs; } catch (e) {}
try { if (typeof spaceShooterSessionStartMs === 'number' && spaceShooterSessionStartMs > 0) spaceShooterSessionStartMs += deltaMs; } catch (e) {}
+ try { if (typeof stackTowerSessionStartAt === 'number' && stackTowerSessionStartAt > 0) stackTowerSessionStartAt += deltaMs; } catch (e) {}
+ try { if (typeof balloonBossSessionStartMs === 'number' && balloonBossSessionStartMs > 0) balloonBossSessionStartMs += deltaMs; } catch (e) {}
}
/** เรียกทุกเฟรมระหว่าง freeze — ดันเวลาไปข้างหน้าเท่าที่ผ่านไปจริง ทำให้ countdown หยุดนิ่ง */
@@ -12864,12 +12888,22 @@
/** เช็คผู้เล่นเดินชนไอคอน — ใครชนก่อนเป็นคนเปิดคำถามให้ทั้งห้อง (server กันซ้ำ) */
function checkSpecialQuizCollision() {
if (!specialQuizIcon || specialQuizCollideSent || specialQuizActiveQuestion) return;
- if (!Number.isFinite(me.x) || !Number.isFinite(me.y)) return;
- const dx = me.x - specialQuizIcon.x;
- const dy = me.y - specialQuizIcon.y;
+ const ix = specialQuizIcon.x;
+ const iy = specialQuizIcon.y;
/* รัศมีกว้างขึ้น (1.25 tile) — ไอคอนวาดใหญ่ 1.75 tile เดินเข้าใกล้ก็ทริกได้ ไม่ต้องทับ tile กลางพอดี */
- if (dx * dx + dy * dy <= 1.25 * 1.25) {
- sendSpecialQuizCollide();
+ const within = (ex, ey) => {
+ if (!Number.isFinite(ex) || !Number.isFinite(ey)) return false;
+ const dx = ex - ix;
+ const dy = ey - iy;
+ return dx * dx + dy * dy <= 1.25 * 1.25;
+ };
+ if (within(me.x, me.y)) { sendSpecialQuizCollide(); return; }
+ /* บอท (preview / เติมคดี) เดินโดนไอคอนก็เปิดคำถามพิเศษให้ทั้งห้อง (ผู้เล่นจริงเป็นคนตอบ) — กันกรณีไม่มีคนเดินไปโดน */
+ if (playBotsEnabled()) {
+ for (const [id, o] of others) {
+ if (!isPreviewBotId(id) || !o) continue;
+ if (within(o.x, o.y)) { sendSpecialQuizCollide(); return; }
+ }
}
}
@@ -13113,6 +13147,8 @@
if (!ms) return;
if (isJumpSurvive() && jumpSurviveSessionStartMs) jumpSurviveSessionStartMs += ms;
if (isSpaceShooter() && spaceShooterSessionStartMs) spaceShooterSessionStartMs += ms;
+ if (isStack() && stackTowerSessionStartAt) stackTowerSessionStartAt += ms;
+ if (isBalloonBoss() && balloonBossSessionStartMs) balloonBossSessionStartMs += ms;
if (isQuizCarry()) {
if (quizCarryAnswerCloseAt > 0) quizCarryAnswerCloseAt += ms;
if (quizCarryOptionRevealAt > 0) quizCarryOptionRevealAt += ms;
@@ -13244,7 +13280,8 @@
if (!disp || !Array.isArray(disp.ranked)) return;
/* mission map — สรุปผลภารกิจตาม mockup (Minigame 1/4–7 + Last Light 07-summary) */
const tfMockSummary = isGcmMissionMockSummaryPlay(mission);
- const mg2Summary = tfMockSummary && missionSkin === 'gauntlet_crown';
+ /* สรุปผลแบบ MG2 (โบนัสพิเศษ 2 คอลัมน์ + popup-result panel) ให้ "ทุกเกม" ใช้ — CSS/asset ต่อ skin มีครบ */
+ const mg2Summary = tfMockSummary;
ov.classList.toggle('gcm-quiz-tf-mock', tfMockSummary);
ov.classList.toggle('gcm-mg2-mock', mg2Summary);
const gcmVrule = document.getElementById('gcm-vrule');
@@ -13438,7 +13475,9 @@
bonusEl.innerHTML = '';
const gLetter = String(disp.grade || 'C').toUpperCase().charAt(0);
const missionOk = gLetter !== 'F';
+ const hasSpecialCard = !!(mission && mission.specialCardAwarded);
let bonusCardHost = bonusEl;
+ /* mg2 summary: โชว์ block "โบนัสพิเศษ" เสมอ (missionOk) — ได้การ์ด=โชว์การ์ด + complete-select, ไม่ได้=complete ธรรมดา + การ์ดเปล่า placeholder */
if (mg2Summary && missionOk) {
const bonusWrap = document.createElement('div');
bonusWrap.className = 'gcm-bonus-mg2 sm-bonus';
@@ -13449,12 +13488,12 @@
bonusHead.textContent = 'โบนัสพิเศษ';
const completeImg = document.createElement('img');
completeImg.className = 'gcm-bonus-mg2-complete complete';
- completeImg.src = gcmMissionMockAssetUrl(mission, 'mission-complete-select.png');
+ completeImg.src = gcmMissionMockAssetUrl(mission, hasSpecialCard ? 'mission-complete-select.png' : 'mission-complete.png');
completeImg.alt = 'ภารกิจสำเร็จ';
completeImg.decoding = 'async';
completeImg.onerror = function () {
this.onerror = null;
- this.src = BASE + '/img/gauntlet-assets/result-check.png';
+ this.src = gcmMissionMockAssetUrl(mission, 'mission-complete-select.png');
};
bonusLeft.appendChild(bonusHead);
bonusLeft.appendChild(completeImg);
@@ -13464,7 +13503,16 @@
bonusWrap.appendChild(bonusRight);
bonusEl.appendChild(bonusWrap);
bonusCardHost = bonusRight;
- } else if (missionOk) {
+ if (!hasSpecialCard) {
+ /* ไม่ได้การ์ดพิเศษ → โชว์การ์ดพื้นหลังเปล่า (popup-result-card-bg.png) ตามดีไซน์ */
+ const bgCard = document.createElement('img');
+ bgCard.className = 'bail-card gcm-special-card-img';
+ bgCard.src = gcmMissionMockAssetUrl(mission, 'popup-result-card-bg.png');
+ bgCard.alt = '';
+ bgCard.decoding = 'async';
+ bonusRight.appendChild(bgCard);
+ }
+ } else if (missionOk && !mg2Summary) {
const row = document.createElement('div');
row.className = 'gcm-bonus-okrow';
row.style.display = 'flex';
@@ -13517,15 +13565,6 @@
}
if (mission && mission.specialCardAwarded) {
renderSpecialCardInBonus(bonusCardHost, mission.specialCardAwarded);
- } else if (mg2Summary && missionOk && rc && rc.th) {
- const cardId = rc.kind === 'culprit' ? 1 : (rc.kind === 'rare' ? 5 : 3);
- renderSpecialCardInBonus(bonusCardHost, {
- th: rc.th,
- en: rc.en || '',
- imageUrl: '/Game/img/special-cards/card-' + cardId + '.png',
- cardId: cardId,
- rarity: rc.kind === 'rare' ? 'rare' : (rc.kind === 'culprit' ? 'legendary' : 'common'),
- });
}
}
const gcmHead = document.getElementById('gcm-heading');
@@ -18126,6 +18165,7 @@
characterId: getPlayCharacterId(),
playMapId: joinPlayMapId || undefined,
playerKey: getJdPlayerKey(),
+ bannedSpectator: playBannedSpectator || undefined,
}, (res) => {
if (!res || !res.ok) {
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
@@ -18139,6 +18179,7 @@
return;
}
myId = socket.id;
+ if (playBannedSpectator) setupBannedSpectatorUi();
if (specialQuizForceDebug) {
try { socket.emit('special-quiz-force', { on: true }); } catch (eF) { /* ignore */ }
}
@@ -19776,53 +19817,43 @@
if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) {
return;
}
- /* ก่อนตอบ / พัก / บอทสับสน / ผี (ตอบผิดไปแล้ว): เดินทุกเฟรมตามทิศ */
- if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
+ /* ก่อนตอบ / พัก / บอทสับสน / ผี: เดินแบบมีจุดหมาย (สุ่มจุดเป้า เดินเข้าหาแบบทแยงลื่น พักบ้าง) — เนียนกว่าสุ่มทิศแกน */
+ const arrivedQ = (typeof o.botWanderTX === 'number' && typeof o.botWanderTY === 'number')
+ && Math.hypot(o.botWanderTX - o.x, o.botWanderTY - o.y) < 0.5;
+ const timedOutQ = typeof o.botWanderRetargetAt === 'number' && now >= o.botWanderRetargetAt;
+ if (typeof o.botWanderTX !== 'number' || typeof o.botWanderTY !== 'number' || arrivedQ || timedOutQ) {
+ o.botWanderTX = 1 + Math.random() * Math.max(1, w - 2);
+ o.botWanderTY = 1 + Math.random() * Math.max(1, h - 2);
+ o.botWanderRetargetAt = now + 3000 + Math.floor(Math.random() * 4500);
+ o.botWanderPauseUntil = (arrivedQ && Math.random() < 0.4) ? now + 500 + Math.floor(Math.random() * 1500) : 0;
}
- if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600;
- if (now >= o.botWanderNextTurn) {
- o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200);
- if (Math.random() < 0.55) {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
- }
- }
- const accX = o.botWanderDx;
- const accY = o.botWanderDy;
- if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up';
- else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left';
- const step = MOVE_SPEED;
- const tox = o.x + accX * step;
- const toy = o.y + accY * step;
const ox = o.x, oy = o.y;
- const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData);
- if (pathStrictW) {
- const moved = quizBattleTryStepMovePlay(o.x, o.y, tox, toy, (nx, ny, fx, fy) => canWalkLikeLobbyForBot(nx, ny, fx, fy, o));
- o.x = moved.x;
- o.y = moved.y;
- if (Math.abs(o.x - ox) < 1e-5 && Math.abs(o.y - oy) < 1e-5) {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
- o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
- }
- } else if (canWalkLikeLobbyForBot(tox, toy, o.x, o.y, o)) {
- o.x = tox;
- o.y = toy;
- } else {
- if (canWalkLikeLobbyForBot(tox, o.y, o.x, o.y, o)) {
+ if (!(o.botWanderPauseUntil && now < o.botWanderPauseUntil)) {
+ let dxq = o.botWanderTX - o.x;
+ let dyq = o.botWanderTY - o.y;
+ const distq = Math.hypot(dxq, dyq) || 1;
+ dxq /= distq; dyq /= distq;
+ if (Math.abs(dyq) > Math.abs(dxq)) o.direction = dyq > 0 ? 'down' : 'up';
+ else o.direction = dxq > 0 ? 'right' : 'left';
+ const step = MOVE_SPEED;
+ const tox = o.x + dxq * step;
+ const toy = o.y + dyq * step;
+ const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData);
+ if (pathStrictW) {
+ const moved = quizBattleTryStepMovePlay(o.x, o.y, tox, toy, (nx, ny, fx, fy) => canWalkLikeLobbyForBot(nx, ny, fx, fy, o));
+ o.x = moved.x;
+ o.y = moved.y;
+ if (Math.abs(o.x - ox) < 1e-5 && Math.abs(o.y - oy) < 1e-5) { o.botWanderTX = undefined; o.botWanderTY = undefined; }
+ } else if (canWalkLikeLobbyForBot(tox, toy, o.x, o.y, o)) {
+ o.x = tox;
+ o.y = toy;
+ } else if (canWalkLikeLobbyForBot(tox, o.y, o.x, o.y, o)) {
o.x = tox;
} else if (canWalkLikeLobbyForBot(o.x, toy, o.x, o.y, o)) {
o.y = toy;
} else {
- const d = pickRandomPreviewBotWanderDir();
- o.botWanderDx = d[0];
- o.botWanderDy = d[1];
- o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
+ o.botWanderTX = undefined;
+ o.botWanderTY = undefined;
}
}
if (!Number.isFinite(o.x)) o.x = 0.5;
@@ -22502,6 +22533,11 @@
}
document.addEventListener('keydown', (e) => {
+ /* Card 3 Ban — ผู้ถูกแบนรอบนี้คุมตัวละครไม่ได้: บล็อกคีย์เกมทั้งหมด (ยังพิมพ์แชตได้) */
+ if (playBannedSpectator && !isChatFocused()) {
+ if (isMovementKey(e.code) || ['Space', 'Enter', 'NumpadEnter', 'KeyF'].includes(e.code)) e.preventDefault();
+ return;
+ }
/* Test Mode shortcut: Ctrl+Q = ตอบถูกใน special quiz (server เติมคำตอบให้) */
if (e.ctrlKey && (e.code === 'KeyQ' || e.key === 'q' || e.key === 'Q')) {
if (specialQuizActiveQuestion && !specialQuizAnswered && __isTestModeOnPlay()) {
diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js
index 40dacf1..1a11b01 100644
--- a/www/html/Game/public/js/room-lobby.js
+++ b/www/html/Game/public/js/room-lobby.js
@@ -2104,28 +2104,27 @@
const w = mapData.width || 20, h = mapData.height || 15;
const now = Date.now();
lobbyBots.forEach((b, id) => {
- if (typeof b.botWanderDx !== 'number' || typeof b.botWanderDy !== 'number' || (b.botWanderDx === 0 && b.botWanderDy === 0)) {
- const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
- b.botWanderDx = d[0];
- b.botWanderDy = d[1];
+ /* เดินแบบมีจุดหมาย: สุ่มจุดเป้าในแมป เดินเข้าหาแบบทแยงลื่น แล้วค่อยสุ่มใหม่เมื่อถึง/ติด/หมดเวลา + พักยืนเป็นช่วง — ดูเป็นธรรมชาติ ไม่เร่ร่อนสุ่มทิศ */
+ const arrived = (typeof b.botWanderTX === 'number' && typeof b.botWanderTY === 'number')
+ && Math.hypot(b.botWanderTX - b.x, b.botWanderTY - b.y) < 0.5;
+ const timedOut = typeof b.botWanderRetargetAt === 'number' && now >= b.botWanderRetargetAt;
+ if (typeof b.botWanderTX !== 'number' || typeof b.botWanderTY !== 'number' || arrived || timedOut) {
+ b.botWanderTX = 1 + Math.random() * Math.max(1, w - 2);
+ b.botWanderTY = 1 + Math.random() * Math.max(1, h - 2);
+ b.botWanderRetargetAt = now + 3000 + Math.floor(Math.random() * 4500);
+ b.botWanderPauseUntil = (arrived && Math.random() < 0.45) ? now + 500 + Math.floor(Math.random() * 1600) : 0;
}
- if (typeof b.botWanderNextTurn !== 'number') b.botWanderNextTurn = now + 600;
- if (now >= b.botWanderNextTurn) {
- b.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200);
- if (Math.random() < 0.55) {
- const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
- b.botWanderDx = d[0];
- b.botWanderDy = d[1];
- }
- }
- const accX = b.botWanderDx;
- const accY = b.botWanderDy;
- if (Math.abs(accY) > Math.abs(accX)) b.direction = accY > 0 ? 'down' : 'up';
- else if (accX !== 0) b.direction = accX > 0 ? 'right' : 'left';
+ if (b.botWanderPauseUntil && now < b.botWanderPauseUntil) { b.isWalking = false; return; }
+ let dx = b.botWanderTX - b.x;
+ let dy = b.botWanderTY - b.y;
+ const dist = Math.hypot(dx, dy) || 1;
+ dx /= dist; dy /= dist;
+ if (Math.abs(dy) > Math.abs(dx)) b.direction = dy > 0 ? 'down' : 'up';
+ else b.direction = dx > 0 ? 'right' : 'left';
const ox = b.x, oy = b.y;
const step = MOVE_SPEED;
- const nx = b.x + accX * step;
- const ny = b.y + accY * step;
+ const nx = b.x + dx * step;
+ const ny = b.y + dy * step;
if (canWalkLobbyBot(nx, ny, b.x, b.y, id)) {
b.x = nx;
b.y = ny;
@@ -2134,10 +2133,8 @@
} else if (canWalkLobbyBot(b.x, ny, b.x, b.y, id)) {
b.y = ny;
} else {
- const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
- b.botWanderDx = d[0];
- b.botWanderDy = d[1];
- b.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
+ b.botWanderTX = undefined;
+ b.botWanderTY = undefined; // ติดผนัง → หาเป้าใหม่ (ไม่กระตุกชนซ้ำ)
}
b.x = Math.max(0, Math.min(w - 0.01, b.x));
b.y = Math.max(0, Math.min(h - 0.01, b.y));
@@ -6519,17 +6516,16 @@
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);
if (caseIdStr) q += '&case=' + encodeURIComponent(caseIdStr);
if (data && data.detectiveMinigame) q += '&detectiveReturn=1';
+ /* Card 3 Ban — ผู้ที่ถูกแบนรอบนี้: เข้าไปดูเพื่อนเล่นได้ แต่ควบคุมตัวละครไม่ได้ (รอบหน้าเล่นปกติ) */
+ if (data && data.bannedPlayerId && data.bannedPlayerId === socket.id) {
+ q += '&banned=1';
+ appendLobbySystemChat('— คุณถูกแบนรอบนี้ (Ban Card) · เข้าไปดูเพื่อนเล่นได้ แต่เล่นเองไม่ได้ · รอบหน้าเล่นได้ปกติ');
+ }
location.href = q;
});
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 02592e0..db23be8 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -5180,7 +5180,7 @@
-
+
v —