fix(play+editor): quiz_carry question on map panel in embed + optional Q zone

play: show quiz-map-question-panel in editor embed for quiz_carry only; bounds
from quizQuestionArea when painted else hub; refactor tile bounds helper;
init quizQuestionArea on join; draw gold Q tiles in play; strip hidden for carry
editor: paint quizQuestion on quiz_carry, ensureQuizCarryAreas syncs quizQuestionArea,
toggle draw mode + hints; editor.html help + cache v0.040
play.js v=0.093

Made-with: Cursor
This commit is contained in:
2026-04-24 15:11:25 +00:00
parent 2cfc1eb2f5
commit 8ca841364a
4 changed files with 72 additions and 47 deletions
+2 -2
View File
@@ -173,7 +173,7 @@
<li><strong>พื้นที่สุ่มจุดเกิด</strong> (ฟ้าอ่อน) — ไม่วาดช่องนี้ได้ ใช้ปุ่ม «ตั้งจุดเกิด»</li>
<li><strong>พื้นที่เริ่มเกม</strong> (ห้องโถง — ส้ม) — host ยืนแล้วกดเริ่ม</li>
<li><strong>ถามตอบ</strong>: โซนถูก/ผิด + พื้นที่คำถาม (ทอง)</li>
<li><strong>หยิบมาวาง</strong>: ม่วง = โชว์คำถาม (กำแพง) · <strong>interactive เขียว</strong> = ยืนแล้วกด F ส่งคำตอบ · สีต่าง ๆ = ตัวเลือก 116 · คำถามในแมป <code>quizQuestions</code> ใส่ <code>choices</code> + <code>correctIndex</code> หรือ <code>answerTrue</code></li>
<li><strong>หยิบมาวาง</strong>: ม่วง = โซนกลาง (กำแพง) · <strong>ทอง</strong> = พื้นที่โชว์ข้อความคำถามบนแผนที่ (ถ้าไม่วาดทอง ข้อความไปที่โซนม่วง) · <strong>interactive เขียว</strong> = ยืนแล้วกด F ส่งคำตอบ · สี = ตัวเลือก 1–16 · <code>quizQuestions</code> ใส่ <code>choices</code> + <code>correctIndex</code> หรือ <code>answerTrue</code></li>
<li><strong>Stack</strong>: โหมดวาดจุดปล่อย (ฟ้า) · จุดซ้อนตึก (ชมพู)</li>
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
<li><strong>ลูกโป้งยิงบอส</strong>: จุดเกิดผู้เล่น P1–P6 + <strong>จุดเกิดบอส</strong> 1 ช่อง · ในเกมเดินช้า ยิงมีดีเลย์ · ลูกโป้งหมด = ตกรอบ</li>
@@ -188,7 +188,7 @@
</div>
</div>
<script src="js/version.js?v=0.0104"></script>
<script src="js/editor.js?v=0.039"></script>
<script src="js/editor.js?v=0.040"></script>
<div class="version-tag">v —</div>
</body>
</html>
+42 -7
View File
@@ -203,6 +203,18 @@
if (!quizCarryOptionArea[y] || quizCarryOptionArea[y].length !== width) quizCarryOptionArea[y] = Array(width).fill(0);
}
}
if (!quizQuestionArea.length || quizQuestionArea.length !== height) {
const existingQ = quizQuestionArea.slice().map(r => r && r.slice());
quizQuestionArea = [];
for (let y = 0; y < height; y++) {
const row = existingQ[y] && existingQ[y].length === width ? existingQ[y].slice() : Array(width).fill(0);
quizQuestionArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizQuestionArea[y] || quizQuestionArea[y].length !== width) quizQuestionArea[y] = Array(width).fill(0);
}
}
}
function ensureQuizBattleDomeArea() {
@@ -450,7 +462,7 @@
const gt = gameTypeEl ? gameTypeEl.value : 'zep';
if (dqTrue) dqTrue.hidden = gt !== 'quiz';
if (dqFalse) dqFalse.hidden = gt !== 'quiz';
if (dqQ) dqQ.hidden = gt !== 'quiz';
if (dqQ) dqQ.hidden = gt !== 'quiz' && gt !== 'quiz_carry';
if (dqStackR) dqStackR.hidden = gt !== 'stack';
if (dqStackL) dqStackL.hidden = gt !== 'stack';
const showCarry = gt === 'quiz_carry';
@@ -528,7 +540,8 @@
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>หยิบมาวาง</strong> — หยิบตัวเลือกแล้วไปส่งที่โซน interactive (เขียว)</p>' + editorHintBulletList([
'<strong>โซนกลาง (ม่วง)</strong> — แสดงคำถาม · ในเกมเป็นกำแพง (เดินเข้าไม่ได้)',
'<strong>โซนกลาง (ม่วง)</strong> — กำแพง · ถ้าไม่วาดโซนทองด้านล่าง ข้อความคำถามจะโชว์บนโซนม่วงในเกม',
'<strong>พื้นที่โชว์ข้อความคำถาม (ทอง)</strong> — วาดโซนทองแยกได้ · ในเกมข้อความจะอยู่ตรงโซนที่วาด',
'<strong>โซน interactive (เขียว)</strong> — ยืนบนหรือชิดขอบแล้วกด <kbd>F</kbd> ส่งคำตอบ (วาดอย่างน้อย 1 ช่อง — ถ้าไม่วาดจะใช้ชิดโซนกลางแทน)',
'<strong>ตัวเลือก 116</strong> — ตรงกับลำดับ <code>choices</code> ใน Admin หรือแมป',
'เล่นพร้อมกันได้ · คำถามจากแมปหรือ Admin',
@@ -645,7 +658,10 @@
if (btnClrBb) btnClrBb.hidden = gt !== 'balloon_boss';
if (drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint' && gt !== 'balloon_boss') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossBossPaint' && gt !== 'balloon_boss') drawModeEl.value = 'wall';
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse' || drawModeEl.value === 'quizQuestion') && gt !== 'quiz') {
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse') && gt !== 'quiz') {
drawModeEl.value = 'wall';
}
if (drawModeEl && drawModeEl.value === 'quizQuestion' && gt !== 'quiz' && gt !== 'quiz_carry') {
drawModeEl.value = 'wall';
}
if (drawModeEl && (drawModeEl.value === 'stackRelease' || drawModeEl.value === 'stackLand') && gt !== 'stack') {
@@ -903,6 +919,19 @@
ctx.textAlign = 'left';
}
if (gtDraw === 'quiz_carry') {
if (quizQuestionArea[y] && quizQuestionArea[y][x] === 1) {
ctx.fillStyle = 'rgba(255, 214, 102, 0.44)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(224, 185, 70, 0.96)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = '#1a1b26';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Q', tx + tileSize / 2, ty + tileSize / 2 + 3);
ctx.textAlign = 'left';
}
const ov = quizCarryOptionArea[y] && quizCarryOptionArea[y][x];
if (quizCarryHubArea[y] && quizCarryHubArea[y][x] === 1) {
ctx.fillStyle = 'rgba(187, 154, 247, 0.45)';
@@ -1145,9 +1174,14 @@
quizFalseArea[y][x] = left ? 1 : 0;
if (left) quizTrueArea[y][x] = 0;
} else if (drawModeEl.value === 'quizQuestion') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
ensureQuizAreas();
quizQuestionArea[y][x] = left ? 1 : 0;
const gtq = gameTypeEl ? gameTypeEl.value : gameType;
if (gtq === 'quiz') {
ensureQuizAreas();
quizQuestionArea[y][x] = left ? 1 : 0;
} else if (gtq === 'quiz_carry') {
ensureQuizCarryAreas();
quizQuestionArea[y][x] = left ? 1 : 0;
} else return;
} else if (drawModeEl.value === 'stackRelease') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'stack') return;
ensureStackAreas();
@@ -1324,7 +1358,8 @@
gameType = gameTypeEl.value;
if (drawModeEl && drawModeEl.value === 'startGame' && gameType !== 'lobby') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'spawnArea' && gameType === 'frogger') drawModeEl.value = 'wall';
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse' || drawModeEl.value === 'quizQuestion') && gameType !== 'quiz') drawModeEl.value = 'wall';
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse') && gameType !== 'quiz') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'quizQuestion' && gameType !== 'quiz' && gameType !== 'quiz_carry') drawModeEl.value = 'wall';
if (drawModeEl && (drawModeEl.value === 'stackRelease' || drawModeEl.value === 'stackLand') && gameType !== 'stack') drawModeEl.value = 'wall';
if (drawModeEl && quizCarryEditorCarryModes().indexOf(drawModeEl.value) >= 0 && gameType !== 'quiz_carry') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'jumpSurvivePlatform' && gameType !== 'jump_survive') drawModeEl.value = 'wall';
+27 -37
View File
@@ -688,9 +688,7 @@
window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200);
}
function getQuizQuestionAreaTileBounds(md) {
if (!md || md.gameType !== 'quiz') return null;
const grid = md.quizQuestionArea;
function getTileBoundsForOnesGrid(grid) {
if (!grid || !grid.length) return null;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (let yy = 0; yy < grid.length; yy++) {
@@ -709,12 +707,23 @@
return { minX, minY, maxX, maxY };
}
function getQuizQuestionAreaTileBounds(md) {
if (!md || md.gameType !== 'quiz') return null;
return getTileBoundsForOnesGrid(md.quizQuestionArea);
}
/** quiz_carry: โซนทองพิเศษสำหรับข้อความคำถาม (ถ้าไม่วาด ใช้โซนกลางม่วงแทนใน syncPlayQuizMapPanel) */
function getQuizCarryQuestionAreaTileBounds(md) {
if (!md || md.gameType !== 'quiz_carry') return null;
return getTileBoundsForOnesGrid(md.quizQuestionArea);
}
function syncPlayQuizMapPanel() {
const panel = document.getElementById('quiz-map-question-panel');
const textEl = document.getElementById('quiz-map-question-text');
if (!panel || !textEl || !mapData) return;
/* เอดิเตอร์ embed: อย่าวางแผงคำถามทับกลางจอ (quiz / quiz_carry ใช้กรอบทองนี้) — บังมองและรู้สึกเหมือนค้าง */
if (previewMode && editorEmbedReturn) {
/* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะโหมด quiz (ทับกลาง) — quiz_carry ต้องโชว์ตามโซนที่วาดบนแผนที่ */
if (previewMode && editorEmbedReturn && !isQuizCarry()) {
panel.classList.add('is-hidden');
panel.setAttribute('aria-hidden', 'true');
return;
@@ -727,7 +736,7 @@
if (!isQuiz() && !isQuizCarry()) return;
const bounds = isQuiz()
? getQuizQuestionAreaTileBounds(mapData)
: getQuizCarryHubTileBounds(mapData);
: (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData));
const text = (playQuizText || '').trim();
if (!bounds || !text) {
panel.classList.add('is-hidden');
@@ -749,41 +758,14 @@
panel.setAttribute('aria-hidden', 'false');
}
/** เอดิเตอร์ embed quiz_carry: แผงทองทับกลางถูกปิด — แสดงคำถามแถบล่างแทน (ไม่บังจอ) */
/** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */
function syncQuizCarryEmbedQuestionStrip() {
const wrap = document.getElementById('quiz-carry-embed-q-strip');
const p = document.getElementById('quiz-carry-embed-q-strip-text');
if (!wrap || !p) return;
if (!(previewMode && editorEmbedReturn && isQuizCarry())) {
wrap.classList.remove('quiz-carry-embed-q-strip--countdown');
wrap.classList.add('is-hidden');
wrap.setAttribute('aria-hidden', 'true');
return;
}
const blocking = isQuizCarryEmbedCountdownBlockingMovement();
const pending = quizCarryEmbedPendingQuestion;
let t = '';
if (blocking && pending) {
t = formatQuizCarryQuestionHud(pending);
if (t) wrap.classList.add('quiz-carry-embed-q-strip--countdown');
else wrap.classList.remove('quiz-carry-embed-q-strip--countdown');
} else if (blocking) {
wrap.classList.remove('quiz-carry-embed-q-strip--countdown');
wrap.classList.add('is-hidden');
wrap.setAttribute('aria-hidden', 'true');
return;
} else {
wrap.classList.remove('quiz-carry-embed-q-strip--countdown');
t = (playQuizText || '').trim();
}
if (!t) {
wrap.classList.add('is-hidden');
wrap.setAttribute('aria-hidden', 'true');
return;
}
p.textContent = t;
wrap.classList.remove('is-hidden');
wrap.setAttribute('aria-hidden', 'false');
wrap.classList.remove('quiz-carry-embed-q-strip--countdown');
wrap.classList.add('is-hidden');
wrap.setAttribute('aria-hidden', 'true');
}
function updatePlayQuizTimerDisplay() {
@@ -5027,6 +5009,7 @@
if (!mapData.quizCarryHubArea) mapData.quizCarryHubArea = [];
if (!mapData.quizCarryOptionArea) mapData.quizCarryOptionArea = [];
if (!mapData.quizQuestions) mapData.quizQuestions = [];
if (!mapData.quizQuestionArea) mapData.quizQuestionArea = [];
normalizeQuizCarryLayersInPlay(mapData);
}
if (mapData.gameType === 'quiz_battle') {
@@ -6944,6 +6927,13 @@
ctx.strokeStyle = 'rgba(200, 170, 255, 0.75)';
ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4);
}
const isCarryQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1;
if (isCarryQ) {
ctx.fillStyle = 'rgba(255, 214, 102, 0.3)';
ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4);
ctx.strokeStyle = 'rgba(224, 185, 70, 0.82)';
ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4);
}
const ov = mapData.quizCarryOptionArea && mapData.quizCarryOptionArea[y] && mapData.quizCarryOptionArea[y][x];
if (ov >= 1 && ov <= QUIZ_CARRY_MAX_OPTION_SLOTS) {
ctx.fillStyle = quizCarryMinimapOptionFillCss(ov);
+1 -1
View File
@@ -765,7 +765,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0166"></script>
<script src="js/play.js?v=0.091"></script>
<script src="js/play.js?v=0.093"></script>
<div class="version-tag">v —</div>
</body>
</html>