update all games and cards

This commit is contained in:
2026-06-14 10:46:46 +00:00
parent 52a45f0ae0
commit cdfd19c7a1
19 changed files with 627 additions and 144 deletions
+91 -1
View File
@@ -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) {
+6
View File
@@ -1155,6 +1155,12 @@
<button type="button" class="btn" id="btn-forced-minigame-save">บันทึก</button>
</div>
<p id="forced-minigame-msg" class="msg" role="status"></p>
<h3 class="admin-subheading" style="margin-top:28px;">บังคับการ์ดพิเศษ → เกม (เทสต์)</h3>
<p class="muted">เลือกว่าจะให้ "การ์ดพิเศษใบไหน" ขึ้นใน "เกมไหน" → เกมที่ตั้งไว้คำถามพิเศษจะ<strong>ขึ้นแน่นอน</strong>และให้การ์ดใบนั้น · ต้องมีคำถามพิเศษของ Level×Case นั้นด้วย · "— ไม่บังคับ —" = ปกติ (สุ่ม 1/3)</p>
<div id="special-card-by-map-rows" style="display:flex;flex-direction:column;gap:8px;max-width:640px;"></div>
<div style="margin-top:12px;"><button type="button" class="btn" id="btn-special-card-by-map-save">บันทึก</button></div>
<p id="special-card-by-map-msg" class="msg" role="status"></p>
</section>
</div><!-- /.admin-content -->
</div>
+8 -5
View File
@@ -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": "ผู้เล่น"
},
+12 -3
View File
@@ -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
}
}
+158
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+119 -83
View File
@@ -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 = '<div style="font:900 13px/1 Kanit;letter-spacing:1px;color:#f7768e">BAN CARD</div>'
+ '<div style="font:800 18px/1.3 Kanit;color:#fff;margin-top:4px">คุณถูกแบนรอบนี้ · กำลังดูเพื่อนเล่น</div>'
+ '<div style="font:600 12px/1.3 Kanit;color:#e6b8c4;margin-top:2px">รอบหน้ากลับมาเล่นได้ตามปกติ</div>';
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/47 + 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()) {
+25 -29
View File
@@ -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;
});
+1 -1
View File
@@ -5180,7 +5180,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.006131808"></script>
<script src="js/play.js?v=0.006131817"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
+2 -2
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0174">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0175">
<link rel="stylesheet" href="css/profile-popup.css?v=6">
<link rel="stylesheet" href="css/customize-popup.css?v=42">
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
@@ -1613,7 +1613,7 @@
<script src="js/display-name.js?v=2"></script>
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/room-lobby.js?v=0.0275"></script>
<script src="js/room-lobby.js?v=0.0277"></script>
<div class="version-tag">v —</div>
</body>
</html>
+151 -11
View File
@@ -521,10 +521,24 @@ function getSpecialQuizSet(settings, level, caseId) {
/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */
/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */
const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp', 'mno9kb07'];
/** คำถามพิเศษขึ้นได้ "ทุกเกม" (7 มินิเกม) — รวม stack(mnn93hpi)+balloon(mnq1eml7) ที่จับด้วยคลิก/แตะไอคอน */
const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mno9kb07', 'mnn93hpi', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp', 'mnq1eml7'];
const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police'];
/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */
/** โอกาสขึ้นคำถามพิเศษต่อรอบ (เล่นจริง) — 1/3 · โหมดเทสต์บังคับการ์ดต่อเกมจะ force ขึ้นเสมอแยกต่างหาก */
const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3;
/** โหมดเทสต์ (admin): บังคับ "การ์ดพิเศษใบไหน → เกมไหน" — { mapId: cardId(1-7) } · เกมที่ตั้งไว้จะ force ขึ้นเสมอด้วยการ์ดนั้น */
function normalizeTestSpecialCardByMap(v) {
const out = {};
if (v && typeof v === 'object') {
SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.forEach((mid) => {
const c = Math.floor(Number(v[mid]));
if (c >= 1 && c <= 7) out[mid] = c;
});
}
return out;
}
/** เวลาให้ตอบต่อข้อ (มิลลิวิ) — ค่าคงที่เริ่มต้น (ย้ายไป Admin ได้ภายหลัง) */
const SPECIAL_QUIZ_ANSWER_MS = 20000;
/** หน่วงโชว์เฉลยก่อนข้อต่อไป */
@@ -558,8 +572,12 @@ function specialQuizTileWalkable(md, x, y) {
const w = md.width || 20;
const h = md.height || 15;
if (x < 0 || x >= w || y < 0 || y >= h) return false;
const row = md.objects && md.objects[y];
return !!(row && row[x] !== 1);
const obj = md.objects && md.objects[y];
if (obj && obj[x] === 1) return false;
/* ต้องไม่ใช่ช่อง blockPlayer ด้วย — ไม่งั้นไอคอนไปโผล่บนกำแพง/บล็อกที่ผู้เล่นเดินเข้าไม่ถึง (เช่น MG1 quiz มี 140 ช่อง) */
const bp = md.blockPlayer && md.blockPlayer[y];
if (bp && bp[x] === 1) return false;
return true;
}
/** หลีกเลี่ยงโซน gameplay (คำตอบจริง/ผิด, hub, ตัวเลือกหยิบมาวาง) เพื่อไม่ชนกลไกเกม */
@@ -576,6 +594,51 @@ function specialQuizTileInGameZone(md, x, y) {
function pickSpecialQuizSpawnTile(md) {
const w = md.width || 20;
const h = md.height || 15;
/* gauntlet (): "" gauntletPlayerSpawns
( x กลางทาง) เลนวงผานเกบได/คลกได ไมนลอยออกนอกเลนเกบไมได */
if (md.gameType === 'gauntlet' && Array.isArray(md.gauntletPlayerSpawns) && md.gauntletPlayerSpawns.length) {
const rows = Array.from(new Set(md.gauntletPlayerSpawns
.map((s) => Math.floor(Number(s.y)))
.filter((n) => Number.isFinite(n) && n >= 0 && n < h)));
const laneCands = [];
const xMin = Math.max(4, Math.floor(w * 0.30));
const xMax = Math.min(w - 2, Math.floor(w * 0.72));
rows.forEach((ry) => {
for (let x = xMin; x <= xMax; x++) {
if (specialQuizTileWalkable(md, x, ry) && !specialQuizTileInGameZone(md, x, ry)) laneCands.push({ x, y: ry });
}
});
if (laneCands.length) {
const p = laneCands[Math.floor(Math.random() * laneCands.length)];
return { x: p.x + 0.5, y: p.y + 0.5 };
}
}
/* stack (Tower block): + "" /
(อยในจอ) เลนคล/แตะเกบได ไมนไปโผลขอบแมปนอกจอ */
if (md.gameType === 'stack') {
const zc = [];
[md.stackReleaseArea, md.stackLandArea].forEach((g) => {
if (Array.isArray(g)) {
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (g[y] && g[y][x]) zc.push({ x, y });
}
});
if (zc.length) {
let cx2 = 0;
let cy2 = 0;
zc.forEach((c) => { cx2 += c.x; cy2 += c.y; });
cx2 = Math.round(cx2 / zc.length);
cy2 = Math.round(cy2 / zc.length);
for (let rad = 0; rad <= 6; rad++) {
for (let yy = cy2 - rad; yy <= cy2 + rad; yy++) {
for (let xx = cx2 - rad; xx <= cx2 + rad; xx++) {
if (specialQuizTileWalkable(md, xx, yy)) return { x: xx + 0.5, y: yy + 0.5 };
}
}
}
/* โซนทั้งหมดเป็น object → วางกลางโซนไปเลย (จะคลิกได้แม้ไม่ walkable เพราะ stack ใช้คลิก) */
return { x: cx2 + 0.5, y: cy2 + 0.5 };
}
}
/* seed BFS (spawnArea)
fallback: md.spawn (าเดนได) แลวคอยสแกนหา walkable tile แรก */
let sx = -1;
@@ -596,6 +659,20 @@ function pickSpecialQuizSpawnTile(md) {
const fy = Math.floor(Number(sp.y));
if (specialQuizTileWalkable(md, fx, fy)) { sx = fx; sy = fy; }
}
if (sx < 0) {
/* spawnArea/spawn ( stack/balloon) seed ""
เพอใหไอคอนอยกลางฉากทมองเห/เออมถ ไมไปโผลมนอกจอ */
const ccx = Math.floor(w / 2);
const ccy = Math.floor(h / 2);
const maxRad = Math.max(w, h);
for (let rad = 0; rad <= maxRad && sx < 0; rad++) {
for (let yy = ccy - rad; yy <= ccy + rad && sx < 0; yy++) {
for (let xx = ccx - rad; xx <= ccx + rad && sx < 0; xx++) {
if (specialQuizTileWalkable(md, xx, yy)) { sx = xx; sy = yy; }
}
}
}
}
if (sx < 0) {
for (let y = 0; y < h && sx < 0; y++) {
for (let x = 0; x < w; x++) {
@@ -625,7 +702,14 @@ function pickSpecialQuizSpawnTile(md) {
}
}
if (!cands.length) return null;
const pick = cands[Math.floor(Math.random() * cands.length)];
/* "/" (2-8 seed) + ·
ไมในระยะนนคอย fallback งหมด */
const near = cands.filter((c) => {
const d = Math.abs(c.x - sx) + Math.abs(c.y - sy);
return d >= 2 && d <= 8;
});
const pool = near.length ? near : cands;
const pick = pool[Math.floor(Math.random() * pool.length)];
return { x: pick.x + 0.5, y: pick.y + 0.5 };
}
@@ -666,10 +750,17 @@ function maybeSpawnSpecialQuizForRun(space) {
const questions = (set && Array.isArray(set.questions) ? set.questions : [])
.map(specialQuizNormalizeQuestion)
.filter(Boolean);
const card = set && set.specialCard;
let card = set && set.specialCard;
/* โหมดเทสต์ (admin): บังคับการ์ดใบที่เลือกสำหรับเกมนี้ + force ขึ้นเสมอ (ไม่ต้องสุ่ม) */
const testCardId = (runtimeGameTiming.testSpecialCardByMap || {})[mapId];
const testForcedCard = (Number(testCardId) >= 1 && Number(testCardId) <= 7);
if (testForcedCard) {
const def = specialCardDef(testCardId) || {};
card = sanitizeSpecialCard({ cardId: Number(testCardId), th: def.th });
}
const hasCard = !!(card && (card.cardId || card.imageUrl));
if (!questions.length || !hasCard) { console.log('[special-quiz] skip:no-questions-or-card', mapId, 'L' + level + '_C' + caseId, 'q=' + questions.length, 'card=' + hasCard); return; }
const forced = !!space.forceSpecialQuiz;
const forced = !!space.forceSpecialQuiz || testForcedCard;
const roll = Math.random();
if (!forced && roll > SPECIAL_QUIZ_SPAWN_CHANCE) { console.log('[special-quiz] skip:roll', mapId, roll.toFixed(3), '>', SPECIAL_QUIZ_SPAWN_CHANCE.toFixed(3)); return; }
if (forced) console.log('[special-quiz] FORCE-spawn (debug)', mapId);
@@ -1586,6 +1677,8 @@ const MINIGAME_RANK_COINS = [10, 8, 6, 4, 2, 0];
/* คะแนน + สถานะรอด/ตาย ของผู้เล่นต่อ gameType ตอนจบมินิเกม */
function minigamePlayerScoreSurvived(space, p, gameType) {
/* Card 3 Ban — ผู้ถูกแบนเข้ามาดูเฉยๆ ไม่ได้เล่น → ไม่นับเป็นผู้รอด/ไม่ได้คะแนน */
if (p && p.bannedSpectator) return { score: 0, survived: false };
switch (gameType) {
case 'quiz': {
const st = space.quizSession && space.quizSession.players ? space.quizSession.players[p.id] : null;
@@ -1819,6 +1912,8 @@ function defaultGameTiming() {
balloonBossBalloonsPerPlayer: 0,
/** โหมดเทสต์: เกมบังคับต่อการ์ด 3 ใบ — ['','',''] = สุ่มหมด, แต่ละช่องใส่ key เพื่อเจาะจงใบนั้น */
forcedMinigameKeys: ['', '', ''],
/** โหมดเทสต์: บังคับการ์ดพิเศษต่อเกม { mapId: cardId } */
testSpecialCardByMap: {},
};
}
@@ -2206,6 +2301,7 @@ function loadGameTiming() {
forcedMinigameKeys: normalizeForcedMinigameKeys(
j.forcedMinigameKeys != null ? j.forcedMinigameKeys : j.forcedMinigameKey,
),
testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap),
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -2416,6 +2512,9 @@ function saveGameTimingToFile(d) {
: (Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKey')
? normalizeForcedMinigameKeys(d.forcedMinigameKey)
: normalizeForcedMinigameKeys(prev.forcedMinigameKeys)),
testSpecialCardByMap: Object.prototype.hasOwnProperty.call(d, 'testSpecialCardByMap')
? normalizeTestSpecialCardByMap(d.testSpecialCardByMap)
: normalizeTestSpecialCardByMap(prev.testSpecialCardByMap),
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;
@@ -3285,10 +3384,14 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
} else if (awardOptions && Array.isArray(awardOptions.playerIds) && awardOptions.playerIds.length) {
awardIds = awardOptions.playerIds.filter((id) => space.peers.has(id));
}
/* Card 3 Ban — ผู้ที่ถูกแบนรอบนี้ไม่ได้เล่น จึงไม่ได้รับหลักฐาน */
if (space.bannedThisRunPlayerId) {
awardIds = awardIds.filter((id) => id !== space.bannedThisRunPlayerId);
}
/* Card 3 Ban
(เชคท id เดมตอนโหวต และ flag bannedSpectator ของ peer ใน play.html reconnect ใหม) */
awardIds = awardIds.filter((id) => {
if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return false;
const pp = space.peers.get(id);
if (pp && pp.bannedSpectator) return false;
return true;
});
/* ไม่มอบการ์ดเมื่อกลับ LobbyB โดยไม่ระบุ (เช่น refresh room-lobby ระหว่างมินิเกม) */
if (!space.detectiveAwardDoneForRun && awardIds.length) {
/* ผ่าน grant…CurrentRun เพื่อให้ set lastEvidenceGrant + ใช้ Card 7 Free Evidence + sync แฟ้ม (สำหรับหน้าเปิดเผยหลักฐาน) */
@@ -3327,6 +3430,16 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
detectiveReturn: true,
};
emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload);
/* Card 3 Ban (when: lobby_next): " LobbyB "
bannedPlayerId ใหเกมรอบถดไปกนไมใหคนทกโหวตเล */
const banPend = space.pendingSpecialCard;
console.log('[ban-debug] return->LobbyB: pendingSpecialCard=', banPend ? ('card' + banPend.cardId + ' consumed=' + banPend.consumed) : 'none', 'pickVote=', !!space.pickVote);
if (banPend && Number(banPend.cardId) === 3 && !banPend.consumed && !space.pickVote) {
banPend.consumed = true;
/* ตั้ง flag — เริ่มโหวตตอนผู้เล่น "rejoin เข้า room-lobby" (ตอน return ผู้เล่นกำลัง navigate play.html→room-lobby.html อยู่ event จะหาย) */
space.pendingBanVoteCard = banPend.card;
console.log('[ban-debug] set pendingBanVoteCard (จะเปิดโหวตตอน rejoin)');
}
}
/* ===== ขั้นพิจารณาคดี — โหวตชี้ตัวคนร้าย ===== */
@@ -6962,7 +7075,7 @@ io.on('connection', (socket) => {
if (qbAllAnswered(room)) qbReveal(room);
});
socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, playerKey, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex }, cb) => {
socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, playerKey, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex, bannedSpectator }, cb) => {
const space = spaces.get(spaceId);
if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' });
const maxPlayers = space.maxPlayers || 10;
@@ -7031,6 +7144,7 @@ io.on('connection', (socket) => {
balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
playerKey: (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '',
reportedMiniScore: 0, reportedMiniSurvived: true,
bannedSpectator: !!bannedSpectator,
};
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
const sn = quizBattleSpawnWorldFromJoinOrderServer(mdJoin, spawnJoinOrder);
@@ -7139,6 +7253,32 @@ io.on('connection', (socket) => {
if (space.peers.get(peerId)?.voiceMicOn) socket.emit('voice-init', { from: peerId, data: initData });
}
}
/* Card 3 Ban: LobbyB + Ban
(คน rejoin คนแรกเปนคนเปดโหวต; คนท rejoin หลงขณะโหวตเปดอย งสถานะใหเหนดวย) */
if (serverMapIsPostCaseLobbyB(space)) {
if (space.pendingBanVoteCard && !space.pickVote) {
const banCard = space.pendingBanVoteCard;
space.pendingBanVoteCard = null;
/* หน่วงให้ room-lobby ฝั่ง client โหลด/เซ็ตอัป UI เสร็จก่อน แล้วค่อยเปิดโหวต (ไม่งั้น overlay โดน reset/ทับตอนหน้ายังไม่พร้อม) */
setTimeout(() => {
const sp = spaces.get(spaceId);
if (!sp || sp.pickVote || !serverMapIsPostCaseLobbyB(sp)) return;
io.to(spaceId).emit('special-card-applied', { card: specialCardClientPayload(banCard), context: 'ban' });
startPlayerPickVote(spaceId, sp, 'ban', (targetId) => {
const sp2 = spaces.get(spaceId);
if (sp2) sp2.bannedPlayerId = targetId || null;
});
}, 2000);
} else if (space.pickVote && !space.pickVote.resolved) {
const pv = space.pickVote;
socket.emit('player-pick-vote-open', {
purpose: pv.purpose,
candidates: pv.candidates.map((c) => ({ id: c.id, nickname: c.nickname, isBot: c.isBot })),
endsAt: pv.endsAt,
durationMs: PICK_VOTE_MS,
});
}
}
});
/** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */
+1 -1
View File
@@ -43,6 +43,6 @@
<button type="button" class="loading-debug-freeze" id="btn-debug-freeze">Freeze: OFF</button>
</main>
<script src="../app-base.js"></script>
<script src="loading.js?v=0.03"></script>
<script src="loading.js?v=0.04"></script>
</body>
</html>
+53 -8
View File
@@ -8,6 +8,25 @@
function ap(p) {
return typeof appPath === 'function' ? appPath(p) : p;
}
/* asset หนัก/ใช้บ่อยของห้อง + 7 มินิเกม — preload ไว้ที่หน้านี้ครั้งเดียว เข้าเกม/ห้องภายหลังจะลื่น (ไม่ pop-in) */
var DEFAULT_PRELOAD_ASSETS = [
'/Game/img/QUESTION/score-bar.png',
'/Game/img/QUESTION/score-avartar.png',
'/Game/img/QUESTION/score.png',
'/Game/img/gauntlet-assets/popup-Howto.png',
'/Game/img/gauntlet-assets/Avartar.png',
'/Game/img/TowerBlock/admin-tab-minigame-3.png',
'/Game/img/quiz-carry/Avartar.png',
'/Game/img/quiz-carry/score-avartar.png',
'/Game/img/Jumper/popup-Howto.png',
'/Game/img/ViolentCrime/score-bar.png',
'/Game/img/ViolentCrime/Rocket-2.png',
'/Game/img/ViolentCrime/life-bar.png',
'/Game/img/MegaVirus/score-bar.png',
'/Game/img/MegaVirus/Artboard 9.png',
'/Game/img/MegaVirus/score-avartar.png'
];
if (!redirect) {
redirect = ap('/Main-Menu/');
}
@@ -127,17 +146,43 @@
return;
}
function tick(ts) {
/* ===== โหลด asset จริง → % จริง (แทน timer ปลอม) ===== */
var preloadList = (params.get('preload') || '')
.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
if (!preloadList.length) preloadList = DEFAULT_PRELOAD_ASSETS.slice();
preloadList = preloadList.map(ap);
var totalAssets = preloadList.length;
var doneAssets = 0;
var MIN_MS = 2500; /* โชว์ทิป + ไม่วูบเร็วไป */
var MAX_MS = 16000; /* safety: ไม่ค้างเกินแม้โหลดไม่ครบ */
var startMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
function nowMs() { return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); }
preloadList.forEach(function (url) {
var img = new Image();
var settled = false;
function one() { if (settled) return; settled = true; doneAssets += 1; }
img.onload = one;
img.onerror = one;
try { img.src = url; } catch (e) { one(); }
setTimeout(one, 9000); /* per-asset timeout กันไฟล์ค้าง */
});
function update() {
if (navigated) return;
if (startTs == null) startTs = ts;
var t = Math.min(1, (ts - startTs) / durationMs);
var p = Math.min(100, Math.floor(t * 100));
paintProgress(p);
if (t >= 1) {
var elapsed = nowMs() - startMs;
var frac = totalAssets ? (doneAssets / totalAssets) : 1;
var p = Math.floor(frac * 100);
/* โหลดเสร็จไวกว่า MIN_MS → ค้าง ~92-99% รอจนถึง MIN_MS กันกระพริบ */
if (p >= 100 && elapsed < MIN_MS) p = Math.max(92, Math.floor((elapsed / MIN_MS) * 100));
paintProgress(Math.max(0, Math.min(100, p)));
if ((doneAssets >= totalAssets && elapsed >= MIN_MS) || elapsed >= MAX_MS) {
paintProgress(100);
goLobby();
return;
}
requestAnimationFrame(tick);
requestAnimationFrame(update);
}
requestAnimationFrame(tick);
requestAnimationFrame(update);
})();