update desige 2 3 5 6 7and quiz battle

This commit is contained in:
2026-06-10 09:32:56 +00:00
parent 816538569d
commit 2f903a791c
47 changed files with 2668 additions and 289 deletions
+4
View File
@@ -2007,6 +2007,10 @@ code {
flex-direction: column;
gap: 0.65rem;
}
/* ตัวกรองหมวด: ซ่อนแถวที่ไม่ตรงหมวด (display:flex ด้านบนทับ [hidden] ของ UA ต้อง override) */
.qb-battle-block[hidden] {
display: none !important;
}
.qb-battle-block-head {
display: flex;
+4 -4
View File
@@ -2108,7 +2108,7 @@
}
}
return {
categoryId: catSel && catSel.value ? catSel.value : 'cybercrime',
categoryId: catSel && catSel.value ? catSel.value : 'topic1',
text: qInp && qInp.value ? String(qInp.value).trim() : '',
choices: [
a0 && a0.value ? String(a0.value).trim() : '',
@@ -2141,7 +2141,7 @@
'การใช้ WI-FI สาธารณะเพื่อดูข่าว',
'การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย',
];
hudCat.textContent = d ? qbBattleCategoryLabel(d.categoryId) : qbBattleCategoryLabel('cybercrime');
hudCat.textContent = d ? qbBattleCategoryLabel(d.categoryId) : qbBattleCategoryLabel('topic1');
qtext.textContent = d && d.text ? d.text : defQ;
var ch = d ? d.choices : defC;
for (var s = 0; s < 3; s++) {
@@ -2191,7 +2191,7 @@
var uid = 'r' + Math.random().toString(36).slice(2, 10);
var block = document.createElement('div');
block.className = 'qb-battle-block';
block.setAttribute('data-category', q.categoryId || 'cybercrime');
block.setAttribute('data-category', q.categoryId || 'topic1');
var head = document.createElement('div');
head.className = 'qb-battle-block-head';
@@ -2205,7 +2205,7 @@
var o = document.createElement('option');
o.value = c.id;
o.textContent = c.labelTh;
if ((q.categoryId || 'cybercrime') === c.id) o.selected = true;
if ((q.categoryId || 'topic1') === c.id) o.selected = true;
selCat.appendChild(o);
});
selCat.addEventListener('change', function () {
@@ -0,0 +1 @@
{"qbroom1":[{"name":"ผู้เล่น8916","score":4,"characterId":"char-1777017632279","ts":1781074083031},{"name":"ผู้เล่น2821","score":4,"characterId":"char-1777017632279","ts":1781079179108},{"name":"Q","score":0,"characterId":"char-1777017632279","ts":1781082837883}],"qbroom2":[{"name":"ผู้เล่น2612","score":1,"characterId":"char-1777017632279","ts":1781079252376}]}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+566 -31
View File
@@ -3155,6 +3155,19 @@
let quizBattleAnsweredComps = new Set();
let quizBattleModalCompId = null;
let qbAutoLastComp = null; // โดมล่าสุดที่ trigger อัตโนมัติ (กันเด้งซ้ำขณะยืนค้าง)
/** ประวัติคำตอบต่อโดม (สำหรับ MY RESULT): compId → { qIndex, firstTry(bool), correct(bool), submittedIdx, q } */
let quizBattleAnswerLog = new Map();
/** เวลาเริ่มเล่นห้อง quiz_battle (ms) — สำหรับ Total time */
let quizBattleSessionStartMs = 0;
let quizBattleScoreSubmittedAt = 0;
/** ล็อกตัวเลือกระหว่างโชว์ผลถูก/ผิด (กันกดซ้ำ + กันเด้งซ้ำ) */
let quizBattleChoiceLocked = false;
/** โชว์ popup Completed แล้วหรือยัง (กันเด้งซ้ำเมื่อตอบครบทุกโดม) */
let quizBattleCompletedShown = false;
/** โดมที่ "ผ่านแล้ว" (ตอบถูกหรือผิดก็ผ่าน) — ใช้ปลดล็อกเส้นทาง + จำว่าเคยตอบ ไม่ต้องตอบซ้ำ */
let quizBattlePassedComps = new Set();
/** ซูมที่ผู้เล่นปรับเองในฉาก quiz_battle (ล้อเมาส์ / ปุ่ม +−) */
let quizBattleUserZoomMul = 1;
const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd'];
document.getElementById('room-id').textContent = spaceId;
@@ -3204,6 +3217,10 @@
if (isQuizQuestionMissionUiMapPlay()) {
wrap.classList.add('is-hidden');
}
/* Quiz Battle: ใช้ HUD ใหม่ (#qb-game-hud) — ซ่อนกล่อง "คะแนน" เดิม */
if (isQuizBattle()) {
wrap.classList.add('is-hidden');
}
}
function initPlayLiveQuizScoresZeros() {
@@ -5469,6 +5486,8 @@
const mwPx = w * tileSize;
const mhPx = h * tileSize;
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
} else if (isQuizBattle()) {
zDraw *= quizBattleUserZoomMul;
}
if (isStackTowerEmbedZoomLockedPlay()) {
playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL;
@@ -9834,6 +9853,80 @@
}
}
/** Quiz Battle (qbroom*) เลขห้องจาก mapId → tag/หมวด */
function quizBattleRoomNoPlay() {
const m = String(currentPlayMapId() || '').match(/qbroom(\d+)/i);
return m ? Math.max(1, Math.min(10, parseInt(m[1], 10))) : 0;
}
/**
* Quiz Battle in-game HUD overlay #qb-game-hud (design plane 1920×1080 scale ตาม stage)
* header หมวด (tag-NN), PLAYERS x/50, SCORE (ตอบถ), host avatar. EXIT/MY RESULT/RANKING wire แยก
*/
function syncQuizBattleHudPlay() {
const root = document.getElementById('qb-game-hud');
if (!root) return;
const on = !!(mapData && isQuizBattle());
root.classList.toggle('is-hidden', !on);
root.setAttribute('aria-hidden', on ? 'false' : 'true');
if (!on) return;
/* ซ่อนกล่อง dev instruction ('QUIZ BATTLE · โหลดคำถามแล้ว...') มุมซ้ายล่าง — ไม่มีใน design */
const qgOv = document.getElementById('quiz-game-overlay');
if (qgOv && !qgOv.classList.contains('is-hidden')) qgOv.classList.add('is-hidden');
const stack = document.getElementById('play-canvas-stack');
if (stack) {
const r = stack.getBoundingClientRect();
const cw = Math.max(1, r.width || 0);
const ch = Math.max(1, r.height || 0);
const s = Math.min(cw / 1920, ch / 1080);
if (s > 0 && isFinite(s)) {
const left = Math.round(r.left + (cw - 1920 * s) / 2);
const top = Math.round(r.top + (ch - 1080 * s) / 2);
const key = left + ',' + top + ',' + s.toFixed(4);
if (root.dataset.box !== key) {
root.dataset.box = key;
root.style.left = left + 'px';
root.style.top = top + 'px';
root.style.transform = 'scale(' + s + ')';
}
}
}
const roomNo = quizBattleRoomNoPlay();
const tag = document.getElementById('qb-hud-tag');
if (tag && roomNo > 0) {
const want = BASE + '/img/qb-ui/tag-' + (roomNo < 10 ? '0' + roomNo : String(roomNo)) + '.png';
if (tag.dataset.src !== want) { tag.dataset.src = want; tag.src = want; }
}
const pv = document.getElementById('qb-hud-players-val');
if (pv) {
const n = countPlayHumans();
const max = (mapData && mapData.maxPlayers) || 50;
const txt = n + '/' + max;
if (pv.textContent !== txt) pv.textContent = txt;
}
const sv = document.getElementById('qb-hud-score-val');
if (sv) {
const sc = quizBattleAnsweredComps ? quizBattleAnsweredComps.size : 0;
if (sv.textContent !== String(sc)) sv.textContent = String(sc);
}
const av = document.getElementById('qb-hud-host-av');
if (av) {
const cid = me.characterId || getPlayCharacterId();
if (cid && av.dataset.cid !== String(cid)) {
av.dataset.cid = String(cid);
/* full-body down avatar (เหมือนหน้า Quiz-Battle/.qb-room-profile-avatar) clip โชว์แค่ส่วนบน */
ensurePlayLobbyIdleAvatarImage(cid);
const src = playLobbyIdleAvatarSrcForCharacter(cid);
if (src) {
av.src = src;
} else {
const im = getCharacterImg(cid, 'down');
if (im && im.src) av.src = im.src;
}
}
}
}
function resetQuizCarryPlayState() {
cancelQuizCarrySessionCompleteResultToSummary();
const qcmo = document.getElementById('quiz-carry-mission-overlay');
@@ -13649,6 +13742,7 @@
ov.classList.add('is-hidden');
ov.setAttribute('aria-hidden', 'true');
}
quizBattleChoiceLocked = false;
}
function flashQuizBattleFeedback(text, ok) {
@@ -13725,20 +13819,30 @@
const ov = document.getElementById('quiz-battle-mcq-overlay');
const textEl = document.getElementById('quiz-battle-mcq-text');
if (!ov || !textEl || !q) return;
const titleEl = document.getElementById('quiz-battle-mcq-title');
if (titleEl) titleEl.textContent = 'QUESTION #' + (quizBattleModalCompId || '');
textEl.textContent = q.text || '';
const labels = ['A', 'B', 'C'];
for (let i = 0; i < 3; i++) {
const btn = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]');
if (btn) {
const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : '';
btn.textContent = labels[i] + (ch ? (': ' + ch.slice(0, 96)) : '');
}
if (!btn) continue;
const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : '';
const letterEl = btn.querySelector('.qbc-letter');
const txtEl = btn.querySelector('.qbc-text');
const markEl = btn.querySelector('.qbc-mark');
if (letterEl) letterEl.textContent = labels[i];
if (txtEl) txtEl.textContent = ch;
if (markEl) { markEl.removeAttribute('src'); markEl.alt = ''; }
btn.classList.remove('is-correct', 'is-wrong', 'is-locked');
btn.disabled = false;
}
quizBattleChoiceLocked = false;
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
}
function trySubmitQuizBattleChoice(idx) {
if (quizBattleChoiceLocked) return;
if (quizBattleModalCompId == null || !mapData || !isQuizBattle()) return;
const compId = quizBattleModalCompId;
const pool = quizBattleMcqPool;
@@ -13751,6 +13855,34 @@
const q = pool[(compId - 1) % pool.length];
if (!q) return;
const ok = Number(q.correctIndex) === Number(idx);
/* ตอบครั้งเดียวจบ — ผิดก็ "ผ่าน" และจำว่าผิด (ไม่ให้ตอบซ้ำ) · log ใช้ทำ MY RESULT (O/X) + accuracy */
quizBattleAnswerLog.set(compId, {
qIndex: (compId - 1) % pool.length,
firstTry: ok,
correct: ok,
submittedIdx: idx,
q: q,
});
quizBattlePassedComps.add(compId); /* ผ่านโดมนี้แล้ว (ถูกหรือผิดก็ผ่าน — ปลดล็อกเส้นทาง) */
/* โชว์ผลถูก/ผิดบนตัวเลือกทันที (q-correct.png / q-wrong.png) แล้วล็อกกันกดซ้ำ */
quizBattleChoiceLocked = true;
const ov = document.getElementById('quiz-battle-mcq-overlay');
if (ov) {
ov.querySelectorAll('.quiz-battle-choice').forEach((b) => { b.classList.add('is-locked'); b.disabled = true; });
const setMark = (i, kind) => {
const b = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]');
if (!b) return;
b.classList.add(kind === 'correct' ? 'is-correct' : 'is-wrong');
const m = b.querySelector('.qbc-mark');
if (m) m.src = BASE + '/img/qb-ui/' + (kind === 'correct' ? 'q-correct.png' : 'q-wrong.png');
};
if (ok) {
setMark(idx, 'correct');
} else {
setMark(idx, 'wrong');
setMark(Number(q.correctIndex), 'correct'); /* เฉลยคำตอบที่ถูก */
}
}
if (ok) {
quizBattleAnsweredComps.add(compId);
if (typeof socket !== 'undefined' && socket && socket.connected) {
@@ -13761,12 +13893,343 @@
playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1;
renderPlayQuizScoreboard(playLiveQuizScores);
}
submitQuizBattleLeaderboardScore();
flashQuizBattleFeedback('ถูกต้อง · +1 · ทางไปข้างหน้าเปิดแล้ว', true);
} else {
flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false);
flashQuizBattleFeedback('ตอบผิด — ผ่านได้เลย (บันทึกว่าผิด) ไม่ต้องตอบซ้ำ', false);
}
quizBattleModalCompId = null;
hideQuizBattleMcqModal();
saveQuizBattleProgress();
/* ปิด popup แล้วเช็คจบ (ทั้งถูกและผิด) — ผิดหน่วงนานขึ้นให้เห็นเฉลยก่อน */
setTimeout(() => {
quizBattleModalCompId = null;
hideQuizBattleMcqModal();
const totalComps = quizBattleTotalCompsPlay();
if (!quizBattleCompletedShown && totalComps > 0 && quizBattlePassedComps.size >= totalComps) {
quizBattleCompletedShown = true;
setTimeout(openQuizBattleCompleted, 280);
}
}, ok ? 950 : 1300);
}
/**
* อผเลนสำหร leaderboard/ranking กกบชอทงใน Main-Lobby
* (localStorage 'roomLobbyDisplayName' 'playerName' าน readStoredDisplayName / jdReadDisplayName)
* อน fallback ไป me.nickname / ?nick= เพอใหตรงกบโปรไฟลเดยวกนทกหน
*/
function quizBattlePlayerDisplayName() {
let stored = '';
try {
if (typeof window.jdReadDisplayName === 'function') {
const v = String(window.jdReadDisplayName() || '').trim();
if (v && v !== 'MONE') stored = v; /* 'MONE' = ค่า default เมื่อยังไม่ตั้งชื่อ → ไม่ถือว่าตั้งแล้ว */
}
} catch (e) { /* ignore */ }
if (!stored) stored = readStoredDisplayName();
const name = stored || (me && me.nickname) || nick || 'ผู้เล่น';
return String(name || 'ผู้เล่น').trim().slice(0, 24) || 'ผู้เล่น';
}
/** ส่งคะแนน (จำนวนตอบถูก) ขึ้น global leaderboard ของห้อง/หมวด */
function submitQuizBattleLeaderboardScore() {
if (!isQuizBattle() || !socket || !socket.connected) return;
const roomNo = quizBattleRoomNoPlay();
if (roomNo <= 0) return;
const score = quizBattleAnsweredComps ? quizBattleAnsweredComps.size : 0;
try {
socket.emit('qb-leaderboard-submit', {
roomId: 'qbroom' + roomNo,
name: quizBattlePlayerDisplayName(),
score: score,
characterId: me.characterId || getPlayCharacterId() || '',
});
} catch (e) { /* ignore */ }
quizBattleScoreSubmittedAt = Date.now();
}
/* ===== Quiz Battle: progress persistence (resume ต่อจากเดิม) ===== */
function quizBattleProgressKey() {
const roomNo = quizBattleRoomNoPlay();
const roomId = 'qbroom' + (roomNo > 0 ? roomNo : 1);
return 'qbProgress:' + roomId + ':' + quizBattlePlayerDisplayName();
}
function saveQuizBattleProgress() {
if (!isQuizBattle()) return;
try {
const passed = [];
quizBattleAnswerLog.forEach((v, c) => {
passed.push({ c: c, qi: v.qIndex, ft: v.firstTry ? 1 : 0, ok: v.correct ? 1 : 0, idx: v.submittedIdx });
});
const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
const elapsed = quizBattleSessionStartMs ? (nowMs - quizBattleSessionStartMs) : 0;
const data = {
passed: passed,
pos: (me && Number.isFinite(me.x) && Number.isFinite(me.y)) ? { x: me.x, y: me.y } : null,
elapsed: Math.max(0, Math.round(elapsed)),
ts: Date.now(),
};
localStorage.setItem(quizBattleProgressKey(), JSON.stringify(data));
} catch (e) { /* ignore */ }
}
function loadQuizBattleProgress() {
try {
const raw = localStorage.getItem(quizBattleProgressKey());
return raw ? JSON.parse(raw) : null;
} catch (e) { return null; }
}
/** คืนสถานะที่เคยเล่น (โดมที่ผ่าน/คำตอบ/เวลา) + ตำแหน่งล่าสุด — เรียกใน setupPlayQuizBattleUi */
function restoreQuizBattleProgress() {
const data = loadQuizBattleProgress();
if (!data || !Array.isArray(data.passed)) return;
data.passed.forEach((p) => {
if (!p || !(p.c > 0)) return;
quizBattleAnswerLog.set(p.c, {
qIndex: (p.qi != null ? p.qi : 0),
firstTry: !!p.ft,
correct: !!p.ok,
submittedIdx: p.idx,
q: null, /* q ผูกใหม่จาก pool ตอนเปิด MY RESULT */
});
quizBattlePassedComps.add(p.c);
if (p.ok) quizBattleAnsweredComps.add(p.c);
});
/* เวลาเดินต่อ (offset session start ด้วยเวลาที่เคยใช้) */
if (data.elapsed > 0) {
const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
quizBattleSessionStartMs = nowMs - data.elapsed;
}
/* sync score HUD + leaderboard ให้ตรงกับที่เคยทำ */
if (myId != null) {
if (!playLiveQuizScores) playLiveQuizScores = {};
playLiveQuizScores[myId] = quizBattleAnsweredComps.size;
renderPlayQuizScoreboard(playLiveQuizScores);
}
/* ย้ายตัวไปจุดเดิม (ถ้ายังเดินได้) แล้วบอกเซิร์ฟเวอร์ */
if (data.pos && Number.isFinite(data.pos.x) && Number.isFinite(data.pos.y)) {
const px = data.pos.x, py = data.pos.y;
setTimeout(() => {
try {
if (!isQuizBattle() || !me) return;
if (typeof canWalkLikeLobby === 'function' && !canWalkLikeLobby(px, py)) return;
me.x = px; me.y = py;
if (socket && socket.connected) socket.emit('move', { x: me.x, y: me.y, direction: me.direction });
draw();
} catch (e) { /* ignore */ }
}, 350);
}
}
/* ===== Quiz Battle: MY RESULT + RANKING popups (Stage 2) ===== */
function qbEscHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function qbFmtDuration(ms) {
const s = Math.max(0, Math.round((ms || 0) / 1000));
const m = Math.floor(s / 60), ss = s % 60;
if (m >= 60) { const h = Math.floor(m / 60); return h + 'h ' + (m % 60) + 'm'; }
if (m > 0) return m + 'm ' + ss + 's';
return ss + ' s';
}
/** จำนวนโดม (สถานีคำถาม) ทั้งหมดของแมป = ค่า comp สูงสุดใน quizBattleDomeComp */
function quizBattleTotalCompsPlay() {
const md = mapData;
if (md && md.quizBattleDomeComp && Array.isArray(md.quizBattleDomeComp)) {
let mx = 0;
const dc = md.quizBattleDomeComp;
for (let y = 0; y < dc.length; y++) {
const row = dc[y]; if (!row) continue;
for (let x = 0; x < row.length; x++) { const v = row[x] | 0; if (v > mx) mx = v; }
}
if (mx > 0) return mx;
}
return quizBattleMcqPool.length || 0;
}
function qbSetText(id, txt) { const el = document.getElementById(id); if (el) el.textContent = txt; }
function qbAvatarSrcForCharacter(cid) {
if (!cid) return '';
try { ensurePlayLobbyIdleAvatarImage(cid); } catch (e) { /* ignore */ }
const src = playLobbyIdleAvatarSrcForCharacter(cid);
if (src) return src;
const im = getCharacterImg(cid, 'down');
return (im && im.src) ? im.src : '';
}
function openQuizBattleMyResult() {
if (!isQuizBattle()) return;
const pop = document.getElementById('qb-myresult-popup');
if (!pop) return;
const total = quizBattleTotalCompsPlay();
const passed = quizBattlePassedComps ? quizBattlePassedComps.size : 0;
let firstCorrect = 0, attempted = 0;
quizBattleAnswerLog.forEach((v) => { attempted++; if (v.firstTry) firstCorrect++; });
const accPct = attempted ? Math.round((firstCorrect / attempted) * 100) : 0;
const progPct = total ? Math.round((passed / total) * 100) : 0;
const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
const elapsed = quizBattleSessionStartMs ? (nowMs - quizBattleSessionStartMs) : 0;
qbSetText('qb-mr-accuracy', accPct + '%');
qbSetText('qb-mr-accuracy-sub', '(' + firstCorrect + '/' + attempted + ')');
qbSetText('qb-mr-progress', progPct + '%');
qbSetText('qb-mr-progress-sub', '(' + passed + '/' + total + ')');
qbSetText('qb-mr-time', qbFmtDuration(elapsed));
const grid = document.getElementById('qb-mr-grid');
if (grid) {
const cells = Math.max(total, attempted, 0);
let html = '';
for (let c = 1; c <= cells; c++) {
const log = quizBattleAnswerLog.get(c);
let cls = 'qb-mr-cell', mark = '';
if (!log) { cls += ' is-unanswered'; }
else if (log.correct) { cls += ' is-correct'; mark = '<img class="qb-mr-mark" src="' + BASE + '/img/qb-ui/q-correct.png" alt="O">'; }
else { cls += ' is-wrong'; mark = '<img class="qb-mr-mark" src="' + BASE + '/img/qb-ui/q-wrong.png" alt="X">'; }
html += '<div class="' + cls + '"><span class="qb-mr-n">' + c + '</span>' + mark + '</div>';
}
grid.innerHTML = html;
}
const det = document.getElementById('qb-mr-detail');
if (det) {
const entries = Array.from(quizBattleAnswerLog.entries()).sort((a, b) => a[0] - b[0]);
if (!entries.length) {
det.innerHTML = '<div class="qb-rank-empty">ยังไม่ได้ตอบคำถาม — เดินเข้าโดมแล้วกด E เพื่อเริ่ม</div>';
} else {
const letters = ['A', 'B', 'C'];
let html = '';
entries.forEach(function (pair) {
const cid = pair[0], log = pair[1];
const q = log.q || quizBattleMcqPool[log.qIndex] || {};
const choices = Array.isArray(q.choices) ? q.choices : [];
const markImg = log.correct ? 'q-correct.png' : 'q-wrong.png';
html += '<div class="qb-mr-q">';
html += '<div class="qb-mr-q-head"><img src="' + BASE + '/img/qb-ui/' + markImg + '" alt=""> ข้อ ' + cid + '</div>';
html += '<div class="qb-mr-q-text">' + qbEscHtml(q.text || '') + '</div>';
choices.forEach(function (ch, i) {
let ocls = 'qb-mr-q-opt';
if (i === Number(q.correctIndex)) ocls += ' is-correct';
else if (i === Number(log.submittedIdx) && !log.correct) ocls += ' is-chosen-wrong';
html += '<div class="' + ocls + '">' + letters[i] + '. ' + qbEscHtml(ch) + '</div>';
});
html += '</div>';
});
det.innerHTML = html;
}
det.scrollTop = 0;
}
pop.classList.remove('is-hidden');
pop.setAttribute('aria-hidden', 'false');
}
function renderQuizBattleRanking(data) {
const top = (data && Array.isArray(data.top)) ? data.top : [];
const meRow = data && data.me ? data.me : null;
const podium = document.getElementById('qb-rank-podium');
if (podium) {
const order = [1, 0, 2]; /* แสดง 2-1-3 */
let html = '';
order.forEach(function (idx) {
const e = top[idx];
if (!e) return;
const rank = idx + 1;
const av = qbAvatarSrcForCharacter(e.characterId);
html += '<div class="qb-rank-pod qb-rank-pod-' + rank + '">';
html += '<div class="qb-rank-pod-av">' + (av ? '<img src="' + qbEscHtml(av) + '" alt="">' : '') + '</div>';
html += '<div class="qb-rank-pod-badge">' + rank + '</div>';
html += '<div class="qb-rank-pod-name">' + qbEscHtml(e.name || '-') + '</div>';
html += '<div class="qb-rank-pod-score">' + (e.score | 0) + '</div>';
html += '</div>';
});
podium.innerHTML = html || '<div class="qb-rank-empty">ยังไม่มีคะแนน</div>';
}
const list = document.getElementById('qb-rank-list');
if (list) {
let html = '';
for (let i = 3; i < top.length && i < 50; i++) {
const e = top[i];
html += '<div class="qb-rank-row"><span class="qb-rank-c1">' + (e.rank || (i + 1)) + '</span>'
+ '<span class="qb-rank-c2">' + qbEscHtml(e.name || '-') + '</span>'
+ '<span class="qb-rank-c3">' + (e.score | 0) + '</span></div>';
}
list.innerHTML = html;
}
const meBox = document.getElementById('qb-rank-me');
if (meBox) {
if (meRow && (meRow.rank || 0) > 0) {
meBox.innerHTML = '<div class="qb-rank-row"><span class="qb-rank-c1">' + (meRow.rank || '-') + '</span>'
+ '<span class="qb-rank-c2">' + qbEscHtml(meRow.name || 'คุณ') + ' (คุณ)</span>'
+ '<span class="qb-rank-c3">' + (meRow.score | 0) + '</span></div>';
} else {
meBox.innerHTML = '';
}
}
}
function openQuizBattleRanking() {
if (!isQuizBattle()) return;
const pop = document.getElementById('qb-ranking-popup');
if (!pop) return;
/* ส่งคะแนนล่าสุดก่อน เพื่อให้อันดับตัวเองเป็นปัจจุบัน */
try { submitQuizBattleLeaderboardScore(); } catch (e) { /* ignore */ }
const roomNo = quizBattleRoomNoPlay();
const roomId = 'qbroom' + (roomNo > 0 ? roomNo : 1);
const myName = quizBattlePlayerDisplayName();
const list = document.getElementById('qb-rank-list');
if (list) list.innerHTML = '<div class="qb-rank-empty">กำลังโหลดอันดับ…</div>';
const podium = document.getElementById('qb-rank-podium');
if (podium) podium.innerHTML = '';
const meBox = document.getElementById('qb-rank-me');
if (meBox) meBox.innerHTML = '';
pop.classList.remove('is-hidden');
pop.setAttribute('aria-hidden', 'false');
if (socket && socket.connected) {
try {
socket.emit('qb-leaderboard-get', { roomId: roomId, name: myName }, function (resp) {
renderQuizBattleRanking(resp || {});
});
} catch (e) {
if (list) list.innerHTML = '<div class="qb-rank-empty">โหลดอันดับไม่สำเร็จ</div>';
}
} else if (list) {
list.innerHTML = '<div class="qb-rank-empty">ยังไม่ได้เชื่อมต่อเซิร์ฟเวอร์</div>';
}
}
function closeQuizBattlePopup(which) {
let id = 'qb-myresult-popup';
if (which === 'ranking') id = 'qb-ranking-popup';
else if (which === 'completed') id = 'qb-completed-popup';
const pop = document.getElementById(id);
if (!pop) return;
pop.classList.add('is-hidden');
pop.setAttribute('aria-hidden', 'true');
}
/** popup "Completed" (popup-result.png) — สรุป Correct / Time Taken / My Rank เมื่อตอบครบทุกโดม */
function openQuizBattleCompleted() {
if (!isQuizBattle()) return;
const pop = document.getElementById('qb-completed-popup');
if (!pop) return;
const total = quizBattleTotalCompsPlay();
let firstCorrect = 0;
quizBattleAnswerLog.forEach((v) => { if (v.firstTry) firstCorrect++; });
const nowMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
const elapsed = quizBattleSessionStartMs ? (nowMs - quizBattleSessionStartMs) : 0;
qbSetText('qb-cp-correct', firstCorrect + ' / ' + total);
qbSetText('qb-cp-time', qbFmtDuration(elapsed));
qbSetText('qb-cp-rank', '—');
/* ส่งคะแนนล่าสุด แล้วดึงอันดับมาแสดง My Rank */
try { submitQuizBattleLeaderboardScore(); } catch (e) { /* ignore */ }
const roomNo = quizBattleRoomNoPlay();
const roomId = 'qbroom' + (roomNo > 0 ? roomNo : 1);
const myName = quizBattlePlayerDisplayName();
if (socket && socket.connected) {
try {
socket.emit('qb-leaderboard-get', { roomId: roomId, name: myName }, function (resp) {
const rank = resp && resp.me && resp.me.rank ? ('#' + resp.me.rank) : '—';
qbSetText('qb-cp-rank', rank);
});
} catch (e) { /* ignore */ }
}
pop.classList.remove('is-hidden');
pop.setAttribute('aria-hidden', 'false');
}
/** หา comp โดมที่ตัวละครเหยียบอยู่ — เช็คหลายจุด + ช่องติดโดม ให้เด้งง่ายเมื่อถึงสถานี */
@@ -13790,7 +14253,7 @@
if (modalOpen) return;
if (compNow === qbAutoLastComp) return;
qbAutoLastComp = compNow;
if (quizBattleAnsweredComps.has(compNow)) return;
if (quizBattlePassedComps.has(compNow)) return;
const pool = quizBattleMcqPool;
if (!pool.length) {
flashQuizBattleFeedback('ยังไม่มีคำถาม — บันทึกใน Admin → Quiz Battle แล้วรีเฟรช', false);
@@ -13812,22 +14275,28 @@
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
/** หีบบนสถานีคำถาม — done=ปลดล็อก(เขียว+ติ๊ก) / ยังไม่ตอบ=ล็อก(น้ำตาล+กุญแจ) + เลขสถานี */
function drawQuizBattleChest(ctx, cx, cy, size, done, num) {
/** หีบบนสถานีคำถาม — ตอบถูก=เขียว+ติ๊ก / ตอบผิด(ผ่านแล้ว)=แดง+กากบาท / ยังไม่ตอบ=ล็อก(น้ำตาล+กุญแจ) + เลขสถานี */
function drawQuizBattleChest(ctx, cx, cy, size, done, num, wrong) {
const w = size * 0.84, h = size * 0.64;
const x0 = cx - w / 2, y0 = cy - h / 2;
ctx.save();
ctx.shadowColor = done ? 'rgba(52,211,153,0.95)' : 'rgba(255,200,70,0.95)';
ctx.shadowColor = done ? (wrong ? 'rgba(248,113,128,0.95)' : 'rgba(52,211,153,0.95)') : 'rgba(255,200,70,0.95)';
ctx.shadowBlur = Math.max(4, size * 0.3);
ctx.fillStyle = done ? '#3f7d57' : '#8a5a2b';
ctx.fillStyle = done ? (wrong ? '#8a3340' : '#3f7d57') : '#8a5a2b';
qbRoundRect(ctx, x0, y0 + h * 0.34, w, h * 0.66, size * 0.08); ctx.fill();
ctx.fillStyle = done ? '#57a875' : '#a86f33';
ctx.fillStyle = done ? (wrong ? '#b3455a' : '#57a875') : '#a86f33';
qbRoundRect(ctx, x0, y0, w, h * 0.44, size * 0.1); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = done ? '#d6f5e3' : '#f1c66a';
ctx.fillStyle = done ? (wrong ? '#ffd6dc' : '#d6f5e3') : '#f1c66a';
ctx.fillRect(x0, y0 + h * 0.30, w, h * 0.10);
ctx.fillRect(cx - size * 0.045, y0, size * 0.09, h);
if (done) {
if (done && wrong) {
ctx.strokeStyle = '#fff0f2'; ctx.lineWidth = Math.max(2, size * 0.08); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(cx - size * 0.12, cy - size * 0.10); ctx.lineTo(cx + size * 0.12, cy + size * 0.12);
ctx.moveTo(cx + size * 0.12, cy - size * 0.10); ctx.lineTo(cx - size * 0.12, cy + size * 0.12);
ctx.stroke();
} else if (done) {
ctx.strokeStyle = '#eafff3'; ctx.lineWidth = Math.max(2, size * 0.08); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(cx - size * 0.13, cy + size * 0.04);
@@ -13859,8 +14328,8 @@
flashQuizBattleFeedback('เข้าใกล้สถานีคำถาม (หีบ) แล้วกด E', false);
return;
}
if (quizBattleAnsweredComps.has(compId)) {
flashQuizBattleFeedback('ตอบโดมนี้ถูกแล้ว', false);
if (quizBattlePassedComps.has(compId)) {
flashQuizBattleFeedback('โดมนี้ผ่านแล้ว', false);
return;
}
const pool = quizBattleMcqPool;
@@ -13925,6 +14394,7 @@
function resetQuizBattlePlayState() {
quizBattleMcqPool = [];
quizBattleAnsweredComps = new Set();
quizBattlePassedComps = new Set();
quizBattleModalCompId = null;
hideQuizBattleMcqModal();
}
@@ -13953,13 +14423,20 @@
: 'คำถามจาก Admin → Quiz Battle (battleQuizMcq) · ช่องโดมติดกัน = 1 ข้อ · ถูกแล้วได้ +1 ต่อโดม';
}
quizBattleAnsweredComps = new Set();
quizBattleAnswerLog = new Map();
quizBattlePassedComps = new Set();
quizBattleUserZoomMul = 1;
quizBattleSessionStartMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
quizBattleModalCompId = null;
quizBattleMcqPool = [];
quizBattleCompletedShown = false;
hideQuizBattleMcqModal();
playLiveQuizScores = {};
if (myId != null) playLiveQuizScores[myId] = 0;
others.forEach((_, id) => { playLiveQuizScores[id] = 0; });
renderPlayQuizScoreboard(playLiveQuizScores);
/* คืนสถานะที่เคยเล่น (โดมที่ผ่าน/ตำแหน่งล่าสุด) — เล่นต่อจากเดิมไม่ต้องตอบซ้ำ */
restoreQuizBattleProgress();
loadQuizBattleMcqPool().then(() => {
const q2 = document.getElementById('quiz-game-question');
if (!q2) return;
@@ -18161,22 +18638,32 @@
const stack = document.getElementById('play-canvas-stack');
if (!stack) return;
const wheelZoomHandler = (e) => {
if (!previewMode || !editorEmbedReturn || !mapData) return;
if (!mapData) return;
const embedZoom = previewMode && editorEmbedReturn;
const qbZoom = isQuizBattle();
if (!embedZoom && !qbZoom) return;
if (isStackTowerEmbedZoomLockedPlay()) return;
if (isQuizQuestionMissionHudActivePlay()) return;
const t = e.target;
if (t && typeof t.closest === 'function') {
if (t.closest('button, input, textarea, select, a[href]')) return;
}
/* ล้อธรรม / trackpad pinch มักไม่มี ctrl — ให้ซูมได้ใน embed; Ctrl/⌘+ล้อยังใช้ได้ */
/* ล้อธรรม / trackpad pinch มักไม่มี ctrl — ให้ซูมได้; Ctrl/⌘+ล้อยังใช้ได้ */
e.preventDefault();
e.stopPropagation();
const raw = e.deltaMode === 1 ? e.deltaY * 14 : e.deltaMode === 2 ? e.deltaY * 320 : e.deltaY;
const k = Math.exp(-raw * 0.002);
playEmbedUserZoomMul = Math.max(
PLAY_EMBED_USER_ZOOM_MIN,
Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * k),
);
if (qbZoom) {
quizBattleUserZoomMul = Math.max(
PLAY_EMBED_USER_ZOOM_MIN,
Math.min(PLAY_EMBED_USER_ZOOM_MAX, quizBattleUserZoomMul * k),
);
} else {
playEmbedUserZoomMul = Math.max(
PLAY_EMBED_USER_ZOOM_MIN,
Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * k),
);
}
draw();
};
stack.addEventListener('wheel', wheelZoomHandler, { passive: false, capture: true });
@@ -18532,8 +19019,8 @@
if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false;
if (isQuizBattle() && quizBattlePathModeActive(mapData)) {
if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false;
if (!quizBattlePathGateAllowsPlay(mapData, x, y, quizBattleAnsweredComps)) {
quizBattleFlashGateBlocked(mapData, x, y, quizBattleAnsweredComps);
if (!quizBattlePathGateAllowsPlay(mapData, x, y, quizBattlePassedComps)) {
quizBattleFlashGateBlocked(mapData, x, y, quizBattlePassedComps);
return false;
}
}
@@ -18585,7 +19072,7 @@
if (isQuizBattle() && quizBattlePathModeActive(mapData)) {
if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false;
if (o && isPreviewBotId(o.id || o.peerId)) { /* preview bots ไม่ล็อกประตู */ }
else if (!quizBattlePathGateAllowsPlay(mapData, x, y, quizBattleAnsweredComps)) return false;
else if (!quizBattlePathGateAllowsPlay(mapData, x, y, quizBattlePassedComps)) return false;
}
return true;
}
@@ -20504,6 +20991,9 @@
camX = qmc.cx;
camY = qmc.cy;
}
} else if (isQuizBattle()) {
/* บอร์ดควิซแนวตั้ง (20×40) — ล็อกกล้องกลางแนวนอน (ไม่ไถลซ้าย/ขวาตามผู้เล่น) เลื่อนตามแนวตั้งเท่านั้น ตรง mock */
camX = (w * tileSize) / 2;
}
const gauntletGroupCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zDraw);
if (gauntletGroupCam) {
@@ -20779,8 +21269,9 @@
const upSame = cid > 0 && comp && comp[y - 1] && comp[y - 1][x] === cid;
const leftSame = cid > 0 && comp && comp[y] && comp[y][x - 1] === cid;
if (!upSame && !leftSame) {
const done = cid > 0 && quizBattleAnsweredComps.has(cid);
drawQuizBattleChest(ctx, sx + size / 2, sy + size / 2, size, done, cid || 0);
const passed = cid > 0 && quizBattlePassedComps.has(cid);
const wrong = passed && !quizBattleAnsweredComps.has(cid);
drawQuizBattleChest(ctx, sx + size / 2, sy + size / 2, size, passed, cid || 0, wrong);
}
}
if (isStack()) {
@@ -21405,6 +21896,7 @@
syncStackTowerDropButtonPlay();
syncStackTowerMissionDomHudPlay();
syncSpaceShooterLifeDomHudPlay();
syncQuizBattleHudPlay();
}
document.addEventListener('keydown', (e) => {
@@ -21817,14 +22309,57 @@
window.location.replace(playDesignMainMenuHref());
}
});
(function wireQuizBattleHudButtons() {
/* EXIT → ออกห้อง กลับหน้าเลือกห้อง Quiz Battle */
const exitBtn = document.getElementById('qb-hud-exit');
if (exitBtn) {
exitBtn.addEventListener('click', () => {
try { saveQuizBattleProgress(); } catch (e) { /* ignore */ }
try { socket.emit('leave-space'); } catch (e) { /* ignore */ }
let href = '../Quiz-Battle/';
try { if (typeof appPath === 'function') href = appPath('/Quiz-Battle/'); } catch (e2) { /* ignore */ }
window.location.href = href;
});
}
/* MY RESULT → popup ผลตอบของฉัน */
const myResultBtn = document.getElementById('qb-hud-myresult');
if (myResultBtn) myResultBtn.addEventListener('click', function () { openQuizBattleMyResult(); });
/* RANKING → popup อันดับ (global leaderboard) */
const rankingBtn = document.getElementById('qb-hud-ranking');
if (rankingBtn) rankingBtn.addEventListener('click', function () { openQuizBattleRanking(); });
/* ZOOM เข้า/ออก ในฉาก */
const applyQbZoom = function (mul) {
quizBattleUserZoomMul = Math.max(PLAY_EMBED_USER_ZOOM_MIN, Math.min(PLAY_EMBED_USER_ZOOM_MAX, quizBattleUserZoomMul * mul));
try { draw(); } catch (e) { /* ignore */ }
};
const zoomInBtn = document.getElementById('qb-hud-zoom-in');
if (zoomInBtn) zoomInBtn.addEventListener('click', function () { applyQbZoom(1.2); });
const zoomOutBtn = document.getElementById('qb-hud-zoom-out');
if (zoomOutBtn) zoomOutBtn.addEventListener('click', function () { applyQbZoom(1 / 1.2); });
/* ปิด popup: ปุ่ม X / backdrop (data-qb-close) */
['qb-myresult-popup', 'qb-ranking-popup', 'qb-completed-popup'].forEach(function (pid) {
const pop = document.getElementById(pid);
if (!pop) return;
pop.addEventListener('click', function (e) {
const t = e.target;
if (t && t.getAttribute && t.getAttribute('data-qb-close')) {
closeQuizBattlePopup(t.getAttribute('data-qb-close'));
}
});
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { closeQuizBattlePopup('myresult'); closeQuizBattlePopup('ranking'); closeQuizBattlePopup('completed'); }
});
})();
(function wireQuizBattleMcqModal() {
const ov = document.getElementById('quiz-battle-mcq-overlay');
if (!ov) return;
ov.addEventListener('click', (e) => {
const t = e.target;
if (!t) return;
if (t.classList && t.classList.contains('quiz-battle-choice')) {
const idx = parseInt(t.getAttribute('data-idx'), 10);
const choice = t.closest ? t.closest('.quiz-battle-choice') : null;
if (choice) {
const idx = parseInt(choice.getAttribute('data-idx'), 10);
if (Number.isFinite(idx) && idx >= 0 && idx <= 2) trySubmitQuizBattleChoice(idx);
return;
}
@@ -22106,7 +22641,7 @@
socket.emit('quiz-carry-lobby-ready', { ready: next });
});
})();
window.addEventListener('beforeunload', () => socket.emit('leave-space'));
window.addEventListener('beforeunload', () => { try { saveQuizBattleProgress(); } catch (e) { /* ignore */ } socket.emit('leave-space'); });
window.addEventListener('resize', () => {
if (!mapData || !canvas) return;
resizeCanvas();
+326 -48
View File
@@ -2123,6 +2123,157 @@
display: flex; align-items: center; gap: 10px;
}
.mg6-life-hud .mg6-life-hearts img { height: 52px; width: auto; }
/* ===== Quiz Battle in-game HUD — design plane 1920×1080 scale ตาม stage ===== */
.qb-game-hud {
position: fixed;
width: 1920px; height: 1080px;
transform-origin: 0 0;
pointer-events: none;
z-index: 55;
font-family: 'NotoSansThai', system-ui, sans-serif;
}
.qb-game-hud.is-hidden { display: none !important; }
.qb-game-hud img { display: block; image-rendering: pixelated; }
.qb-game-hud button { pointer-events: auto; background: none; border: 0; padding: 0; cursor: pointer; }
.qb-game-hud button:active { transform: translateY(1px); }
/* header หมวด (tag-NN.png 389×135) ซ้ายบน */
.qb-hud-tag { position: absolute; left: 26px; top: 18px; width: 389px; height: auto; }
/* PLAYERS กลางบน */
.qb-hud-players {
position: absolute; left: 50%; top: 44px; transform: translateX(-50%);
font-weight: 800; font-size: 40px; line-height: 1; color: #e8f6ff;
letter-spacing: 0.04em; text-shadow: 0 2px 10px rgba(0,0,0,0.7), 0 0 18px rgba(70,200,255,0.5);
white-space: nowrap;
}
.qb-hud-players span { color: #7df9ff; }
/* SCORE — ซ้ายของ host avatar */
.qb-hud-score {
position: absolute; right: 210px; top: 44px;
font-weight: 800; font-size: 40px; line-height: 1; color: #e8f6ff;
letter-spacing: 0.04em; text-shadow: 0 2px 10px rgba(0,0,0,0.7), 0 0 18px rgba(70,200,255,0.5);
white-space: nowrap;
}
.qb-hud-score span { color: #ffd93d; }
/* host avatar — กรอบ btn-profile-bg.png 159×157 + clip avatar ข้างใน (เหมือน Quiz-Battle/.qb-room-profile) */
.qb-hud-host {
position: absolute; right: 18px; top: 14px; width: 159px; height: 157px;
background: url(/Game/img/btn-profile-bg.png) center/100% 100% no-repeat;
overflow: hidden;
}
.qb-hud-host-clip {
position: absolute; left: 18px; top: 10px; width: 124px; height: 118px;
overflow: hidden; border-radius: 12px;
}
.qb-hud-host-av {
position: absolute; left: 50%; top: 5px; width: 120%; height: auto; max-width: none;
object-fit: contain; object-position: center top; transform: translateX(-50%);
image-rendering: pixelated;
}
/* ปุ่มข้าง (btn-*.png 122×142) */
.qb-hud-btn img { width: 108px; height: auto; }
.qb-hud-exit { position: absolute; left: 26px; top: 184px; }
.qb-hud-myresult { position: absolute; right: 30px; top: 240px; }
.qb-hud-ranking { position: absolute; right: 30px; top: 384px; }
/* mic ขวาล่าง (ตาม mockup) */
.qb-hud-mic { position: absolute; right: 34px; bottom: 40px; }
.qb-hud-mic img { width: 96px; height: 96px; object-fit: contain; }
/* ปุ่มซูมเข้า/ออก (ซ้ายล่าง) */
.qb-hud-zoom { position: absolute; left: 30px; bottom: 40px; display: flex; flex-direction: column; gap: 12px; }
.qb-hud-zoom-btn { width: 66px; height: 66px; border-radius: 16px; border: 2px solid rgba(96,210,255,0.7); background: rgba(16,34,56,0.85); color: #aef0ff; font-size: 38px; font-weight: 800; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 14px rgba(64,200,255,0.3); padding: 0; }
.qb-hud-zoom-btn:hover { border-color: rgba(96,210,255,1); background: rgba(24,48,74,0.95); }
.qb-hud-zoom-btn:active { transform: scale(0.94); }
/* ===== Quiz Battle popups (MY RESULT / RANKING) ===== */
.qb-popup { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; font-family: 'Noto Sans Thai','Prompt',sans-serif; }
.qb-popup.is-hidden { display: none !important; }
.qb-popup-backdrop { position: absolute; inset: 0; background: rgba(2,8,20,0.74); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.qb-popup-frame { position: relative; }
.qb-popup-close { position: absolute; background: transparent; border: 0; cursor: pointer; z-index: 6; }
/* ---- MY RESULT ---- */
.qb-mr-frame {
width: min(92vw, 1160px); aspect-ratio: 1290/951;
background: url(/Game/img/qb-ui/popup-myresult.png) center/100% 100% no-repeat;
}
.qb-mr-frame .qb-popup-close { width: 6.6%; aspect-ratio: 1; right: 3%; top: 5.2%; }
.qb-mr-body {
position: absolute; left: 7.5%; right: 6.8%; top: 17.5%; bottom: 6.5%;
display: flex; flex-direction: column; gap: 2.2%; color: #eaf6ff;
}
.qb-mr-stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 2.4%; flex: 0 0 auto; }
.qb-mr-stat { background: rgba(20,42,66,0.66); border: 1px solid rgba(96,210,255,0.42); border-radius: 12px; padding: 1.4% 2%; text-align: center; }
.qb-mr-stat-h { font-size: clamp(11px,1.15vw,16px); color: rgba(168,222,255,0.85); letter-spacing: .03em; }
.qb-mr-stat-v { font-size: clamp(16px,1.9vw,27px); font-weight: 800; color: #7fe9ff; margin-top: 2px; line-height: 1.1; }
.qb-mr-stat-v small { font-size: 58%; font-weight: 600; color: rgba(200,235,255,.78); }
.qb-mr-grid { display: grid; grid-template-columns: repeat(10,1fr); gap: .55%; flex: 0 0 auto; }
.qb-mr-cell { position: relative; aspect-ratio: 1; border-radius: 8px; background: rgba(16,34,54,0.55); border: 1px solid rgba(96,210,255,0.28); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
.qb-mr-cell.is-correct { border-color: rgba(64,210,140,0.7); background: rgba(24,60,44,0.5); }
.qb-mr-cell.is-wrong { border-color: rgba(232,90,110,0.7); background: rgba(64,26,34,0.5); }
.qb-mr-cell.is-unanswered { opacity: .42; }
.qb-mr-cell .qb-mr-n { font-size: clamp(9px,.85vw,13px); color: rgba(180,225,255,.82); line-height: 1; }
.qb-mr-cell .qb-mr-mark { width: 58%; height: auto; }
.qb-mr-detail { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 12px; display: flex; flex-direction: column; gap: 8px; }
.qb-mr-q { background: rgba(18,38,60,0.62); border: 1px solid rgba(96,210,255,0.28); border-radius: 10px; padding: 9px 13px; }
.qb-mr-q-head { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: clamp(12px,1.05vw,16px); color: #cfeeff; }
.qb-mr-q-head img { width: 20px; height: 20px; flex: 0 0 auto; }
.qb-mr-q-text { font-size: clamp(11px,1vw,15px); margin: 5px 0 7px; color: #dff1ff; }
.qb-mr-q-opt { font-size: clamp(10px,.92vw,14px); padding: 4px 9px; border-radius: 6px; margin: 3px 0; background: rgba(255,255,255,0.05); color: rgba(220,238,255,.85); }
.qb-mr-q-opt.is-correct { background: rgba(40,160,90,0.34); color: #bfffd6; font-weight: 600; }
.qb-mr-q-opt.is-chosen-wrong { background: rgba(195,62,74,0.36); color: #ffd0d4; font-weight: 600; }
.qb-mr-detail::-webkit-scrollbar, .qb-rank-list::-webkit-scrollbar { width: 11px; }
.qb-mr-detail::-webkit-scrollbar-track, .qb-rank-list::-webkit-scrollbar-track { background: url(/Game/img/qb-ui/q-scroll-bg.png) center/100% 100% no-repeat; }
.qb-mr-detail::-webkit-scrollbar-thumb, .qb-rank-list::-webkit-scrollbar-thumb { background: url(/Game/img/qb-ui/q-scroll-bar.png) center/100% 100% no-repeat; border-radius: 6px; }
/* ---- RANKING ---- */
.qb-rank-frame {
position: relative; width: min(88vw, 820px); max-height: 88vh; padding: 30px 34px 30px; box-sizing: border-box;
background: linear-gradient(180deg, rgba(18,40,64,0.98), rgba(11,24,42,0.98));
border: 2px solid rgba(96,210,255,0.85); border-radius: 22px;
box-shadow: 0 0 34px rgba(64,200,255,0.45), inset 0 0 22px rgba(64,200,255,0.16);
display: flex; flex-direction: column; color: #eaf6ff;
}
.qb-rank-title { position: absolute; left: 50%; top: -26px; transform: translateX(-50%); background: linear-gradient(180deg,#0c2238,#0a1c30); border: 2px solid rgba(96,210,255,0.9); border-radius: 14px; padding: 9px 48px; font-size: clamp(18px,2vw,28px); font-weight: 800; color: #aef0ff; letter-spacing: .05em; box-shadow: 0 0 18px rgba(64,200,255,0.5); white-space: nowrap; }
.qb-rank-frame .qb-popup-close { width: 46px; height: 46px; right: 6px; top: 6px; border-radius: 10px; }
.qb-rank-frame .qb-popup-close::before, .qb-rank-frame .qb-popup-close::after { content: ''; position: absolute; left: 50%; top: 50%; width: 26px; height: 4px; background: #8fe7ff; border-radius: 2px; box-shadow: 0 0 8px rgba(96,210,255,0.7); }
.qb-rank-frame .qb-popup-close::before { transform: translate(-50%,-50%) rotate(45deg); }
.qb-rank-frame .qb-popup-close::after { transform: translate(-50%,-50%) rotate(-45deg); }
.qb-rank-body { display: flex; flex-direction: column; min-height: 0; margin-top: 14px; }
.qb-rank-podium { display: flex; align-items: flex-end; justify-content: center; gap: 16px; margin-bottom: 16px; flex: 0 0 auto; }
.qb-rank-pod { display: flex; flex-direction: column; align-items: center; }
.qb-rank-pod-av { border-radius: 50%; background: rgba(12,30,50,0.9); border: 3px solid rgba(96,210,255,0.7); overflow: hidden; display: flex; align-items: flex-end; justify-content: center; }
.qb-rank-pod-av img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }
.qb-rank-pod-1 .qb-rank-pod-av { width: 108px; height: 108px; border-color: #ffd34d; box-shadow: 0 0 18px rgba(255,200,60,0.6); }
.qb-rank-pod-2 .qb-rank-pod-av { width: 86px; height: 86px; border-color: #cfe2ff; }
.qb-rank-pod-3 .qb-rank-pod-av { width: 86px; height: 86px; border-color: #ff9d6b; }
.qb-rank-pod-badge { margin-top: -14px; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 15px; color: #06121f; }
.qb-rank-pod-1 .qb-rank-pod-badge { background: #ffd34d; }
.qb-rank-pod-2 .qb-rank-pod-badge { background: #cfe2ff; }
.qb-rank-pod-3 .qb-rank-pod-badge { background: #ff9d6b; }
.qb-rank-pod-name { font-size: clamp(11px,1vw,14px); font-weight: 700; margin-top: 6px; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center; }
.qb-rank-pod-score { font-size: clamp(11px,1vw,15px); color: #7fe9ff; font-weight: 800; }
.qb-rank-listhead, .qb-rank-row { display: grid; grid-template-columns: 64px 1fr 90px; align-items: center; padding: 7px 14px; }
.qb-rank-listhead { color: rgba(168,222,255,0.8); font-size: clamp(10px,.9vw,13px); letter-spacing: .06em; border-bottom: 1px solid rgba(96,210,255,0.3); }
.qb-rank-c3 { text-align: right; }
.qb-rank-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.qb-rank-row { background: rgba(18,38,60,0.5); border: 1px solid rgba(96,210,255,0.22); border-radius: 10px; margin: 5px 0; font-size: clamp(11px,1vw,15px); }
.qb-rank-row .qb-rank-c1 { font-weight: 800; color: #aef0ff; }
.qb-rank-row .qb-rank-c3 { font-weight: 800; color: #7fe9ff; }
.qb-rank-me { flex: 0 0 auto; margin-top: 8px; }
.qb-rank-me .qb-rank-row { background: rgba(96,210,255,0.18); border-color: rgba(96,210,255,0.7); box-shadow: 0 0 14px rgba(64,200,255,0.35); margin: 0; }
.qb-rank-empty { text-align: center; color: rgba(190,225,255,0.7); padding: 24px 0; font-size: 14px; }
/* ---- COMPLETED ---- */
.qb-completed-frame {
width: min(92vw, 1180px); aspect-ratio: 1344/781;
background: url(/Game/img/qb-ui/popup-result.png) center/100% 100% no-repeat;
}
.qb-completed-frame .qb-popup-close { width: 6%; aspect-ratio: 1; right: 5%; top: 9%; }
.qb-completed-body {
position: absolute; left: 11.2%; right: 11.1%; top: 50.5%; height: 16%;
display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.86%;
}
.qb-cp-val {
display: flex; align-items: center; justify-content: center; text-align: center;
font-family: 'Noto Sans Thai','Prompt',sans-serif; font-weight: 800;
font-size: clamp(20px, 2.6vw, 40px); color: #7fe9ff;
text-shadow: 0 0 14px rgba(64,200,255,0.4);
}
#play-cyber-hud.play-cyber-hud--question-mission .play-cyber-ss-meta {
font-size: 0.5rem;
letter-spacing: 0.06em;
@@ -4423,68 +4574,119 @@
}
.quiz-battle-mcq-card {
position: relative;
max-width: min(92vw, 440px);
width: 100%;
background: linear-gradient(165deg, rgba(14, 18, 36, 0.97), rgba(8, 12, 28, 0.98));
border: 1px solid rgba(0, 255, 240, 0.28);
border-radius: 14px;
padding: 1.1rem 1.15rem 1rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.5),
0 0 40px rgba(0, 200, 255, 0.12);
font-family: 'Share Tech Mono', ui-monospace, monospace;
width: min(94vw, 640px);
box-sizing: border-box;
padding: 40px 34px 30px;
background: linear-gradient(180deg, rgba(20, 42, 68, 0.98), rgba(12, 24, 44, 0.98));
border: 2px solid rgba(96, 210, 255, 0.85);
border-radius: 22px;
box-shadow: 0 0 34px rgba(64, 200, 255, 0.45), inset 0 0 22px rgba(64, 200, 255, 0.16);
font-family: 'Noto Sans Thai', 'Prompt', sans-serif;
}
.quiz-battle-mcq-title {
margin: 0 0 0.6rem;
font-family: 'Orbitron', sans-serif;
font-size: 0.78rem;
position: absolute;
left: 50%;
top: -24px;
transform: translateX(-50%);
margin: 0;
white-space: nowrap;
background: linear-gradient(180deg, #ff5bd0, #c01e88);
border: 2px solid rgba(255, 150, 225, 0.9);
border-radius: 14px;
padding: 8px 40px;
font-size: clamp(15px, 1.7vw, 22px);
font-weight: 800;
letter-spacing: 0.16em;
color: #5cf0ff;
text-shadow: 0 0 12px rgba(92, 240, 255, 0.4);
letter-spacing: 0.08em;
color: #fff;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
box-shadow: 0 0 18px rgba(255, 90, 200, 0.5);
}
#quiz-battle-mcq-close {
position: absolute;
right: 10px;
top: 10px;
width: 42px;
height: 42px;
padding: 0;
border: 0;
border-radius: 10px;
background: transparent;
cursor: pointer;
}
#quiz-battle-mcq-close::before,
#quiz-battle-mcq-close::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 24px;
height: 4px;
background: #8fe7ff;
border-radius: 2px;
box-shadow: 0 0 8px rgba(96, 210, 255, 0.7);
}
#quiz-battle-mcq-close::before { transform: translate(-50%, -50%) rotate(45deg); }
#quiz-battle-mcq-close::after { transform: translate(-50%, -50%) rotate(-45deg); }
.quiz-battle-mcq-text {
margin: 0 0 1rem;
font-size: clamp(0.9rem, 2.8vw, 1.05rem);
margin: 10px 0 18px;
padding: 14px 18px;
background: rgba(10, 24, 42, 0.7);
border: 1px solid rgba(96, 210, 255, 0.35);
border-radius: 12px;
font-size: clamp(15px, 1.6vw, 20px);
line-height: 1.5;
color: #e2e8f0;
font-weight: 600;
color: #eaf6ff;
font-weight: 700;
text-align: center;
}
.quiz-battle-mcq-actions {
display: flex;
flex-direction: column;
gap: 0.45rem;
margin-bottom: 0.65rem;
gap: 12px;
}
.quiz-battle-choice {
position: relative;
display: flex;
align-items: center;
gap: 14px;
width: 100%;
text-align: left;
padding: 0.55rem 0.75rem;
border-radius: 8px;
border: 1px solid rgba(122, 162, 247, 0.45);
background: rgba(20, 28, 48, 0.95);
color: #c0caf5;
font-size: 0.82rem;
padding: 12px 16px;
border-radius: 14px;
border: 2px solid rgba(96, 210, 255, 0.5);
background: rgba(16, 34, 56, 0.92);
color: #dff1ff;
font-size: clamp(13px, 1.4vw, 17px);
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
.quiz-battle-choice:hover {
border-color: rgba(0, 255, 240, 0.55);
box-shadow: 0 0 16px rgba(0, 200, 255, 0.15);
border-color: rgba(96, 210, 255, 0.95);
box-shadow: 0 0 16px rgba(64, 200, 255, 0.3);
}
#quiz-battle-mcq-close {
width: 100%;
margin-top: 0.15rem;
padding: 0.45rem;
border: none;
border-radius: 8px;
background: rgba(60, 70, 100, 0.6);
color: #a9b1d6;
font-weight: 700;
cursor: pointer;
font-size: 0.78rem;
.quiz-battle-choice .qbc-letter {
flex: 0 0 auto;
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(96, 210, 255, 0.2);
border: 1px solid rgba(96, 210, 255, 0.7);
font-weight: 800;
color: #aef0ff;
}
.quiz-battle-choice .qbc-text { flex: 1 1 auto; }
.quiz-battle-choice .qbc-mark { flex: 0 0 auto; width: 28px; height: 28px; display: none; }
.quiz-battle-choice.is-correct { border-color: #38d98a; background: rgba(28, 84, 58, 0.92); color: #d6ffe7; }
.quiz-battle-choice.is-correct .qbc-letter { background: rgba(56, 217, 138, 0.3); border-color: #38d98a; color: #d6ffe7; }
.quiz-battle-choice.is-wrong { border-color: #ff5a6e; background: rgba(86, 28, 38, 0.92); color: #ffd6db; }
.quiz-battle-choice.is-wrong .qbc-letter { background: rgba(255, 90, 110, 0.3); border-color: #ff5a6e; color: #ffd6db; }
.quiz-battle-choice.is-correct .qbc-mark,
.quiz-battle-choice.is-wrong .qbc-mark { display: block; }
.quiz-battle-choice.is-locked { pointer-events: none; }
</style>
</head>
<body>
@@ -4618,6 +4820,82 @@
<div class="mg6-life-hearts" id="mg6-life-hearts"></div>
</div>
</div>
<!-- Quiz Battle (qbroom*) in-game HUD — DOM overlay สเกล design 1920×1080 (header หมวด, PLAYERS, SCORE, host, EXIT, MY RESULT, RANKING) -->
<div id="qb-game-hud" class="qb-game-hud is-hidden" aria-hidden="true">
<img class="qb-hud-tag" id="qb-hud-tag" src="" alt="" decoding="async" />
<div class="qb-hud-players">PLAYERS : <span id="qb-hud-players-val">1/50</span></div>
<div class="qb-hud-score">SCORE : <span id="qb-hud-score-val">0</span></div>
<div class="qb-hud-host">
<div class="qb-hud-host-clip"><img class="qb-hud-host-av" id="qb-hud-host-av" src="" alt="" decoding="async" /></div>
</div>
<button type="button" class="qb-hud-btn qb-hud-exit" id="qb-hud-exit" aria-label="ออกจากห้อง">
<img src="/Game/img/qb-ui/btn-exit-room.png" alt="EXIT" decoding="async" />
</button>
<button type="button" class="qb-hud-btn qb-hud-myresult" id="qb-hud-myresult" aria-label="ผลของฉัน">
<img src="/Game/img/qb-ui/btn-my-result.png" alt="MY RESULT" decoding="async" />
</button>
<button type="button" class="qb-hud-btn qb-hud-ranking" id="qb-hud-ranking" aria-label="อันดับ">
<img src="/Game/img/qb-ui/btn-ranking.png" alt="RANKING" decoding="async" />
</button>
<button type="button" class="qb-hud-btn qb-hud-mic" id="qb-hud-mic" aria-label="ไมโครโฟน">
<img id="qb-hud-mic-img" src="/Game/img/btn-mic-on.png" alt="mic" decoding="async" />
</button>
<div class="qb-hud-zoom" id="qb-hud-zoom">
<button type="button" class="qb-hud-zoom-btn" id="qb-hud-zoom-in" aria-label="ซูมเข้า">+</button>
<button type="button" class="qb-hud-zoom-btn" id="qb-hud-zoom-out" aria-label="ซูมออก">&#8722;</button>
</div>
</div>
<!-- Quiz Battle: MY RESULT popup -->
<div id="qb-myresult-popup" class="qb-popup is-hidden" role="dialog" aria-modal="true" aria-hidden="true">
<div class="qb-popup-backdrop" data-qb-close="myresult" aria-hidden="true"></div>
<div class="qb-popup-frame qb-mr-frame">
<button type="button" class="qb-popup-close" id="qb-myresult-close" data-qb-close="myresult" aria-label="ปิด"></button>
<div class="qb-mr-body">
<div class="qb-mr-stats">
<div class="qb-mr-stat">
<div class="qb-mr-stat-h">Accuracy (avg.)</div>
<div class="qb-mr-stat-v"><span id="qb-mr-accuracy">0%</span> <small id="qb-mr-accuracy-sub">(0/0)</small></div>
</div>
<div class="qb-mr-stat">
<div class="qb-mr-stat-h">Progress</div>
<div class="qb-mr-stat-v"><span id="qb-mr-progress">0%</span> <small id="qb-mr-progress-sub">(0/0)</small></div>
</div>
<div class="qb-mr-stat">
<div class="qb-mr-stat-h">Total time</div>
<div class="qb-mr-stat-v" id="qb-mr-time">0 s</div>
</div>
</div>
<div class="qb-mr-grid" id="qb-mr-grid"></div>
<div class="qb-mr-detail" id="qb-mr-detail"></div>
</div>
</div>
</div>
<!-- Quiz Battle: RANKING popup -->
<div id="qb-ranking-popup" class="qb-popup is-hidden" role="dialog" aria-modal="true" aria-hidden="true">
<div class="qb-popup-backdrop" data-qb-close="ranking" aria-hidden="true"></div>
<div class="qb-popup-frame qb-rank-frame">
<div class="qb-rank-title">Ranking</div>
<button type="button" class="qb-popup-close" id="qb-ranking-close" data-qb-close="ranking" aria-label="ปิด"></button>
<div class="qb-rank-body">
<div class="qb-rank-podium" id="qb-rank-podium"></div>
<div class="qb-rank-listhead"><span class="qb-rank-c1">RANK</span><span class="qb-rank-c2">PLAYER</span><span class="qb-rank-c3">SCORE</span></div>
<div class="qb-rank-list" id="qb-rank-list"></div>
<div class="qb-rank-me" id="qb-rank-me"></div>
</div>
</div>
</div>
<!-- Quiz Battle: COMPLETED popup -->
<div id="qb-completed-popup" class="qb-popup is-hidden" role="dialog" aria-modal="true" aria-hidden="true">
<div class="qb-popup-backdrop" data-qb-close="completed" aria-hidden="true"></div>
<div class="qb-popup-frame qb-completed-frame">
<button type="button" class="qb-popup-close" id="qb-completed-close" data-qb-close="completed" aria-label="ปิด"></button>
<div class="qb-completed-body">
<div class="qb-cp-val" id="qb-cp-correct">0 / 0</div>
<div class="qb-cp-val" id="qb-cp-time">0 s</div>
<div class="qb-cp-val" id="qb-cp-rank"></div>
</div>
</div>
</div>
<div id="play-quiz-scoreboard" class="is-hidden" aria-live="polite">
<div class="play-quiz-scoreboard-title">คะแนน</div>
<ul id="play-quiz-scoreboard-list" class="play-quiz-scoreboard-list"></ul>
@@ -4662,14 +4940,14 @@
<div id="quiz-battle-mcq-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="quiz-battle-mcq-title">
<div class="quiz-battle-mcq-backdrop" aria-hidden="true"></div>
<div class="quiz-battle-mcq-card">
<h2 id="quiz-battle-mcq-title" class="quiz-battle-mcq-title">QUIZ BATTLE</h2>
<h2 id="quiz-battle-mcq-title" class="quiz-battle-mcq-title">QUESTION</h2>
<button type="button" id="quiz-battle-mcq-close" aria-label="ปิด"></button>
<p id="quiz-battle-mcq-text" class="quiz-battle-mcq-text"></p>
<div class="quiz-battle-mcq-actions">
<button type="button" class="quiz-battle-choice" data-idx="0">A</button>
<button type="button" class="quiz-battle-choice" data-idx="1">B</button>
<button type="button" class="quiz-battle-choice" data-idx="2">C</button>
<button type="button" class="quiz-battle-choice" data-idx="0"><span class="qbc-letter">A</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
<button type="button" class="quiz-battle-choice" data-idx="1"><span class="qbc-letter">B</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
<button type="button" class="quiz-battle-choice" data-idx="2"><span class="qbc-letter">C</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
</div>
<button type="button" id="quiz-battle-mcq-close">ปิด · Close</button>
</div>
</div>
<div id="gauntlet-crown-howto-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-label="HOW TO PLAY — Last Light">
@@ -4874,7 +5152,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.00610200020"></script>
<script src="js/play.js?v=0.00610200030"></script>
<div class="version-tag">v —</div>
</body>
</html>
+67 -6
View File
@@ -248,6 +248,57 @@ const QUIZ_SETTINGS_MIRROR_PATH = process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH
? path.resolve(String(process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH).trim())
: '';
const GAME_TIMING_PATH = path.join(__dirname, 'data', 'game-timing.json');
/* ===== Quiz Battle global leaderboard (เก็บคะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */
const QB_LEADERBOARD_PATH = path.join(__dirname, 'data', 'quiz-battle-leaderboard.json');
/** @type {{[roomId:string]: Array<{name:string,score:number,characterId:string,ts:number}>}} */
let qbLeaderboardStore = {};
function loadQbLeaderboard() {
try {
if (fs.existsSync(QB_LEADERBOARD_PATH)) {
const j = JSON.parse(fs.readFileSync(QB_LEADERBOARD_PATH, 'utf8'));
qbLeaderboardStore = (j && typeof j === 'object') ? j : {};
}
} catch (e) { qbLeaderboardStore = {}; }
}
function saveQbLeaderboard() {
try {
const dir = path.dirname(QB_LEADERBOARD_PATH);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(QB_LEADERBOARD_PATH, JSON.stringify(qbLeaderboardStore), 'utf8');
} catch (e) { /* ignore */ }
}
function qbLeaderboardSubmit(roomId, name, score, characterId) {
roomId = String(roomId || '').trim();
name = String(name || '').trim().slice(0, 24);
score = Math.max(0, Math.floor(Number(score) || 0));
if (!roomId || !name) return;
if (!Array.isArray(qbLeaderboardStore[roomId])) qbLeaderboardStore[roomId] = [];
const list = qbLeaderboardStore[roomId];
const ex = list.find((e) => e && e.name === name);
if (ex) {
if (score > (ex.score || 0)) { ex.score = score; ex.characterId = String(characterId || ''); ex.ts = Date.now(); }
} else {
list.push({ name, score, characterId: String(characterId || ''), ts: Date.now() });
}
list.sort((a, b) => (b.score - a.score) || (a.ts - b.ts));
if (list.length > 500) list.length = 500;
saveQbLeaderboard();
}
function qbLeaderboardGet(roomId, name) {
roomId = String(roomId || '').trim();
const list = Array.isArray(qbLeaderboardStore[roomId]) ? qbLeaderboardStore[roomId] : [];
const top = list.slice(0, 50).map((e, i) => ({ rank: i + 1, name: e.name, score: e.score || 0, characterId: e.characterId || '' }));
let me = null;
const nm = String(name || '').trim();
if (nm) {
const idx = list.findIndex((e) => e && e.name === nm);
if (idx >= 0) me = { rank: idx + 1, name: nm, score: list[idx].score || 0, characterId: list[idx].characterId || '' };
}
return { top, me, total: list.length };
}
loadQbLeaderboard();
/** ช่องตัวเลือกบนกริด quiz_carry เก็บเลข 1..N (ไม่ใช่แค่ 0/1 เหมือน hub) */
const QUIZ_CARRY_MAX_OPTION_SLOTS = 16;
const maps = new Map();
@@ -5433,13 +5484,11 @@ const server = http.createServer((req, res) => {
}
if (url.startsWith(BASE_PATH + '/img/gauntlet-assets/') && req.method === 'GET') {
const sub = decodeURIComponent(url.slice((BASE_PATH + '/img/gauntlet-assets/').length).split('?')[0]);
const fname = safeGauntletStoredFilename(sub);
if (!fname) {
res.writeHead(400);
return res.end();
}
// production: nginx เสิร์ฟ /Game/img/* เป็น static ทั้งหมด — local node จึงต้องเสิร์ฟไฟล์ที่มีอยู่จริงในโฟลเดอร์ด้วย
// (ไม่จำกัดเฉพาะไฟล์อัปโหลด gauntlet-<hash>) ไม่งั้น asset ดีไซน์ (popup-Howto, Avartar, btn-ready…) จะ 400 เฉพาะตอนรันผ่าน node ตรง ๆ
const fname = safeGauntletStoredFilename(sub) || path.basename(sub); // basename กัน path traversal
const fp = path.join(GAUNTLET_ASSETS_DIR, fname);
if (!fs.existsSync(fp)) {
if (!fname || !fs.existsSync(fp)) {
res.writeHead(404);
return res.end();
}
@@ -5451,6 +5500,9 @@ const server = http.createServer((req, res) => {
});
}
if (url.indexOf(BASE_PATH) === 0) url = url.slice(BASE_PATH.length) || '/index.html';
// ตัด query string / hash ออกก่อน resolve เป็น path ไฟล์ (เช่น /play.html?space=...&map=... → /play.html)
// ไม่ตัด จะหาไฟล์ชื่อ "play.html?space=..." ไม่เจอ → 404 (เข้า node ตรง ๆ พร้อม query)
url = url.split('?')[0].split('#')[0] || '/index.html';
const filePath = path.join(__dirname, 'public', url);
const ext = path.extname(filePath);
const types = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css' };
@@ -6556,6 +6608,15 @@ function qbwBroadcastScores(sid, space) {
}
io.on('connection', (socket) => {
/* ===== Quiz Battle global leaderboard (คะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */
socket.on('qb-leaderboard-submit', (d) => {
if (!d || typeof d !== 'object') return;
qbLeaderboardSubmit(d.roomId, d.name, d.score, d.characterId);
});
socket.on('qb-leaderboard-get', (d, cb) => {
if (typeof cb !== 'function') return;
try { cb(qbLeaderboardGet(d && d.roomId, d && d.name)); } catch (e) { cb({ top: [], me: null, total: 0 }); }
});
/* ===== Quiz Battle ฉากเดินตอบ (ZEP) — sync คะแนน ===== */
socket.on('quizbattle-hello', (cb) => {
const sid = socket.data.spaceId;