update all games and cards
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": "ผู้เล่น"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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/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()) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||