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 —