@@ -407,7 +416,7 @@
-
+
v —
diff --git a/www/html/Game/public/js/editor.js b/www/html/Game/public/js/editor.js
index 5ec6bba..291221c 100644
--- a/www/html/Game/public/js/editor.js
+++ b/www/html/Game/public/js/editor.js
@@ -108,6 +108,13 @@
let quizBattleDomeArea = [];
/** quiz_battle: กริด 0/1 — ถ้ามีอย่างน้อย 1 ช่อง = ในเกมเดินได้เฉพาะบนเส้นทาง */
let quizBattlePathArea = [];
+ /** quiz_battle: กริด 0/สถานี — จุดประตูบนเส้นทาง (ต้องตอบสถานี N ก่อนผ่านช่องนี้และข้างหน้า) */
+ let quizBattlePathGateArea = [];
+ /** quiz_battle: หมวดคำถามของห้อง (topic1..topic10) — กรองจาก battleQuizMcq */
+ let editorQuizBattleCategoryId = '';
+ /** quiz_battle: รายการคำถามใน Editor [{ compId, text }] */
+ let editorQuizBattleMcqList = [];
+ let editorQuizBattleDomeComp = null;
let stackReleaseArea = [], stackLandArea = [];
/** jump_survive: แพลตฟอร์ม { x, y, w, h } tile — ใช้เต็มที่เมื่อมีมุมข้าง */
let jumpSurvivePlatforms = [];
@@ -1243,6 +1250,157 @@
}
}
+ function ensureQuizBattlePathGateArea() {
+ if (!quizBattlePathGateArea.length || quizBattlePathGateArea.length !== height) {
+ const existing = quizBattlePathGateArea.slice().map(r => r && r.slice());
+ quizBattlePathGateArea = [];
+ for (let y = 0; y < height; y++) {
+ const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
+ quizBattlePathGateArea.push(row);
+ }
+ } else {
+ for (let y = 0; y < height; y++) {
+ if (!quizBattlePathGateArea[y] || quizBattlePathGateArea[y].length !== width) quizBattlePathGateArea[y] = Array(width).fill(0);
+ }
+ }
+ }
+
+ function inferQuizBattleCategoryFromMapId(id) {
+ const m = /^qbroom(\d+)$/i.exec(String(id || '').trim());
+ if (!m) return '';
+ const n = parseInt(m[1], 10);
+ return n > 0 ? ('topic' + n) : '';
+ }
+
+ function rebuildEditorQuizBattleDomeComp() {
+ if (!quizBattleDomeArea.length) {
+ editorQuizBattleDomeComp = null;
+ return;
+ }
+ const w = width, h = height;
+ const dome = quizBattleDomeArea;
+ const comp = Array(h).fill(0).map(() => Array(w).fill(0));
+ const seen = Array(h).fill(0).map(() => Array(w).fill(false));
+ let compId = 0;
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ if (!dome[y] || dome[y][x] !== 1 || seen[y][x]) continue;
+ compId++;
+ const q = [[x, y]];
+ seen[y][x] = true;
+ while (q.length) {
+ const [cx, cy] = q.shift();
+ comp[cy][cx] = compId;
+ for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
+ const nx = cx + dx, ny = cy + dy;
+ if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
+ if (!dome[ny] || dome[ny][nx] !== 1 || seen[ny][nx]) continue;
+ seen[ny][nx] = true;
+ q.push([nx, ny]);
+ }
+ }
+ }
+ }
+ editorQuizBattleDomeComp = comp;
+ }
+
+ function getEditorQuizBattleDomeCompAt(tx, ty) {
+ if (!editorQuizBattleDomeComp) rebuildEditorQuizBattleDomeComp();
+ if (!editorQuizBattleDomeComp || ty < 0 || tx < 0 || ty >= height || tx >= width) return 0;
+ return editorQuizBattleDomeComp[ty][tx] || 0;
+ }
+
+ function quizBattleGateQuestionSnippet(compId) {
+ const row = editorQuizBattleMcqList.find((q) => q.compId === compId);
+ if (!row || !row.text) return '';
+ const t = String(row.text).trim();
+ return t.length > 48 ? t.slice(0, 48) + '…' : t;
+ }
+
+ function sanitizeEditorBattleMcqRow(raw, roomCat) {
+ if (!raw || !String(raw.text || '').trim()) return null;
+ if (roomCat && String(raw.categoryId || '').trim() !== roomCat) return null;
+ const ch = Array.isArray(raw.choices) ? raw.choices : [];
+ if (ch.length !== 3) return null;
+ const a = String(ch[0] || '').trim();
+ const b = String(ch[1] || '').trim();
+ const c = String(ch[2] || '').trim();
+ if (!a || !b || !c) return null;
+ return { text: String(raw.text).trim() };
+ }
+
+ function rebuildQuizBattleGateQuestionSelect() {
+ const sel = document.getElementById('quiz-battle-gate-question');
+ if (!sel) return;
+ const prev = sel.value;
+ sel.innerHTML = '';
+ if (!editorQuizBattleMcqList.length) {
+ const opt = document.createElement('option');
+ opt.value = '1';
+ opt.textContent = 'ยังไม่มีคำถาม — บันทึกใน Admin → Quiz Battle';
+ sel.appendChild(opt);
+ return;
+ }
+ editorQuizBattleMcqList.forEach((row) => {
+ const opt = document.createElement('option');
+ opt.value = String(row.compId);
+ const snip = row.text.length > 56 ? row.text.slice(0, 56) + '…' : row.text;
+ opt.textContent = 'ข้อ ' + row.compId + ': ' + snip;
+ opt.title = row.text;
+ sel.appendChild(opt);
+ });
+ if (prev && editorQuizBattleMcqList.some((q) => String(q.compId) === prev)) sel.value = prev;
+ }
+
+ async function loadEditorQuizBattleMcqList() {
+ const roomCat = editorQuizBattleCategoryId ? String(editorQuizBattleCategoryId).trim() : '';
+ editorQuizBattleMcqList = [];
+ const bust = '_=' + Date.now();
+ const tryUrls = [
+ BASE + '/api/quiz-settings?' + bust,
+ BASE + '/api-quiz-battle-mcq.php?' + bust,
+ ];
+ for (let u = 0; u < tryUrls.length; u++) {
+ try {
+ const r = await fetch(tryUrls[u], { cache: 'no-store' });
+ if (!r.ok) continue;
+ const s = await r.json();
+ const arr = s && Array.isArray(s.battleQuizMcq) ? s.battleQuizMcq : [];
+ let compId = 0;
+ for (let i = 0; i < arr.length; i++) {
+ const row = sanitizeEditorBattleMcqRow(arr[i], roomCat);
+ if (!row) continue;
+ compId++;
+ editorQuizBattleMcqList.push({ compId: compId, text: row.text });
+ }
+ if (editorQuizBattleMcqList.length) break;
+ } catch (e) { /* next */ }
+ }
+ rebuildQuizBattleGateQuestionSelect();
+ if (statusEl && drawModeEl && drawModeEl.value === 'quizBattlePathGate') {
+ const n = editorQuizBattleMcqList.length;
+ statusEl.textContent = n
+ ? ('โหลดคำถามแล้ว ' + n + ' ข้อ' + (roomCat ? (' (' + roomCat + ')') : '') + ' — เลือกคำถามแล้วคลิกเส้นทางม่วง')
+ : 'ยังไม่มีคำถามใน Admin → Quiz Battle' + (roomCat ? (' สำหรับ ' + roomCat) : '');
+ }
+ }
+
+ function readQuizBattleGateStationInput() {
+ const sel = document.getElementById('quiz-battle-gate-question');
+ let n = sel ? parseInt(sel.value, 10) : 1;
+ if (!(n > 0)) n = 1;
+ if (editorQuizBattleMcqList.length) n = Math.min(n, editorQuizBattleMcqList.length);
+ else if (n > 99) n = 99;
+ return n;
+ }
+
+ function setQuizBattleGateQuestionSelect(compId) {
+ const sel = document.getElementById('quiz-battle-gate-question');
+ if (!sel || !(compId > 0)) return;
+ const v = String(compId);
+ if ([...sel.options].some((o) => o.value === v)) sel.value = v;
+ }
+
function ensureStartGameArea() {
if (!startGameArea.length || startGameArea.length !== height) {
const existing = startGameArea.slice().map(r => r && r.slice());
@@ -1912,6 +2070,7 @@
const dqCarryOpts = document.querySelectorAll('.quiz-carry-mode-opt');
const dqBattleDome = document.getElementById('draw-mode-option-quiz-battle-dome');
const dqBattlePath = document.getElementById('draw-mode-option-quiz-battle-path');
+ const dqBattleGate = document.getElementById('draw-mode-option-quiz-battle-path-gate');
if (!froggerWrap) return;
const gt = gameTypeEl ? gameTypeEl.value : 'zep';
if (drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'jumpSurvivePlatform1';
@@ -1932,6 +2091,10 @@
dqBattlePath.hidden = gt !== 'quiz_battle';
if (gt !== 'quiz_battle' && drawModeEl && drawModeEl.value === 'quizBattlePath') drawModeEl.value = 'wall';
}
+ if (dqBattleGate) {
+ dqBattleGate.hidden = gt !== 'quiz_battle';
+ if (gt !== 'quiz_battle' && drawModeEl && drawModeEl.value === 'quizBattlePathGate') drawModeEl.value = 'wall';
+ }
if (hint) hint.style.removeProperty('display');
if (gt === 'frogger') {
froggerWrap.style.display = 'block';
@@ -2046,10 +2209,15 @@
draw();
} else if (gt === 'quiz_battle') {
froggerWrap.style.display = 'none';
+ if (!editorQuizBattleCategoryId && mapId) {
+ editorQuizBattleCategoryId = inferQuizBattleCategoryFromMapId(mapId);
+ }
+ loadEditorQuizBattleMcqList();
if (hint) {
hint.innerHTML = 'Quiz Battle — เส้นทาง + โดมถาม A / B / C
' + editorHintBulletList([
'โหมด เส้นทางเดิน (ม่วง) — วาดซิกแซกเหมือนทางในเกม · ถ้ามีอย่างน้อย 1 ช่อง ผู้เล่นเดินได้เฉพาะบนเส้นทาง · ไม่วาดเลย = เดินอิสระทั้งแมป',
'โหมด โดมคำถาม — วางบนเส้นทาง (หรือทั่วแมปถ้าไม่ใช้เส้นทาง) · ในเกมกด E',
+ 'โหมด จุดปลดล็อก / ประตู (แดง) — เลือกคำถามจากเมนู · คลิกโดมเพื่อเลือกคำถามนั้น · คลิกเส้นทางม่วงหลังโดมเพื่อวางประตู · ไม่วาด = คำนวณอัตโนมัติ',
'รูป พื้นหลัง เปลี่ยนทีหลังได้ — ตรรกะเส้นทาง/โดมอยู่ที่กริด ไม่ผูกกราฟิก',
'ข้อสอบจาก Admin → Quiz Battle (battleQuizMcq) · แนะนำ พื้นที่สุ่มจุดเกิด ให้อยู่บนเส้นทางหรือใกล้จุดเริ่ม',
]);
@@ -2146,7 +2314,7 @@
if (drawModeEl && carryModes.indexOf(drawModeEl.value) >= 0 && gt !== 'quiz_carry') {
drawModeEl.value = 'wall';
}
- if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath') && gt !== 'quiz_battle') {
+ if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath' || drawModeEl.value === 'quizBattlePathGate') && gt !== 'quiz_battle') {
drawModeEl.value = 'wall';
}
const jPlat1 = document.getElementById('draw-mode-option-jump-platform-1');
@@ -2190,6 +2358,7 @@
carryEmbedCountdownArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizBattleDomeArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizBattlePathArea = Array(height).fill(0).map(() => Array(width).fill(0));
+ quizBattlePathGateArea = Array(height).fill(0).map(() => Array(width).fill(0));
stackReleaseArea = Array(height).fill(0).map(() => Array(width).fill(0));
stackLandArea = Array(height).fill(0).map(() => Array(width).fill(0));
jumpSurvivePlatformArea = Array(height).fill(0).map(() => Array(width).fill(0));
@@ -2317,6 +2486,8 @@
if (gtDraw === 'quiz_battle') {
ensureQuizBattlePathArea();
ensureQuizBattleDomeArea();
+ ensureQuizBattlePathGateArea();
+ rebuildEditorQuizBattleDomeComp();
}
if (gtDraw === 'stack') ensureStackAreas();
if (gtDraw === 'jump_survive') {
@@ -2658,6 +2829,7 @@
ctx.strokeRect(tx + 1, ty + 1, tileSize - 2, tileSize - 2);
}
if (gtDraw === 'quiz_battle' && quizBattleDomeArea[y] && quizBattleDomeArea[y][x] === 1) {
+ const dComp = getEditorQuizBattleDomeCompAt(x, y);
ctx.fillStyle = 'rgba(100, 200, 255, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 100, 130, 0.92)';
@@ -2667,7 +2839,21 @@
ctx.fillStyle = '#1a1b26';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
- ctx.fillText('E', tx + tileSize / 2, ty + tileSize / 2 + 3);
+ ctx.fillText(dComp > 0 ? ('E' + dComp) : 'E', tx + tileSize / 2, ty + tileSize / 2 + 3);
+ ctx.textAlign = 'left';
+ }
+ if (gtDraw === 'quiz_battle' && quizBattlePathGateArea[y] && quizBattlePathGateArea[y][x] > 0) {
+ const gn = quizBattlePathGateArea[y][x];
+ ctx.fillStyle = 'rgba(255, 55, 55, 0.62)';
+ ctx.fillRect(tx + Math.floor(tileSize / 2) - 4, ty + 2, 8, tileSize - 4);
+ ctx.strokeStyle = 'rgba(255, 220, 120, 0.95)';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(tx + Math.floor(tileSize / 2) - 4, ty + 2, 8, tileSize - 4);
+ ctx.lineWidth = 1;
+ ctx.fillStyle = '#fff8e8';
+ ctx.font = 'bold 8px sans-serif';
+ ctx.textAlign = 'center';
+ ctx.fillText('Q' + gn, tx + tileSize / 2, ty + tileSize - 5);
ctx.textAlign = 'left';
}
}
@@ -3191,6 +3377,39 @@
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
ensureQuizBattleDomeArea();
quizBattleDomeArea[y][x] = left ? 1 : 0;
+ rebuildEditorQuizBattleDomeComp();
+ } else if (drawModeEl.value === 'quizBattlePathGate') {
+ if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
+ ensureQuizBattlePathArea();
+ ensureQuizBattlePathGateArea();
+ if (left && quizBattleDomeArea[y] && quizBattleDomeArea[y][x] === 1) {
+ const dComp = getEditorQuizBattleDomeCompAt(x, y);
+ if (dComp > 0) {
+ setQuizBattleGateQuestionSelect(dComp);
+ const snip = quizBattleGateQuestionSnippet(dComp);
+ if (statusEl) {
+ statusEl.textContent = 'เลือกคำถามข้อ ' + dComp + (snip ? (' — 「' + snip + '」') : '') + ' · คลิกเส้นทางม่วงเพื่อวางประตู';
+ }
+ }
+ return;
+ }
+ if (!quizBattlePathArea[y] || quizBattlePathArea[y][x] !== 1) {
+ if (statusEl) statusEl.textContent = 'ประตูต้องวางบนเส้นทางม่วง (คลิกโดม E1/E2… เพื่อเลือกคำถาม)';
+ return;
+ }
+ if (left) {
+ const st = readQuizBattleGateStationInput();
+ quizBattlePathGateArea[y][x] = st;
+ const snip = quizBattleGateQuestionSnippet(st);
+ if (statusEl) {
+ statusEl.textContent = 'ประตูข้อ ' + st + (snip ? (' 「' + snip + '」') : '') + ' → (' + x + ',' + y + ')';
+ }
+ const next = editorQuizBattleMcqList.find((q) => q.compId > st);
+ if (next) setQuizBattleGateQuestionSelect(next.compId);
+ } else {
+ quizBattlePathGateArea[y][x] = 0;
+ if (statusEl) statusEl.textContent = 'ลบประตูช่อง (' + x + ',' + y + ')';
+ }
} else if (drawModeEl.value === 'carryEmbedCountdown') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_carry') return;
ensureQuizCarryAreas();
@@ -3613,7 +3832,7 @@
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gameType !== 'space_shooter' && gameType !== 'jump_survive') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossBossPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
- if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath') && gameType !== 'quiz_battle') drawModeEl.value = 'wall';
+ if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath' || drawModeEl.value === 'quizBattlePathGate') && gameType !== 'quiz_battle') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn' && !supportsLobbySpawnPaint(gameType)) drawModeEl.value = 'wall';
toggleFroggerUI();
syncEditorStackHudMock();
@@ -3740,6 +3959,7 @@
ensureQuizCarryAreas();
ensureQuizBattleDomeArea();
ensureQuizBattlePathArea();
+ ensureQuizBattlePathGateArea();
ensureJumpSurvivePlatformArea();
ensureJumpSurviveHazardArea();
ensureShooterSpawnSlots();
@@ -3802,6 +4022,10 @@
})(),
quizBattleDomeArea: gameType === 'quiz_battle' ? quizBattleDomeArea.map(r => r.slice()) : [],
quizBattlePathArea: gameType === 'quiz_battle' ? quizBattlePathArea.map(r => r.slice()) : [],
+ quizBattlePathGateArea: gameType === 'quiz_battle' ? quizBattlePathGateArea.map(r => r.slice()) : [],
+ quizBattleCategoryId: gameType === 'quiz_battle' && editorQuizBattleCategoryId
+ ? String(editorQuizBattleCategoryId).trim()
+ : undefined,
stackReleaseArea: gameType === 'stack' ? stackReleaseArea.map(r => r.slice()) : [],
stackLandArea: gameType === 'stack' ? stackLandArea.map(r => r.slice()) : [],
jumpSurvivePlatforms: gameType === 'jump_survive' ? jumpSurvivePlatforms.slice() : [],
@@ -3927,6 +4151,17 @@
const gt = gameTypeEl ? gameTypeEl.value : gameType;
bindWrap.style.display = (drawModeEl.value === 'cellImage' && gt === 'quiz_carry') ? 'flex' : 'none';
}
+ const gateWrap = document.getElementById('quiz-battle-gate-wrap');
+ if (gateWrap) {
+ const gt = gameTypeEl ? gameTypeEl.value : gameType;
+ const showGate = drawModeEl.value === 'quizBattlePathGate' && gt === 'quiz_battle';
+ gateWrap.style.display = showGate ? 'inline-flex' : 'none';
+ if (showGate && !editorQuizBattleMcqList.length) loadEditorQuizBattleMcqList();
+ }
+ }
+ const gateReloadBtn = document.getElementById('quiz-battle-gate-reload');
+ if (gateReloadBtn) {
+ gateReloadBtn.addEventListener('click', () => { loadEditorQuizBattleMcqList(); });
}
if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); syncLobbySpawnAuxUi(); });
syncDrawModeAuxUi();
@@ -4119,6 +4354,12 @@
: Array(height).fill(0).map(() => Array(width).fill(0));
quizBattleDomeArea = m.quizBattleDomeArea && m.quizBattleDomeArea.length ? m.quizBattleDomeArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizBattlePathArea = m.quizBattlePathArea && m.quizBattlePathArea.length ? m.quizBattlePathArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
+ quizBattlePathGateArea = m.quizBattlePathGateArea && m.quizBattlePathGateArea.length ? m.quizBattlePathGateArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
+ editorQuizBattleCategoryId = m.quizBattleCategoryId
+ ? String(m.quizBattleCategoryId).trim()
+ : inferQuizBattleCategoryFromMapId(mapId);
+ rebuildEditorQuizBattleDomeComp();
+ if (m.gameType === 'quiz_battle') loadEditorQuizBattleMcqList();
jumpSurvivePlatforms = Array.isArray(m.jumpSurvivePlatforms) ? m.jumpSurvivePlatforms.map((p) => (p && typeof p === 'object' ? { ...p } : null)).filter(Boolean) : [];
jumpSurvivePlatformArea = m.jumpSurvivePlatformArea && m.jumpSurvivePlatformArea.length ? m.jumpSurvivePlatformArea.map((r) => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
jumpSurvivePlatformVariantArea = m.jumpSurvivePlatformVariantArea && m.jumpSurvivePlatformVariantArea.length
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 5317f05..9d64bd9 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -12541,6 +12541,213 @@
md.quizBattleDomeComp = comp;
}
+ function hasQuizBattleManualPathGatesPlay(md) {
+ const mg = md && md.quizBattlePathGateArea;
+ if (!mg || !Array.isArray(mg)) return false;
+ for (let ty = 0; ty < mg.length; ty++) {
+ const row = mg[ty];
+ if (!row) continue;
+ for (let tx = 0; tx < row.length; tx++) {
+ if ((row[tx] | 0) > 0) return true;
+ }
+ }
+ return false;
+ }
+
+ function buildQuizBattlePathGateFromManualPlay(md) {
+ const w = md.width || 20, h = md.height || 15;
+ const path = md.quizBattlePathArea;
+ const dome = md.quizBattleDomeArea;
+ const manual = md.quizBattlePathGateArea;
+ const gate = Array(h).fill(0).map(() => Array(w).fill(0));
+ const bestReq = Array(h).fill(0).map(() => Array(w).fill(-1));
+ const queue = [];
+
+ function enqueue(tx, ty, req) {
+ if (tx < 0 || ty < 0 || tx >= w || ty >= h) return;
+ const onPath = path[ty] && path[ty][tx] === 1;
+ const onDome = dome && dome[ty] && dome[ty][tx] === 1;
+ if (!onPath && !onDome) return;
+ if (bestReq[ty][tx] >= 0 && req >= bestReq[ty][tx]) return;
+ bestReq[ty][tx] = req;
+ queue.push({ tx, ty, req });
+ }
+
+ const sp = md.spawn || { x: 1, y: 1 };
+ const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0));
+ const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0));
+ enqueue(stx, sty, 0);
+ if (bestReq[sty][stx] < 0) {
+ for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) enqueue(stx + dx, sty + dy, 0);
+ }
+ const sa = md.spawnArea;
+ if (sa && Array.isArray(sa)) {
+ for (let ty = 0; ty < h; ty++) {
+ const row = sa[ty];
+ if (!row) continue;
+ for (let tx = 0; tx < w; tx++) {
+ if (row[tx] === 1) enqueue(tx, ty, 0);
+ }
+ }
+ }
+
+ while (queue.length) {
+ const cur = queue.shift();
+ const { tx, ty, req } = cur;
+ if (bestReq[ty][tx] !== req) continue;
+ const onDome = dome && dome[ty] && dome[ty][tx] === 1;
+ let enterReq = req;
+ const mg = manual && manual[ty] && manual[ty][tx] ? (manual[ty][tx] | 0) : 0;
+ if (onDome) enterReq = 0;
+ else if (mg > 0) enterReq = Math.max(enterReq, mg);
+ if (path[ty] && path[ty][tx] === 1) gate[ty][tx] = enterReq;
+ let outReq = enterReq;
+ if (mg > 0) outReq = Math.max(outReq, mg);
+ for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
+ enqueue(tx + dx, ty + dy, outReq);
+ }
+ }
+ md.quizBattlePathGateRequired = gate;
+ }
+
+ /** ประตูบนเลน — เลนหลังโดม C ต้องตอบถูกโดม 1..C ก่อน (ยืนบนโดมได้โดยไม่ต้องตอบโดมนั้น) */
+ function buildQuizBattlePathGateRequiredPlay(md) {
+ if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActive(md)) {
+ if (md) md.quizBattlePathGateRequired = null;
+ return;
+ }
+ if (hasQuizBattleManualPathGatesPlay(md)) {
+ buildQuizBattlePathGateFromManualPlay(md);
+ return;
+ }
+ normalizeQuizBattleDomeInPlay(md);
+ const w = md.width || 20, h = md.height || 15;
+ const path = md.quizBattlePathArea;
+ const dome = md.quizBattleDomeArea;
+ const domeComp = md.quizBattleDomeComp;
+ const gate = Array(h).fill(0).map(() => Array(w).fill(0));
+ const dist = Array(h).fill(0).map(() => Array(w).fill(-1));
+ const queue = [];
+ const sp = md.spawn || { x: 1, y: 1 };
+ const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0));
+ const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0));
+
+ function tryEnqueue(tx, ty, d) {
+ if (tx < 0 || ty < 0 || tx >= w || ty >= h) return;
+ if (dist[ty][tx] >= 0) return;
+ const onPath = path[ty] && path[ty][tx] === 1;
+ const onDome = dome && dome[ty] && dome[ty][tx] === 1;
+ if (!onPath && !onDome) return;
+ dist[ty][tx] = d;
+ queue.push({ tx, ty, d });
+ }
+
+ tryEnqueue(stx, sty, 0);
+ if (dist[sty][stx] < 0) {
+ for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) tryEnqueue(stx + dx, sty + dy, 0);
+ }
+ const sa = md.spawnArea;
+ if (sa && Array.isArray(sa)) {
+ for (let ty = 0; ty < h; ty++) {
+ const row = sa[ty];
+ if (!row) continue;
+ for (let tx = 0; tx < w; tx++) {
+ if (row[tx] === 1) tryEnqueue(tx, ty, 0);
+ }
+ }
+ }
+
+ const domeMinDist = {};
+ let maxComp = 0;
+ while (queue.length) {
+ const cur = queue.shift();
+ const { tx, ty, d } = cur;
+ const dc = domeComp[ty] && domeComp[ty][tx] ? domeComp[ty][tx] : 0;
+ if (dc > 0) {
+ maxComp = Math.max(maxComp, dc);
+ if (domeMinDist[dc] == null || d < domeMinDist[dc]) domeMinDist[dc] = d;
+ }
+ for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
+ const nx = tx + dx, ny = ty + dy;
+ if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
+ if (dist[ny][nx] >= 0) continue;
+ const onPath = path[ny] && path[ny][nx] === 1;
+ const onDome = dome && dome[ny] && dome[ny][nx] === 1;
+ if (onPath || onDome) tryEnqueue(nx, ny, d + 1);
+ }
+ }
+
+ for (let ty = 0; ty < h; ty++) {
+ for (let tx = 0; tx < w; tx++) {
+ if (!path[ty] || path[ty][tx] !== 1) continue;
+ const d = dist[ty][tx];
+ if (d < 0) {
+ gate[ty][tx] = maxComp;
+ continue;
+ }
+ let req = 0;
+ for (let c = 1; c <= maxComp; c++) {
+ if (domeMinDist[c] != null && domeMinDist[c] < d) req = Math.max(req, c);
+ }
+ gate[ty][tx] = req;
+ }
+ }
+ md.quizBattlePathGateRequired = gate;
+ }
+
+ function quizBattlePathGateRequiredAtPlay(md, px, py) {
+ const gate = md && md.quizBattlePathGateRequired;
+ if (!gate || !quizBattlePathModeActive(md)) return 0;
+ const path = md.quizBattlePathArea;
+ const tiles = quizBattleSpriteBoundsTilesPlay(md, px, py);
+ let maxReq = 0;
+ for (const k of tiles) {
+ const p = k.split(',');
+ const tx = +p[0], ty = +p[1];
+ if (!path[ty] || path[ty][tx] !== 1) continue;
+ maxReq = Math.max(maxReq, gate[ty][tx] || 0);
+ }
+ return maxReq;
+ }
+
+ function quizBattleFirstMissingGateCompPlay(md, px, py, solvedSet) {
+ const maxReq = quizBattlePathGateRequiredAtPlay(md, px, py);
+ if (maxReq <= 0) return 0;
+ for (let c = 1; c <= maxReq; c++) {
+ if (!solvedSet.has(c)) return c;
+ }
+ return 0;
+ }
+
+ function quizBattlePathGateAllowsPlay(md, px, py, solvedSet) {
+ if (quizBattleFootprintOnDomePlay(md, px, py)) return true;
+ return quizBattleFirstMissingGateCompPlay(md, px, py, solvedSet) === 0;
+ }
+
+ let quizBattleGateFlashAt = 0;
+ function quizBattleGateQuestionSnippetPlay(compId) {
+ if (!(compId > 0) || !quizBattleMcqPool.length) return '';
+ const q = quizBattleMcqPool[(compId - 1) % quizBattleMcqPool.length];
+ if (!q || !q.text) return '';
+ const t = String(q.text).trim();
+ return t.length > 42 ? t.slice(0, 42) + '…' : t;
+ }
+
+ function quizBattleFlashGateBlocked(md, px, py, solvedSet) {
+ const need = quizBattleFirstMissingGateCompPlay(md, px, py, solvedSet);
+ if (!need) return;
+ const now = performance.now();
+ if (now - quizBattleGateFlashAt < 1400) return;
+ quizBattleGateFlashAt = now;
+ const snip = quizBattleGateQuestionSnippetPlay(need);
+ flashQuizBattleFeedback(
+ snip
+ ? ('ตอบข้อ ' + need + ' ก่อนผ่าน: 「' + snip + '」')
+ : ('ตอบคำถามข้อ ' + need + ' ก่อนผ่านทางนี้'),
+ false
+ );
+ }
+
/** เส้นทาง Quiz Battle — วาดอย่างน้อย 1 ช่องแล้วจะเดินได้เฉพาะบนเส้นทาง (เปลี่ยนแค่รูปพื้นหลังได้) */
function normalizeQuizBattlePathInPlay(md) {
if (!md || md.gameType !== 'quiz_battle') return;
@@ -12767,6 +12974,41 @@
return Math.min.apply(null, arr);
}
+ function quizBattleFootprintOnDomePlay(md, px, py) {
+ const dome = md && md.quizBattleDomeArea;
+ if (!dome) return false;
+ for (const k of quizTilesFootprintPlay(px, py)) {
+ const p = k.split(',');
+ const tx = +p[0], ty = +p[1];
+ if (dome[ty] && dome[ty][tx] === 1) return true;
+ }
+ return false;
+ }
+
+ /** โดมใต้เท้า หรือช่องติดกัน (8 ทิศ) — ให้กด E / เด้งคำถามได้แม้ยืนข้างสถานี */
+ function getNearestQuizBattleDomeCompAt(px, py) {
+ const direct = getPrimaryQuizBattleDomeCompAt(px, py);
+ if (direct != null) return direct;
+ if (!mapData || !isQuizBattle()) return null;
+ const dome = mapData.quizBattleDomeArea;
+ const comp = mapData.quizBattleDomeComp;
+ if (!dome) return null;
+ const w = mapData.width || 20, h = mapData.height || 15;
+ let best = null;
+ for (const k of quizTilesFootprintPlay(px, py)) {
+ const p = k.split(',');
+ const tx = +p[0], ty = +p[1];
+ for (const [dx, dy] of [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [-1, 1], [1, -1], [-1, -1]]) {
+ const nx = tx + dx, ny = ty + dy;
+ if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
+ if (!dome[ny] || dome[ny][nx] !== 1) continue;
+ const cid = comp && comp[ny] && comp[ny][nx] ? comp[ny][nx] : 1;
+ if (best == null || cid < best) best = cid;
+ }
+ }
+ return best;
+ }
+
function showQuizBattleMcqModal(q) {
const ov = document.getElementById('quiz-battle-mcq-overlay');
const textEl = document.getElementById('quiz-battle-mcq-text');
@@ -12799,12 +13041,15 @@
const ok = Number(q.correctIndex) === Number(idx);
if (ok) {
quizBattleAnsweredComps.add(compId);
+ if (typeof socket !== 'undefined' && socket && socket.connected) {
+ try { socket.emit('quizbattle-solve', { compId: compId }, function () {}); } catch (e) { /* ignore */ }
+ }
if (myId != null) {
if (!playLiveQuizScores) playLiveQuizScores = {};
playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1;
renderPlayQuizScoreboard(playLiveQuizScores);
}
- flashQuizBattleFeedback('ถูกต้อง · +1', true);
+ flashQuizBattleFeedback('ถูกต้อง · +1 · ทางไปข้างหน้าเปิดแล้ว', true);
} else {
flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false);
}
@@ -12812,15 +13057,15 @@
hideQuizBattleMcqModal();
}
- /** หา comp โดมที่ตัวละครเหยียบอยู่ — เช็คหลายจุด (กลาง/เท้า/หัว/ซ้าย-ขวา) ให้เด้งง่ายเมื่อถึงสถานี */
+ /** หา comp โดมที่ตัวละครเหยียบอยู่ — เช็คหลายจุด + ช่องติดโดม ให้เด้งง่ายเมื่อถึงสถานี */
function quizBattleDomeAtMe() {
if (!isQuizBattle()) return null;
- const pts = [[me.x, me.y], [me.x, me.y + 0.45], [me.x, me.y - 0.25], [me.x + 0.3, me.y], [me.x - 0.3, me.y]];
+ const pts = [[me.x, me.y], [me.x, me.y + 0.45], [me.x, me.y - 0.25], [me.x + 0.35, me.y], [me.x - 0.35, me.y]];
for (let i = 0; i < pts.length; i++) {
- const c = getPrimaryQuizBattleDomeCompAt(pts[i][0], pts[i][1]);
+ const c = getNearestQuizBattleDomeCompAt(pts[i][0], pts[i][1]);
if (c != null) return c;
}
- return null;
+ return getNearestQuizBattleDomeCompAt(me.x, me.y);
}
/** ZEP-style: เหยียบสถานี (โดม) ที่ยังไม่ตอบ → เปิดคำถามอัตโนมัติ (ครั้งเดียวต่อการเข้า · เดินออกแล้วกลับมาใหม่ถึงเด้งอีก) */
@@ -12835,7 +13080,10 @@
qbAutoLastComp = compNow;
if (quizBattleAnsweredComps.has(compNow)) return;
const pool = quizBattleMcqPool;
- if (!pool.length) return;
+ if (!pool.length) {
+ flashQuizBattleFeedback('ยังไม่มีคำถาม — บันทึกใน Admin → Quiz Battle แล้วรีเฟรช', false);
+ return;
+ }
const q = pool[(compNow - 1) % pool.length];
if (!q) return;
quizBattleModalCompId = compNow;
@@ -12894,9 +13142,9 @@
function tryOpenQuizBattleFromKey() {
if (!isQuizBattle() || !mapData || myId == null) return;
- const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y);
+ const compId = quizBattleDomeAtMe();
if (compId == null) {
- flashQuizBattleFeedback('ยังไม่ยืนบนโดมคำถาม', false);
+ flashQuizBattleFeedback('เข้าใกล้สถานีคำถาม (หีบ) แล้วกด E', false);
return;
}
if (quizBattleAnsweredComps.has(compId)) {
@@ -15797,6 +16045,7 @@
if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = [];
normalizeQuizBattlePathInPlay(mapData);
normalizeQuizBattleDomeInPlay(mapData);
+ buildQuizBattlePathGateRequiredPlay(mapData);
}
tileSize = mapData.tileSize || 32;
mapBackgroundImg = null;
@@ -17074,6 +17323,7 @@
if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = [];
normalizeQuizBattlePathInPlay(mapData);
normalizeQuizBattleDomeInPlay(mapData);
+ buildQuizBattlePathGateRequiredPlay(mapData);
if (quizBattlePathModeActive(mapData)) {
const ms = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y);
me.x = ms.x;
@@ -17559,6 +17809,10 @@
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);
+ return false;
+ }
}
return true;
}
@@ -17607,6 +17861,8 @@
if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false;
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;
}
return true;
}
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 38b10ac..9fabc97 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -3974,7 +3974,7 @@
-
+
v —