update desige 2 3 5 6 7and quiz battle
@@ -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;
|
||||
|
||||
@@ -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}]}
|
||||
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -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="ซูมออก">−</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||