Files
justice/www/html/Game/public/js/editor.js
T
giteaadmin 8ca841364a 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
2026-04-24 15:11:25 +00:00

1605 lines
87 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const BASE = '/Game';
const params = new URLSearchParams(window.location.search);
const mapId = params.get('id');
const editorEmbed = params.get('embed') === '1' || params.get('from') === 'admin';
if (editorEmbed) {
document.documentElement.classList.add('editor-embed-admin');
document.body.classList.add('editor-embed-admin');
}
function buildEditorSearch(nextMapId) {
const p = new URLSearchParams();
if (nextMapId) p.set('id', nextMapId);
if (editorEmbed) p.set('embed', '1');
const s = p.toString();
return s ? ('?' + s) : '';
}
const canvas = document.getElementById('editor-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const mapLoadEl = document.getElementById('map-load');
const mapName = document.getElementById('map-name');
const mapW = document.getElementById('map-w');
const mapH = document.getElementById('map-h');
const tileSizeEl = document.getElementById('tile-size');
const characterCellsWEl = document.getElementById('character-cells-w');
const characterCellsHEl = document.getElementById('character-cells-h');
const drawModeEl = document.getElementById('draw-mode');
const gameTypeEl = document.getElementById('game-type');
const statusEl = document.getElementById('editor-status');
const QUIZ_CARRY_EDITOR_OPT_COUNT = 16;
function quizCarryEditorDrawPalette(slot1based) {
const i = Math.max(0, Math.min(QUIZ_CARRY_EDITOR_OPT_COUNT - 1, slot1based - 1));
const h = (i * 47 + 100) % 360;
return {
fill: 'hsla(' + h + ',58%,48%,0.52)',
stroke: 'hsl(' + h + ',65%,42%)',
};
}
function quizCarryEditorCarryModes() {
const m = ['quizCarryHub'];
for (let n = 1; n <= QUIZ_CARRY_EDITOR_OPT_COUNT; n++) m.push('quizCarryOpt' + n);
return m;
}
let tileSize = 32, width = 20, height = 15, characterCellsW = 1, characterCellsH = 1;
function clampCharacterFootprint(n) {
const v = parseInt(n, 10);
if (!Number.isFinite(v)) return 1;
return Math.max(1, Math.min(4, v));
}
function readCharacterFootprintInputs() {
const cw = characterCellsWEl ? clampCharacterFootprint(characterCellsWEl.value) : characterCellsW;
const ch = characterCellsHEl ? clampCharacterFootprint(characterCellsHEl.value) : characterCellsH;
return { cw, ch };
}
function applyCharacterFootprintInputs(cw, ch) {
characterCellsW = clampCharacterFootprint(cw);
characterCellsH = clampCharacterFootprint(ch);
if (characterCellsWEl) characterCellsWEl.value = String(characterCellsW);
if (characterCellsHEl) characterCellsHEl.value = String(characterCellsH);
}
let gameType = 'zep';
let lanes = [];
let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 };
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [];
let quizCarryHubArea = [], quizCarryOptionArea = [];
let quizBattleDomeArea = [];
/** quiz_battle: กริด 0/1 — ถ้ามีอย่างน้อย 1 ช่อง = ในเกมเดินได้เฉพาะบนเส้นทาง */
let quizBattlePathArea = [];
let stackReleaseArea = [], stackLandArea = [];
/** jump_survive: แพลตฟอร์ม { x, y, w, h } tile — ใช้เต็มที่เมื่อมีมุมข้าง */
let jumpSurvivePlatforms = [];
let jumpSurvivePlatformArea = [];
let cellColors = [];
let gauntletPlayerSpawns = [];
/** space_shooter: กริด 0 หรือ 1–6 = ลำดับผู้เล่น P1–P6 */
let shooterSpawnSlots = [];
/** balloon_boss: กริด 0 หรือ 1–6 = จุดเกิดผู้เล่น · บอส = tile เดียว */
let balloonBossPlayerSlots = [];
let balloonBossBossSpawn = null;
let showMapInGame = true;
let backgroundImage = null;
let backgroundImageImg = null;
let isSpawnMode = false;
let editorFroggerAnimationId = null;
/** Bullet list for game-type hint (block layout; avoid one long inline line). */
function editorHintBulletList(items) {
if (!items || !items.length) return '';
return '<ul class="editor-hint-bullets">' + items.map(function (html) {
return '<li>' + html + '</li>';
}).join('') + '</ul>';
}
function ensureInteractive() {
if (!interactive.length || interactive.length !== height) {
const existing = interactive.slice().map(r => r && r.slice());
interactive = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
interactive.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!interactive[y] || interactive[y].length !== width) interactive[y] = Array(width).fill(0);
}
}
}
function ensureQuizAreas() {
if (!quizTrueArea.length || quizTrueArea.length !== height) {
const existingT = quizTrueArea.slice().map(r => r && r.slice());
quizTrueArea = [];
for (let y = 0; y < height; y++) {
const row = existingT[y] && existingT[y].length === width ? existingT[y].slice() : Array(width).fill(0);
quizTrueArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizTrueArea[y] || quizTrueArea[y].length !== width) quizTrueArea[y] = Array(width).fill(0);
}
}
if (!quizFalseArea.length || quizFalseArea.length !== height) {
const existingF = quizFalseArea.slice().map(r => r && r.slice());
quizFalseArea = [];
for (let y = 0; y < height; y++) {
const row = existingF[y] && existingF[y].length === width ? existingF[y].slice() : Array(width).fill(0);
quizFalseArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizFalseArea[y] || quizFalseArea[y].length !== width) quizFalseArea[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 ensureStackAreas() {
if (!stackReleaseArea.length || stackReleaseArea.length !== height) {
const existingR = stackReleaseArea.slice().map(r => r && r.slice());
stackReleaseArea = [];
for (let y = 0; y < height; y++) {
const row = existingR[y] && existingR[y].length === width ? existingR[y].slice() : Array(width).fill(0);
stackReleaseArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!stackReleaseArea[y] || stackReleaseArea[y].length !== width) stackReleaseArea[y] = Array(width).fill(0);
}
}
if (!stackLandArea.length || stackLandArea.length !== height) {
const existingL = stackLandArea.slice().map(r => r && r.slice());
stackLandArea = [];
for (let y = 0; y < height; y++) {
const row = existingL[y] && existingL[y].length === width ? existingL[y].slice() : Array(width).fill(0);
stackLandArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!stackLandArea[y] || stackLandArea[y].length !== width) stackLandArea[y] = Array(width).fill(0);
}
}
}
function ensureQuizCarryAreas() {
if (!quizCarryHubArea.length || quizCarryHubArea.length !== height) {
const existingH = quizCarryHubArea.slice().map(r => r && r.slice());
quizCarryHubArea = [];
for (let y = 0; y < height; y++) {
const row = existingH[y] && existingH[y].length === width ? existingH[y].slice() : Array(width).fill(0);
quizCarryHubArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizCarryHubArea[y] || quizCarryHubArea[y].length !== width) quizCarryHubArea[y] = Array(width).fill(0);
}
}
if (!quizCarryOptionArea.length || quizCarryOptionArea.length !== height) {
const existingO = quizCarryOptionArea.slice().map(r => r && r.slice());
quizCarryOptionArea = [];
for (let y = 0; y < height; y++) {
const row = existingO[y] && existingO[y].length === width ? existingO[y].slice() : Array(width).fill(0);
quizCarryOptionArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
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() {
if (!quizBattleDomeArea.length || quizBattleDomeArea.length !== height) {
const existing = quizBattleDomeArea.slice().map(r => r && r.slice());
quizBattleDomeArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
quizBattleDomeArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizBattleDomeArea[y] || quizBattleDomeArea[y].length !== width) quizBattleDomeArea[y] = Array(width).fill(0);
}
}
}
function ensureQuizBattlePathArea() {
if (!quizBattlePathArea.length || quizBattlePathArea.length !== height) {
const existing = quizBattlePathArea.slice().map(r => r && r.slice());
quizBattlePathArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
quizBattlePathArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!quizBattlePathArea[y] || quizBattlePathArea[y].length !== width) quizBattlePathArea[y] = Array(width).fill(0);
}
}
}
function ensureStartGameArea() {
if (!startGameArea.length || startGameArea.length !== height) {
const existing = startGameArea.slice().map(r => r && r.slice());
startGameArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
startGameArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!startGameArea[y] || startGameArea[y].length !== width) startGameArea[y] = Array(width).fill(0);
}
}
}
/** โหลดแมปแล้ว objects/ground แถวไม่ครบ → คลิกวาดพัง — เติมกริดให้ตรง width×height */
function ensureObjectsGroundGrids() {
const z = () => Array(width).fill(0);
const pad = (arr) => {
const ex = (arr && arr.slice) ? arr.map((r) => (r && r.slice ? r.slice() : null)) : [];
const out = [];
for (let y = 0; y < height; y++) {
const r = ex[y];
if (r && r.length === width) out.push(r.slice());
else {
const row = z();
if (r) for (let x = 0; x < Math.min(width, r.length); x++) if (r[x] != null) row[x] = r[x];
out.push(row);
}
}
return out;
};
objects = pad(objects);
ground = pad(ground);
}
function ensureShooterSpawnSlots() {
if (!shooterSpawnSlots.length || shooterSpawnSlots.length !== height) {
const existing = shooterSpawnSlots.slice().map((r) => r && r.slice());
shooterSpawnSlots = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
shooterSpawnSlots.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!shooterSpawnSlots[y] || shooterSpawnSlots[y].length !== width) {
shooterSpawnSlots[y] = Array(width).fill(0);
}
}
}
}
function ensureBalloonBossPlayerSlots() {
if (!balloonBossPlayerSlots.length || balloonBossPlayerSlots.length !== height) {
const existing = balloonBossPlayerSlots.slice().map((r) => r && r.slice());
balloonBossPlayerSlots = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
balloonBossPlayerSlots.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!balloonBossPlayerSlots[y] || balloonBossPlayerSlots[y].length !== width) {
balloonBossPlayerSlots[y] = Array(width).fill(0);
}
}
}
}
function ensureJumpSurvivePlatformArea() {
if (!jumpSurvivePlatformArea.length || jumpSurvivePlatformArea.length !== height) {
const existing = jumpSurvivePlatformArea.slice().map((r) => r && r.slice());
jumpSurvivePlatformArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
jumpSurvivePlatformArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!jumpSurvivePlatformArea[y] || jumpSurvivePlatformArea[y].length !== width) {
jumpSurvivePlatformArea[y] = Array(width).fill(0);
}
}
}
}
function sanitizeGauntletPlayerSpawns() {
gauntletPlayerSpawns = gauntletPlayerSpawns
.filter((s) => s && Number.isFinite(s.x) && Number.isFinite(s.y) &&
s.x >= 0 && s.x < width && s.y >= 0 && s.y < height &&
!(objects[s.y] && objects[s.y][s.x] === 1))
.map((s) => ({ x: Math.floor(s.x), y: Math.floor(s.y) }))
.slice(0, 6);
}
function ensureSpawnArea() {
if (!spawnArea.length || spawnArea.length !== height) {
const existing = spawnArea.slice().map(r => r && r.slice());
spawnArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
spawnArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!spawnArea[y] || spawnArea[y].length !== width) spawnArea[y] = Array(width).fill(0);
}
}
}
function buildDefaultLanes() {
const arr = [];
for (let y = 0; y < height; y++) {
if (y === 0) arr.push({ y: 0, type: 'goal' });
else if (y === height - 1) arr.push({ y: height - 1, type: 'spawn' });
else arr.push({ y, type: 'safe', speed: 1, dir: 1 });
}
return arr;
}
function ensureLanes() {
if (lanes.length !== height) {
const existing = lanes.slice();
lanes = [];
for (let y = 0; y < height; y++) {
const old = existing.find(l => l.y === y);
if (old) lanes.push({ ...old, y });
else lanes.push(y === 0 ? { y: 0, type: 'goal' } : y === height - 1 ? { y: height - 1, type: 'spawn' } : { y, type: 'safe', speed: 1, dir: 1 });
}
}
}
function getVehiclePositionsEditor(lane, timeMs) {
if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return [];
const speed = (lane.speed != null ? lane.speed : 1) * 0.05;
const dir = lane.dir === -1 ? -1 : 1;
const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5);
const period = width + 2;
const count = Math.max(2, Math.floor(period / spacing));
const positions = [];
for (let i = 0; i < count; i++) {
const p = i * spacing + (timeMs * speed * dir) / 60;
const pNorm = ((p % period) + period) % period;
const vx = pNorm - 1;
positions.push(Math.max(-1, Math.min(width, vx)));
}
return positions;
}
function renderLanesTable() {
const wrap = document.getElementById('frogger-lanes-table');
const froggerWrap = document.getElementById('frogger-lanes-wrap');
if (!wrap || !froggerWrap) return;
ensureLanes();
wrap.innerHTML = '';
const table = document.createElement('table');
table.innerHTML = '<thead><tr><th>แถว (y)</th><th>ประเภท</th><th>ความเร็ว</th><th>ทิศทาง</th><th>ระยะห่าง</th></tr></thead><tbody></tbody>';
const tbody = table.querySelector('tbody');
lanes.forEach((lane) => {
const tr = document.createElement('tr');
const isRoadOrWater = lane.type === 'road' || lane.type === 'water';
const spacingVal = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5);
tr.innerHTML =
'<td>' + lane.y + '</td>' +
'<td><select class="lane-type" data-y="' + lane.y + '">' +
'<option value="spawn"' + (lane.type === 'spawn' ? ' selected' : '') + '>จุดเกิด</option>' +
'<option value="safe"' + (lane.type === 'safe' ? ' selected' : '') + '>ปลอดภัย</option>' +
'<option value="road"' + (lane.type === 'road' ? ' selected' : '') + '>ถนน (รถ)</option>' +
'<option value="water"' + (lane.type === 'water' ? ' selected' : '') + '>น้ำ (ท่อนซุง)</option>' +
'<option value="goal"' + (lane.type === 'goal' ? ' selected' : '') + '>ปลายทาง</option>' +
'</select></td>' +
'<td><input type="number" class="lane-speed" data-y="' + lane.y + '" min="0.5" max="5" step="0.5" value="' + (lane.speed != null ? lane.speed : 1) + '" ' + (isRoadOrWater ? '' : ' disabled') + '></td>' +
'<td><select class="lane-dir" data-y="' + lane.y + '" ' + (isRoadOrWater ? '' : ' disabled') + '>' +
'<option value="1"' + (lane.dir === 1 ? ' selected' : '') + '>ขวา</option>' +
'<option value="-1"' + (lane.dir === -1 ? ' selected' : '') + '>ซ้าย</option>' +
'</select></td>' +
'<td><input type="number" class="lane-spacing" data-y="' + lane.y + '" min="1.5" max="8" step="0.5" value="' + spacingVal + '" title="ระยะห่างรถ/ท่อนซุง (ยิ่งมากยิ่งห่าง)" ' + (isRoadOrWater ? '' : ' disabled') + '></td>';
tbody.appendChild(tr);
});
wrap.appendChild(table);
wrap.querySelectorAll('.lane-type').forEach(sel => {
sel.addEventListener('change', function () {
const y = parseInt(this.dataset.y, 10);
const lane = lanes.find(l => l.y === y);
if (lane) { lane.type = this.value; lane.speed = lane.speed != null ? lane.speed : 1; lane.dir = lane.dir != null ? lane.dir : 1; if (lane.type === 'road' || lane.type === 'water') lane.spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5); renderLanesTable(); draw(); }
});
});
wrap.querySelectorAll('.lane-speed').forEach(inp => {
inp.addEventListener('input', function () { const y = parseInt(this.dataset.y, 10); const lane = lanes.find(l => l.y === y); if (lane) lane.speed = parseFloat(this.value) || 1; draw(); });
});
wrap.querySelectorAll('.lane-dir').forEach(sel => {
sel.addEventListener('change', function () { const y = parseInt(this.dataset.y, 10); const lane = lanes.find(l => l.y === y); if (lane) lane.dir = parseInt(this.value, 10) || 1; draw(); });
});
wrap.querySelectorAll('.lane-spacing').forEach(inp => {
inp.addEventListener('input', function () { const y = parseInt(this.dataset.y, 10); const lane = lanes.find(l => l.y === y); if (lane) { lane.spacing = parseFloat(this.value) || 2; draw(); } });
});
}
function toggleFroggerUI() {
const froggerWrap = document.getElementById('frogger-lanes-wrap');
const hint = document.getElementById('game-type-hint');
const legEl = document.getElementById('frogger-lanes-legend');
const dqTrue = document.getElementById('draw-mode-option-quiz-true');
const dqFalse = document.getElementById('draw-mode-option-quiz-false');
const dqQ = document.getElementById('draw-mode-option-quiz-question');
const dqStackR = document.getElementById('draw-mode-option-stack-release');
const dqStackL = document.getElementById('draw-mode-option-stack-land');
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');
if (!froggerWrap) return;
const gt = gameTypeEl ? gameTypeEl.value : 'zep';
if (dqTrue) dqTrue.hidden = gt !== 'quiz';
if (dqFalse) dqFalse.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';
dqCarryOpts.forEach(function (node) { node.hidden = !showCarry; });
if (dqBattleDome) {
dqBattleDome.hidden = gt !== 'quiz_battle';
if (gt !== 'quiz_battle' && drawModeEl && drawModeEl.value === 'quizBattleDome') drawModeEl.value = 'wall';
}
if (dqBattlePath) {
dqBattlePath.hidden = gt !== 'quiz_battle';
if (gt !== 'quiz_battle' && drawModeEl && drawModeEl.value === 'quizBattlePath') drawModeEl.value = 'wall';
}
if (hint) hint.style.removeProperty('display');
if (gt === 'frogger') {
froggerWrap.style.display = 'block';
if (legEl) legEl.innerHTML = 'แถวบน (y=0) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ <strong>ระยะห่าง</strong> (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)';
if (hint) {
hint.innerHTML = editorHintBulletList([
'แถวบน (y=0) = <strong>ปลายทาง</strong>',
'แถวล่าง = <strong>จุดเกิด</strong>',
'ตั้งถนน/น้ำด้านล่าง — ความเร็ว ทิศทาง ระยะห่างรถหรือท่อนซุง',
]);
}
ensureLanes();
renderLanesTable();
draw();
if (editorFroggerAnimationId != null) cancelAnimationFrame(editorFroggerAnimationId);
editorFroggerAnimationId = requestAnimationFrame(editorFroggerTick);
} else if (gt === 'gauntlet') {
froggerWrap.style.display = 'none';
if (legEl) legEl.innerHTML = '<strong>พรมแดงสุดท้าย:</strong> ลำดับเกิดกำหนดแค่ <strong>แถว (y)</strong> — ในเกมทุกคนยืน <strong>คอลัมน์ x เดียวกัน</strong> (ซ้ายสุดจากจุดที่วาด) ไม่เฉียง';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead">โหมด <strong>พรมแดงสุดท้าย</strong></p>' + editorHintBulletList([
'อุปสรรคเลนเฉพาะ<strong>แถวที่มีคน</strong> + เลเซอร์ทุกแถว · ชนแล้วถอยซ้าย',
'ลำดับเกิด <strong>16</strong> หรือช่องฟ้า',
'กระโดด: <kbd>Space</kbd> · <kbd>W</kbd> · <kbd>↑</kbd>',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'lobby') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = editorHintBulletList([
'วาดฉาก<strong>ห้องโถง</strong> — รอผู้เล่นและตกแต่งฉาก',
'<strong>พื้นที่เริ่มเกม (ส้ม)</strong> — host ยืนในโซนนี้ก่อนกดเริ่ม',
'<strong>พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน)</strong> — ผู้เล่นใหม่สุ่มเกิดในช่องที่วาด',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'quiz') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = editorHintBulletList([
'<strong>โซนถูก (ฟ้า)</strong> · <strong>โซนผิด (ชมพู)</strong> · <strong>พื้นที่คำถาม (ทอง)</strong>',
'<strong>พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน)</strong> — ผู้เล่นเข้าห้องสุ่มยืนในโซน',
'คำถามและเวลา — ตั้งที่ <strong>Admin → คำถามเกม</strong>',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'stack') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>Stack</strong> — สลับจังหวะลงตึก</p>' + editorHintBulletList([
'<strong>จุดปล่อย (ฟ้า)</strong> — แนวสวิง crane ด้านบน',
'<strong>จุดซ้อนตึก (ชมพู)</strong> — แพลตฟอร์มรองรับบล็อก',
'ในเกมไม่มีเดิน — กด <kbd>Space</kbd> เพื่อปล่อยบล็อก',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'quiz_carry') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>หยิบมาวาง</strong> — หยิบตัวเลือกแล้วไปส่งที่โซน interactive (เขียว)</p>' + editorHintBulletList([
'<strong>โซนกลาง (ม่วง)</strong> — กำแพง · ถ้าไม่วาดโซนทองด้านล่าง ข้อความคำถามจะโชว์บนโซนม่วงในเกม',
'<strong>พื้นที่โชว์ข้อความคำถาม (ทอง)</strong> — วาดโซนทองแยกได้ · ในเกมข้อความจะอยู่ตรงโซนที่วาด',
'<strong>โซน interactive (เขียว)</strong> — ยืนบนหรือชิดขอบแล้วกด <kbd>F</kbd> ส่งคำตอบ (วาดอย่างน้อย 1 ช่อง — ถ้าไม่วาดจะใช้ชิดโซนกลางแทน)',
'<strong>ตัวเลือก 116</strong> — ตรงกับลำดับ <code>choices</code> ใน Admin หรือแมป',
'เล่นพร้อมกันได้ · คำถามจากแมปหรือ Admin',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'jump_survive') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>กระโดดให้รอด</strong> — กำแพง + แพลตฟอร์มเลื่อนขึ้นในกรอบจอ</p>' + editorHintBulletList([
'<strong>กำแพง</strong> = ขอบซ้ายขวา/บน (ชนแล้วตายหรือออกนอกจอ) · <strong>โหมดวาดแพลตฟอร์ม</strong> (ฟ้าอมเขียว) = ท่อนยืนได้ — ตอนเล่นแพลตฟอร์มจะเลื่อนขึ้นในกรอด ไม่ลากทั้งฉาก',
'ในเกม: กระโดดสูงขึ้น · แพลตฟอร์มเลื่อนขึ้นแล้ว<strong>วนกลับมาด้านล่าง</strong> (คาบ = ความสูงแมป) · กล้องตามตัว — หลุดขอบบน/ล่างจอ = กลับจุดเกิด',
'พื้นที่สุ่มเกิด (ฟ้า) + ปุ่มตั้งจุดเกิด — แนะนำสูงหลายแถวเพื่อให้มีที่กระโดด',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'space_shooter') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>ยิงยานอวกาศ</strong> — หินตก · ผู้เล่น/บอทยิงขึ้น · สูงสุด 6 คน</p>' + editorHintBulletList([
'เลือกโหมดวาด <strong>จุดเกิดยาน 16</strong> แล้วเลือกหมายเลข P ด้านขวา — คลิกซ้ายวาง · คลิกขวาลบช่อง · ช่องเดียวต่อหมายเลข (วางซ้ำจะย้ายจุด)',
'ลำดับผู้เล่นในเกม: <strong>คุณก่อน</strong> แล้วตามรหัสผู้เล่นอื่น แล้วบอททดสอบ — ตรงกับ P1, P2, …',
'แนะนำ <strong>รูปพื้นหลัง</strong> อวกาศ + ปิดโชว์กริดในเกม — เล่น: A/D หรือ ←/→ เลื่อน · W/S หรือ ↑/↓ ขึ้นลง (ไม่เกินครึ่งล่างแมป) · Space ยิง',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'balloon_boss') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>ลูกโป้งยิงบอส (Mega Virus)</strong> — สูงสุด 6 คน · บอสกลางจอ</p>' + editorHintBulletList([
'โหมด <strong>จุดเกิดผู้เล่น 16</strong> + เลข P ด้านขวา — คลิกซ้ายวาง · ขวาลบ (เหมือนยิงยาน)',
'โหมด <strong>จุดเกิดบอส</strong> — คลิกช่อง <strong>หนึ่งช่อง</strong> เป็นตำแหน่งศูนย์กลางบอส (กะโหลก + โล่หกเหลี่ยม)',
'ในเกม: คุมแบบ<strong>ยานมีแรงเฉื่อย</strong> (กดทิศแล้วค่อยเร่ง ปล่อยแล้วยังไหล มีแรงหน่วง) — ไม่สแนปทันที · Space ยิงมี<strong>ดีเลย์</strong> · บอสยิงกลับ · ลูกโป้งหมด = ตกรอบ',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'quiz_battle') {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>Quiz Battle</strong> — เส้นทาง + โดมถาม A / B / C</p>' + editorHintBulletList([
'โหมด <strong>เส้นทางเดิน (ม่วง)</strong> — วาดซิกแซกเหมือนทางในเกม · ถ้ามีอย่างน้อย 1 ช่อง ผู้เล่น<strong>เดินได้เฉพาะบนเส้นทาง</strong> · ไม่วาดเลย = เดินอิสระทั้งแมป',
'โหมด <strong>โดมคำถาม</strong> — วางบนเส้นทาง (หรือทั่วแมปถ้าไม่ใช้เส้นทาง) · ในเกมกด <kbd>E</kbd>',
'รูป <strong>พื้นหลัง</strong> เปลี่ยนทีหลังได้ — ตรรกะเส้นทาง/โดมอยู่ที่กริด ไม่ผูกกราฟิก',
'ข้อสอบจาก <strong>Admin → Quiz Battle</strong> (<code>battleQuizMcq</code>) · แนะนำ <strong>พื้นที่สุ่มจุดเกิด</strong> ให้อยู่บนเส้นทางหรือใกล้จุดเริ่ม',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else {
froggerWrap.style.display = 'none';
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead">เลือกโหมดด้านบน — แต่ละโหมดวาดคนละแบบ</p>' + editorHintBulletList([
'<strong>ZEP</strong> — เดินอิสระบนแผนที่',
'<strong>กบข้ามถนน</strong> — ตารางแถว / ถนน น้ำ',
'<strong>พรมแดงสุดท้าย</strong> — กระโดดหลบ (ไม่ใช่ตารางแถวแบบกบ)',
'<strong>Stack</strong> — จุดปล่อย (ฟ้า) + จุดซ้อน (ชมพู)',
'<strong>กระโดดให้รอด</strong> — แพลตฟอร์มรอดชีวิต',
'<strong>ยิงยานอวกาศ</strong> — หินตก · จุดเกิด P1–P6',
'<strong>ลูกโป้งยิงบอส</strong> — จุดเกิดผู้เล่น + บอส · ดีเลย์ยิง · ลูกโป้ง = ชีวิต',
'<strong>Quiz Battle</strong> — โดม + กด <kbd>E</kbd> ตอบ A B C',
'<strong>ห้องโถง</strong> — พื้นที่เริ่มเกม (ส้ม) · <strong>ตอบคำถาม</strong> — โซนถูก–ผิด + คำถาม',
'<strong>หยิบมาวาง</strong> — โซนกลาง + ตัวเลือก 116 · <strong>ฟ้าอ่อน</strong> = สุ่มจุดเกิด',
]);
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
}
const sgOpt = document.getElementById('draw-mode-option-start-game');
if (sgOpt) {
const show = gt === 'lobby';
sgOpt.hidden = !show;
if (!show && drawModeEl && drawModeEl.value === 'startGame') drawModeEl.value = 'wall';
}
const saOpt = document.getElementById('draw-mode-option-spawn-area');
if (saOpt) {
const showSa = gt !== 'frogger';
saOpt.hidden = !showSa;
if (!showSa && drawModeEl && drawModeEl.value === 'spawnArea') drawModeEl.value = 'wall';
}
const gSpOpt = document.getElementById('draw-mode-option-gauntlet-player-spawn');
const btnClrG = document.getElementById('btn-clear-gauntlet-spawns');
if (gSpOpt) {
const showG = gt === 'gauntlet';
gSpOpt.hidden = !showG;
if (!showG && drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn') drawModeEl.value = 'wall';
}
if (btnClrG) btnClrG.hidden = gt !== 'gauntlet';
const shOpt = document.getElementById('draw-mode-option-shooter-spawn');
const shWrap = document.getElementById('shooter-slot-paint-wrap');
const btnClrSh = document.getElementById('btn-clear-shooter-spawns');
if (shOpt) {
shOpt.hidden = gt !== 'space_shooter';
if (gt !== 'space_shooter' && drawModeEl && drawModeEl.value === 'shooterSpawnPaint') drawModeEl.value = 'wall';
}
if (shWrap) shWrap.style.display = gt === 'space_shooter' ? '' : 'none';
if (btnClrSh) btnClrSh.hidden = gt !== 'space_shooter';
const bbPlayerOpt = document.getElementById('draw-mode-option-balloon-boss-player');
const bbBossOpt = document.getElementById('draw-mode-option-balloon-boss-boss');
const bbWrap = document.getElementById('balloon-boss-slot-paint-wrap');
const btnClrBb = document.getElementById('btn-clear-balloon-boss-spawns');
if (bbPlayerOpt) {
bbPlayerOpt.hidden = gt !== 'balloon_boss';
if (gt !== 'balloon_boss' && drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint') drawModeEl.value = 'wall';
}
if (bbBossOpt) {
bbBossOpt.hidden = gt !== 'balloon_boss';
if (gt !== 'balloon_boss' && drawModeEl && drawModeEl.value === 'balloonBossBossPaint') drawModeEl.value = 'wall';
}
if (bbWrap) bbWrap.style.display = gt === 'balloon_boss' ? '' : 'none';
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') && 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') {
drawModeEl.value = 'wall';
}
const carryModes = quizCarryEditorCarryModes();
if (drawModeEl && carryModes.indexOf(drawModeEl.value) >= 0 && gt !== 'quiz_carry') {
drawModeEl.value = 'wall';
}
if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath') && gt !== 'quiz_battle') {
drawModeEl.value = 'wall';
}
const jPlatOpt = document.getElementById('draw-mode-option-jump-platform');
if (jPlatOpt) {
jPlatOpt.hidden = gt !== 'jump_survive';
if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'wall';
}
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gt !== 'space_shooter') drawModeEl.value = 'wall';
}
function initGrid() {
width = Math.max(10, Math.min(50, parseInt(mapW.value, 10) || 20));
height = Math.max(10, Math.min(40, parseInt(mapH.value, 10) || 15));
tileSize = Math.max(16, Math.min(64, parseInt(tileSizeEl.value, 10) || 32));
ground = Array(height).fill(0).map(() => Array(width).fill(0));
objects = Array(height).fill(0).map(() => Array(width).fill(0));
blockPlayer = Array(height).fill(0).map(() => Array(width).fill(0));
interactive = Array(height).fill(0).map(() => Array(width).fill(0));
startGameArea = Array(height).fill(0).map(() => Array(width).fill(0));
spawnArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizTrueArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizFalseArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizQuestionArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizCarryHubArea = Array(height).fill(0).map(() => Array(width).fill(0));
quizCarryOptionArea = 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));
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));
shooterSpawnSlots = Array(height).fill(0).map(() => Array(width).fill(0));
balloonBossPlayerSlots = Array(height).fill(0).map(() => Array(width).fill(0));
balloonBossBossSpawn = null;
cellColors = Array(height).fill(0).map(() => Array(width).fill(null));
for (let i = 0; i < width; i++) { objects[0][i] = 1; objects[height - 1][i] = 1; }
for (let i = 0; i < height; i++) { objects[i][0] = 1; objects[i][width - 1] = 1; }
spawn = { x: 1, y: 1 };
if (gameType === 'frogger') { lanes = buildDefaultLanes(); ensureLanes(); }
if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = [];
jumpSurvivePlatforms = [];
gauntletPlayerSpawns = [];
const fpNew = readCharacterFootprintInputs();
applyCharacterFootprintInputs(fpNew.cw, fpNew.ch);
mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
resize(); draw();
toggleFroggerUI();
}
function resize() {
canvas.width = width * tileSize;
canvas.height = height * tileSize;
sanitizeGauntletPlayerSpawns();
ensureShooterSpawnSlots();
ensureBalloonBossPlayerSlots();
draw();
}
function getCell(e) {
const r = canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - r.left) / tileSize);
const y = Math.floor((e.clientY - r.top) / tileSize);
return { x: Math.max(0, Math.min(width - 1, x)), y: Math.max(0, Math.min(height - 1, y)) };
}
function draw() {
const gtDraw = gameTypeEl ? gameTypeEl.value : gameType;
const isFrogger = gtDraw === 'frogger';
const isGauntlet = gtDraw === 'gauntlet';
const isStack = gtDraw === 'stack';
const isQuizCarry = gtDraw === 'quiz_carry';
const isQuizBattle = gtDraw === 'quiz_battle';
const isJumpSurvive = gtDraw === 'jump_survive';
const isSpaceShooter = gtDraw === 'space_shooter';
const isBalloonBoss = gtDraw === 'balloon_boss';
const usesLanes = isFrogger;
if (usesLanes) ensureLanes();
ensureInteractive();
ensureStartGameArea();
ensureSpawnArea();
if (gtDraw === 'quiz') ensureQuizAreas();
if (gtDraw === 'quiz_carry') ensureQuizCarryAreas();
if (gtDraw === 'quiz_battle') {
ensureQuizBattlePathArea();
ensureQuizBattleDomeArea();
}
if (gtDraw === 'stack') ensureStackAreas();
if (gtDraw === 'jump_survive') ensureJumpSurvivePlatformArea();
if (gtDraw === 'space_shooter') ensureShooterSpawnSlots();
if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots();
if (backgroundImageImg && backgroundImageImg.complete && backgroundImageImg.naturalWidth) {
ctx.drawImage(backgroundImageImg, 0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = '#1a1b26';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
for (let y = 0; y < height; y++) {
var rowFill = null;
if (usesLanes && lanes[y]) {
if (lanes[y].type === 'goal') rowFill = 'rgba(158,206,106,0.35)';
else if (lanes[y].type === 'spawn') rowFill = 'rgba(187,154,247,0.35)';
else if (lanes[y].type === 'road') rowFill = 'rgba(120,100,80,0.5)';
else if (lanes[y].type === 'water') rowFill = 'rgba(125,207,255,0.4)';
} else if (isGauntlet) {
rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.1)' : 'rgba(160,80,120,0.08)';
} else if (isStack) {
rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.09)' : 'rgba(187, 154, 247, 0.07)';
} else if (isQuizCarry) {
rowFill = (y % 2 === 0) ? 'rgba(100, 180, 160, 0.06)' : 'rgba(80, 140, 200, 0.05)';
} else if (isQuizBattle) {
rowFill = (y % 2 === 0) ? 'rgba(120, 190, 255, 0.07)' : 'rgba(255, 120, 150, 0.06)';
} else if (isJumpSurvive) {
rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)';
} else if (isSpaceShooter) {
rowFill = (y % 2 === 0) ? 'rgba(25, 40, 95, 0.42)' : 'rgba(18, 28, 72, 0.48)';
} else if (isBalloonBoss) {
rowFill = (y % 2 === 0) ? 'rgba(35, 22, 68, 0.45)' : 'rgba(22, 14, 52, 0.5)';
}
for (let x = 0; x < width; x++) {
const tx = x * tileSize, ty = y * tileSize;
if (objects[y][x] === 1) {
ctx.fillStyle = 'rgba(65,72,104,0.9)';
ctx.fillRect(tx, ty, tileSize, tileSize);
ctx.strokeStyle = '#565f89';
ctx.strokeRect(tx, ty, tileSize, tileSize);
} else {
const cellColor = cellColors[y] && cellColors[y][x];
if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(tx, ty, tileSize, tileSize); }
else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(tx, ty, tileSize, tileSize); }
else if (!backgroundImageImg || !backgroundImageImg.complete) {
ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335';
ctx.fillRect(tx, ty, tileSize, tileSize);
}
}
if (blockPlayer[y] && blockPlayer[y][x] === 1) {
ctx.fillStyle = 'rgba(255,180,80,0.5)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255,180,80,0.9)';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
if (interactive[y] && interactive[y][x] === 1) {
ctx.fillStyle = 'rgba(158,206,106,0.45)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = '#9ece6a';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
if (startGameArea[y] && startGameArea[y][x] === 1) {
ctx.fillStyle = 'rgba(255, 158, 100, 0.5)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 120, 60, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
}
if (spawnArea[y] && spawnArea[y][x] === 1) {
ctx.fillStyle = 'rgba(122, 162, 247, 0.38)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(122, 162, 247, 0.92)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
}
if (gtDraw === 'quiz') {
if (quizQuestionArea[y] && quizQuestionArea[y][x] === 1) {
ctx.fillStyle = 'rgba(255, 214, 102, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(224, 185, 70, 0.95)';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
if (quizTrueArea[y] && quizTrueArea[y][x] === 1) {
ctx.fillStyle = 'rgba(86, 202, 255, 0.5)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(122, 220, 255, 0.95)';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
if (quizFalseArea[y] && quizFalseArea[y][x] === 1) {
ctx.fillStyle = 'rgba(247, 118, 190, 0.5)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 130, 200, 0.95)';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
}
if (gtDraw === 'stack') {
if (stackReleaseArea[y] && stackReleaseArea[y][x] === 1) {
ctx.fillStyle = 'rgba(125, 207, 255, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(122, 220, 255, 0.92)';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
}
if (stackLandArea[y] && stackLandArea[y][x] === 1) {
ctx.fillStyle = 'rgba(247, 118, 190, 0.42)';
ctx.fillRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.strokeStyle = 'rgba(255, 130, 200, 0.92)';
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
}
}
if (gtDraw === 'jump_survive' && jumpSurvivePlatformArea[y] && jumpSurvivePlatformArea[y][x] === 1) {
ctx.fillStyle = 'rgba(125, 235, 200, 0.48)';
ctx.fillRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.strokeStyle = 'rgba(160, 255, 220, 0.92)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.lineWidth = 1;
}
if (gtDraw === 'space_shooter' && shooterSpawnSlots[y] && shooterSpawnSlots[y][x] >= 1 && shooterSpawnSlots[y][x] <= 6) {
const sn = shooterSpawnSlots[y][x];
const hue = (sn * 52 + 180) % 360;
ctx.fillStyle = 'hsla(' + hue + ', 70%, 55%, 0.38)';
ctx.fillRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.strokeStyle = 'hsla(' + hue + ', 85%, 65%, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.lineWidth = 1;
ctx.fillStyle = '#e0f7ff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('P' + sn, tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (gtDraw === 'balloon_boss' && balloonBossPlayerSlots[y] && balloonBossPlayerSlots[y][x] >= 1 && balloonBossPlayerSlots[y][x] <= 6) {
const sn = balloonBossPlayerSlots[y][x];
const hue = (sn * 47 + 320) % 360;
ctx.fillStyle = 'hsla(' + hue + ', 72%, 52%, 0.4)';
ctx.fillRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.strokeStyle = 'hsla(' + hue + ', 88%, 62%, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.lineWidth = 1;
ctx.fillStyle = '#ffe0ff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('P' + sn, tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (gtDraw === 'balloon_boss' && balloonBossBossSpawn && balloonBossBossSpawn.x === x && balloonBossBossSpawn.y === y) {
ctx.fillStyle = 'rgba(255, 80, 120, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 120, 200, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = '#1a1b26';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('BOSS', tx + tileSize / 2, ty + tileSize / 2 + 4);
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)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(200, 170, 255, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
}
if (ov >= 1 && ov <= QUIZ_CARRY_EDITOR_OPT_COUNT) {
const pal = quizCarryEditorDrawPalette(ov);
ctx.fillStyle = pal.fill;
ctx.fillRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.strokeStyle = pal.stroke;
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.fillStyle = '#1a1b26';
const lab = String(ov);
ctx.font = lab.length > 1 ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(lab, tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
}
if (gtDraw === 'quiz_battle' && quizBattlePathArea[y] && quizBattlePathArea[y][x] === 1) {
ctx.fillStyle = 'rgba(186, 130, 255, 0.4)';
ctx.fillRect(tx + 1, ty + 1, tileSize - 2, tileSize - 2);
ctx.strokeStyle = 'rgba(210, 170, 255, 0.72)';
ctx.lineWidth = 1;
ctx.strokeRect(tx + 1, ty + 1, tileSize - 2, tileSize - 2);
}
if (gtDraw === 'quiz_battle' && quizBattleDomeArea[y] && quizBattleDomeArea[y][x] === 1) {
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)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = '#1a1b26';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('E', tx + tileSize / 2, ty + tileSize / 2 + 3);
ctx.textAlign = 'left';
}
}
}
/* จุดเกิด + footprint กว้าง×สูง — ต้องวาดหลังลูปกริด ไม่งั้นช่องข้างเคียงจะทับสีเขียวจนเหลือแค่มุม/ครึ่งวง */
if (spawn && spawn.x >= 0 && spawn.y >= 0 && spawn.x < width && spawn.y < height) {
const fp = readCharacterFootprintInputs();
const cw0 = fp.cw, ch0 = fp.ch;
const effW = Math.max(1, Math.min(cw0, width - spawn.x));
const effH = Math.max(1, Math.min(ch0, height - spawn.y));
const tx0 = spawn.x * tileSize;
const ty0 = spawn.y * tileSize;
const rw = effW * tileSize;
const rh = effH * tileSize;
const cx0 = tx0 + rw / 2;
const cy0 = ty0 + rh / 2;
ctx.fillStyle = 'rgba(158,206,106,0.35)';
ctx.fillRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2);
ctx.strokeStyle = '#9ece6a';
ctx.lineWidth = 2;
ctx.strokeRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2);
ctx.fillStyle = 'rgba(158,206,106,0.7)';
ctx.beginPath();
ctx.arc(cx0, cy0, Math.min(tileSize / 3, Math.min(rw, rh) / 4), 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#9ece6a';
ctx.stroke();
}
if (isFrogger && lanes.length) {
const timeMs = Date.now();
for (let y = 0; y < height; y++) {
const lane = lanes.find(l => l.y === y);
if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue;
const positions = getVehiclePositionsEditor(lane, timeMs);
const isRoad = lane.type === 'road';
for (let i = 0; i < positions.length; i++) {
const vx = positions[i];
const tx = vx * tileSize, ty = y * tileSize;
ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, (tileSize - 4) * 0.7);
ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a';
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, (tileSize - 4) * 0.7);
}
}
ctx.fillStyle = '#7aa2f7';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('โหมดกบข้ามถนน (จำลองรถ/ท่อนซุง)', 8, 20);
}
if (isGauntlet) {
ctx.fillStyle = '#f7768e';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(gauntletPlayerSpawns.length
? 'พรมแดง — ลำดับ 1–6 จากโหมด «ลำดับเกิด» (คลิกซ้ายตามลำดับ)'
: 'พรมแดง — ช่องฟ้า = ลำดับ 1→6 อัตโนมัติ (y บน→ล่าง) หรือใช้โหมดลำดับเกิดกำหนดเอง', 8, 20);
const drawGNum = (s, num) => {
const cx = s.x * tileSize + tileSize / 2;
const cy = s.y * tileSize + tileSize / 2 + 4;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.arc(cx, cy - 3, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(String(num), cx, cy + 2);
ctx.textAlign = 'left';
};
if (gauntletPlayerSpawns.length) {
for (let i = 0; i < gauntletPlayerSpawns.length; i++) {
const s = gauntletPlayerSpawns[i];
if (!s || s.x < 0 || s.x >= width || s.y < 0 || s.y >= height) continue;
if (objects[s.y][s.x] === 1) continue;
drawGNum(s, i + 1);
}
} else {
ensureSpawnArea();
const gsSlots = [];
for (let yy = 0; yy < height; yy++) {
for (let xx = 0; xx < width; xx++) {
if (spawnArea[yy] && spawnArea[yy][xx] === 1 && objects[yy][xx] !== 1) gsSlots.push({ x: xx, y: yy });
}
}
gsSlots.sort((a, b) => a.y - b.y || a.x - b.x);
for (let i = 0; i < Math.min(6, gsSlots.length); i++) drawGNum(gsSlots[i], i + 1);
}
}
if (gtDraw === 'lobby') {
ctx.fillStyle = '#bb9af7';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('โหมดห้องโถง (ฉากรอผู้เล่น)', 8, 20);
}
if (gtDraw === 'quiz') {
ctx.fillStyle = '#f7768e';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('โหมดตอบคำถาม (ถูก/ผิด)', 8, 20);
}
if (isStack) {
ctx.fillStyle = '#7dcfff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Stack — ฟ้า = ปล่อยบล็อก · ชมพู = แพลตฟอร์ม · บันทึกแล้วกดทดลองเล่น (Space ปล่อย)', 8, 20);
}
if (isQuizCarry) {
ctx.fillStyle = '#bb9af7';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('หยิบมาวาง — ม่วง = กลาง · 1–16 = ตัวเลือก · บันทึกแล้วทดลองเล่น', 8, 20);
}
if (isSpaceShooter) {
ctx.fillStyle = '#7df9ff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('ยิงยาน — วาดจุดเกิด P1–P6 · แนะนำพื้นหลังอวกาศ + ปิดโชว์กริดในเกม', 8, 20);
}
if (isBalloonBoss) {
ctx.fillStyle = '#ff79c6';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('ลูกโป้งยิงบอส — P1–P6 + โหมด BOSS 1 ช่อง · ปิดโชว์กริดในเกมให้เหมือนรีเฟอเรนซ์', 8, 20);
}
const fpHud = readCharacterFootprintInputs();
ctx.fillStyle = '#c0caf5';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('ขนาดตัวละคร: สูง ' + fpHud.ch + ' × กว้าง ' + fpHud.cw + ' ช่อง (แนวตั้ง×แนวนอน · มุมซ้ายบนจุดเกิด)', 8, canvas.height - 8);
}
function editorFroggerTick() {
const gt = gameTypeEl ? gameTypeEl.value : '';
if (gt !== 'frogger') return;
draw();
editorFroggerAnimationId = requestAnimationFrame(editorFroggerTick);
}
function getCellColorWithAlpha() {
const colorEl = document.getElementById('cell-color');
const alphaEl = document.getElementById('cell-alpha');
if (!colorEl) return null;
const hex = colorEl.value;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const a = alphaEl ? parseInt(alphaEl.value, 10) / 100 : 1;
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
}
function setCell(x, y, left) {
if (drawModeEl.value === 'gauntletPlayerSpawn') {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
if (gt !== 'gauntlet') return;
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดไม่ได้';
return;
}
if (left) {
if (gauntletPlayerSpawns.some((s) => s.x === x && s.y === y)) return;
if (gauntletPlayerSpawns.length >= 6) {
if (statusEl) statusEl.textContent = 'ครบ 6 จุดแล้ว — คลิกขวาที่ช่องเพื่อถอด หรือปุ่มล้างลำดับ';
return;
}
gauntletPlayerSpawns.push({ x, y });
if (statusEl) statusEl.textContent = 'พรมแดง: ผู้เล่น ' + gauntletPlayerSpawns.length + ' = ช่อง (' + x + ',' + y + ')';
} else {
const before = gauntletPlayerSpawns.length;
gauntletPlayerSpawns = gauntletPlayerSpawns.filter((s) => !(s.x === x && s.y === y));
if (statusEl) {
statusEl.textContent = before > gauntletPlayerSpawns.length ? 'ถอดจุดเกิดที่ช่องนี้แล้ว' : 'ไม่มีจุดเกิดที่ช่องนี้';
}
}
return;
}
if (drawModeEl.value === 'wall') {
objects[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'interactive') {
if (!interactive[y]) interactive[y] = Array(width).fill(0);
interactive[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'spawnArea') {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
if (gt === 'frogger') return;
ensureSpawnArea();
spawnArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'startGame') {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
if (gt !== 'lobby') return;
ensureStartGameArea();
startGameArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'quizTrue') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
ensureQuizAreas();
quizTrueArea[y][x] = left ? 1 : 0;
if (left) quizFalseArea[y][x] = 0;
} else if (drawModeEl.value === 'quizFalse') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
ensureQuizAreas();
quizFalseArea[y][x] = left ? 1 : 0;
if (left) quizTrueArea[y][x] = 0;
} else if (drawModeEl.value === 'quizQuestion') {
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();
stackReleaseArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'stackLand') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'stack') return;
ensureStackAreas();
stackLandArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'jumpSurvivePlatform') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'jump_survive') return;
ensureJumpSurvivePlatformArea();
jumpSurvivePlatformArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'shooterSpawnPaint') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'space_shooter') return;
ensureShooterSpawnSlots();
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดยานไม่ได้';
return;
}
const slotEl = document.getElementById('shooter-slot-paint');
const slot = Math.max(1, Math.min(6, parseInt(slotEl && slotEl.value, 10) || 1));
if (left) {
for (let yy = 0; yy < height; yy++) {
for (let xx = 0; xx < width; xx++) {
if (shooterSpawnSlots[yy] && shooterSpawnSlots[yy][xx] === slot) shooterSpawnSlots[yy][xx] = 0;
}
}
shooterSpawnSlots[y][x] = slot;
if (statusEl) statusEl.textContent = 'ยาน P' + slot + ' → ช่อง (' + x + ',' + y + ')';
} else {
shooterSpawnSlots[y][x] = 0;
if (statusEl) statusEl.textContent = 'ลบจุดเกิดยานที่ช่อง (' + x + ',' + y + ')';
}
return;
} else if (drawModeEl.value === 'balloonBossPlayerPaint') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'balloon_boss') return;
ensureBalloonBossPlayerSlots();
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดผู้เล่นไม่ได้';
return;
}
const slotEl = document.getElementById('balloon-boss-slot-paint');
const slot = Math.max(1, Math.min(6, parseInt(slotEl && slotEl.value, 10) || 1));
if (left) {
for (let yy = 0; yy < height; yy++) {
for (let xx = 0; xx < width; xx++) {
if (balloonBossPlayerSlots[yy] && balloonBossPlayerSlots[yy][xx] === slot) balloonBossPlayerSlots[yy][xx] = 0;
}
}
balloonBossPlayerSlots[y][x] = slot;
if (statusEl) statusEl.textContent = 'ผู้เล่น P' + slot + ' → ช่อง (' + x + ',' + y + ')';
} else {
balloonBossPlayerSlots[y][x] = 0;
if (statusEl) statusEl.textContent = 'ลบจุดเกิดผู้เล่นที่ช่อง (' + x + ',' + y + ')';
}
return;
} else if (drawModeEl.value === 'balloonBossBossPaint') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'balloon_boss') return;
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดบอสไม่ได้';
return;
}
if (left) {
balloonBossBossSpawn = { x: x, y: y };
if (statusEl) statusEl.textContent = 'บอส → ช่อง (' + x + ',' + y + ')';
} else {
balloonBossBossSpawn = null;
if (statusEl) statusEl.textContent = 'ลบจุดเกิดบอส (เกมจะใช้กลางแมป)';
}
return;
} else if (drawModeEl.value === 'quizBattlePath') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
ensureQuizBattlePathArea();
quizBattlePathArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'quizBattleDome') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
ensureQuizBattleDomeArea();
quizBattleDomeArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'quizCarryHub') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_carry') return;
ensureQuizCarryAreas();
quizCarryHubArea[y][x] = left ? 1 : 0;
if (left) quizCarryOptionArea[y][x] = 0;
} else {
const carryOptM = /^quizCarryOpt(\d+)$/.exec(drawModeEl.value);
if (carryOptM && (gameTypeEl ? gameTypeEl.value : gameType) === 'quiz_carry') {
ensureQuizCarryAreas();
const val = parseInt(carryOptM[1], 10);
if (val >= 1 && val <= QUIZ_CARRY_EDITOR_OPT_COUNT) {
if (left) {
quizCarryOptionArea[y][x] = val;
quizCarryHubArea[y][x] = 0;
} else {
quizCarryOptionArea[y][x] = 0;
}
}
} else if (drawModeEl.value === 'color') {
if (!cellColors[y]) cellColors[y] = Array(width).fill(null);
cellColors[y][x] = left ? getCellColorWithAlpha() : null;
} else {
if (!blockPlayer[y]) blockPlayer[y] = Array(width).fill(0);
blockPlayer[y][x] = left ? 1 : 0;
}
}
}
canvas.addEventListener('mousedown', (e) => {
const { x, y } = getCell(e);
if (isSpawnMode) {
if (objects[y][x] === 0) { spawn = { x, y }; draw(); statusEl.textContent = 'จุดเกิด: ' + x + ',' + y; }
isSpawnMode = false;
document.getElementById('btn-spawn').textContent = 'ตั้งจุดเกิด';
return;
}
setCell(x, y, e.button === 0);
draw();
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
canvas.addEventListener('mousemove', (e) => {
if (e.buttons !== 1 && e.buttons !== 2) return;
const { x, y } = getCell(e);
setCell(x, y, e.buttons === 1);
draw();
});
if (mapLoadEl) {
fetch(BASE + '/api/maps')
.then(r => r.json())
.then(list => {
mapLoadEl.innerHTML = '<option value="">— เลือกฉากเดิม —</option>';
(list || []).forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = (m.name || m.id) + ' (' + m.id + ')';
mapLoadEl.appendChild(opt);
});
if (mapId) mapLoadEl.value = mapId;
})
.catch(() => { mapLoadEl.innerHTML = '<option value="">— เลือกฉากเดิม —</option>'; });
document.getElementById('btn-load').addEventListener('click', () => {
const id = mapLoadEl && mapLoadEl.value;
if (id) window.location.href = 'editor.html' + buildEditorSearch(id);
});
}
const bgInput = document.getElementById('bg-image');
if (bgInput) {
bgInput.addEventListener('change', (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
const r = new FileReader();
r.onload = () => {
backgroundImage = r.result;
backgroundImageImg = new Image();
backgroundImageImg.onload = () => { draw(); };
backgroundImageImg.src = backgroundImage;
};
r.readAsDataURL(file);
e.target.value = '';
});
}
document.getElementById('btn-clear-bg').addEventListener('click', () => {
backgroundImage = null;
backgroundImageImg = null;
draw();
});
document.getElementById('btn-new').addEventListener('click', initGrid);
document.getElementById('btn-spawn').addEventListener('click', () => {
isSpawnMode = true;
document.getElementById('btn-spawn').textContent = 'คลิกช่องที่ต้องการ';
});
if (gameTypeEl) gameTypeEl.addEventListener('change', () => {
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') && 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';
if (drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gameType !== 'space_shooter') 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';
toggleFroggerUI();
});
const btnClearGauntletSpawns = document.getElementById('btn-clear-gauntlet-spawns');
if (btnClearGauntletSpawns) {
btnClearGauntletSpawns.addEventListener('click', () => {
gauntletPlayerSpawns = [];
if (statusEl) statusEl.textContent = 'ล้างลำดับเกิดพรมแดง (1–6) แล้ว';
draw();
});
}
const btnClearShooterSpawns = document.getElementById('btn-clear-shooter-spawns');
if (btnClearShooterSpawns) {
btnClearShooterSpawns.addEventListener('click', () => {
ensureShooterSpawnSlots();
for (let yy = 0; yy < height; yy++) {
for (let xx = 0; xx < width; xx++) shooterSpawnSlots[yy][xx] = 0;
}
if (statusEl) statusEl.textContent = 'ล้างจุดเกิดยานทั้งหมดแล้ว';
draw();
});
}
const btnClearBalloonBoss = document.getElementById('btn-clear-balloon-boss-spawns');
if (btnClearBalloonBoss) {
btnClearBalloonBoss.addEventListener('click', () => {
ensureBalloonBossPlayerSlots();
for (let yy = 0; yy < height; yy++) {
for (let xx = 0; xx < width; xx++) balloonBossPlayerSlots[yy][xx] = 0;
}
balloonBossBossSpawn = null;
if (statusEl) statusEl.textContent = 'ล้างจุดเกิดผู้เล่น + บอสแล้ว';
draw();
});
}
function openPlayTestMap() {
const id = mapId && String(mapId).trim();
if (!id) {
if (statusEl) statusEl.textContent = 'บันทึกฉากก่อน จึงจะทดลองเล่นได้ (ต้องมีรหัสฉากใน URL)';
try { alert('บันทึกฉากก่อน — ปุ่มทดลองเล่นต้องใช้รหัสฉากหลังบันทึก (แก้จากฉากเดิมแล้วกดบันทึก)'); } catch (e) { /* ignore */ }
return;
}
const nick = 'ทดสอบ' + Math.floor(Math.random() * 9000 + 1000);
if (statusEl) statusEl.textContent = 'กำลังสร้างห้องทดสอบ…';
var maxPl = (gameTypeEl && (gameTypeEl.value === 'gauntlet' || gameTypeEl.value === 'space_shooter' || gameTypeEl.value === 'balloon_boss')) ? 6
: (gameTypeEl && (gameTypeEl.value === 'stack' || gameTypeEl.value === 'quiz_carry' || gameTypeEl.value === 'quiz_battle' || gameTypeEl.value === 'jump_survive')) ? 8 : 4;
fetch(BASE + '/api/spaces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mapId: id, isPrivate: true, name: 'preview', maxPlayers: maxPl }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data || !data.ok) {
if (statusEl) statusEl.textContent = (data && data.error) || 'สร้างห้องทดสอบไม่ได้';
return;
}
var sid = data.spaceId;
var gt = gameTypeEl ? (gameTypeEl.value || 'zep') : 'zep';
/* โถง lobby ต้องใช้ room-lobby (มีปุ่มเริ่มเกม + โซนส้ม) — play.html ไม่มี UI host */
var page = gt === 'lobby' ? 'room-lobby.html' : 'play.html';
var q = page + '?space=' + encodeURIComponent(sid)
+ '&nick=' + encodeURIComponent(nick)
+ '&map=' + encodeURIComponent(id)
+ '&preview=1&defaultChar=1&editorEmbed=1';
if (statusEl) statusEl.textContent = 'เปิดหน้าทดสอบเล่นในแท็บใหม่แล้ว — ปิดแท็บเมื่อเล่นเสร็จ';
window.open(BASE + '/' + q, '_blank', 'noopener,noreferrer');
})
.catch(function () {
if (statusEl) statusEl.textContent = 'สร้างห้องทดสอบไม่ได้';
});
}
try { window.gameEditorOpenPlayTest = openPlayTestMap; } catch (e) { /* ignore */ }
var btnPlayTest = document.getElementById('btn-play-test');
if (btnPlayTest) btnPlayTest.addEventListener('click', openPlayTestMap);
document.getElementById('btn-save').addEventListener('click', () => {
const name = (mapName.value || '').trim() || 'ฉากใหม่';
gameType = gameTypeEl ? (gameTypeEl.value || 'zep') : 'zep';
if (gameType === 'frogger') ensureLanes();
ensureInteractive();
ensureStartGameArea();
ensureSpawnArea();
ensureQuizAreas();
ensureQuizCarryAreas();
ensureQuizBattleDomeArea();
ensureQuizBattlePathArea();
ensureJumpSurvivePlatformArea();
ensureShooterSpawnSlots();
ensureBalloonBossPlayerSlots();
ensureStackAreas();
const fpSave = readCharacterFootprintInputs();
applyCharacterFootprintInputs(fpSave.cw, fpSave.ch);
const showMapInGameEl = document.getElementById('show-map-in-game');
showMapInGame = showMapInGameEl ? showMapInGameEl.checked : true;
const body = {
name, gameType, width, height, tileSize,
characterCells: Math.max(fpSave.cw, fpSave.ch),
characterCellsW: fpSave.cw,
characterCellsH: fpSave.ch,
ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
quizTrueArea, quizFalseArea, quizQuestionArea,
quizCarryHubArea: gameType === 'quiz_carry' ? quizCarryHubArea.map(r => r.slice()) : [],
quizCarryOptionArea: gameType === 'quiz_carry' ? quizCarryOptionArea.map(r => r.slice()) : [],
quizBattleDomeArea: gameType === 'quiz_battle' ? quizBattleDomeArea.map(r => r.slice()) : [],
quizBattlePathArea: gameType === 'quiz_battle' ? quizBattlePathArea.map(r => r.slice()) : [],
stackReleaseArea: gameType === 'stack' ? stackReleaseArea.map(r => r.slice()) : [],
stackLandArea: gameType === 'stack' ? stackLandArea.map(r => r.slice()) : [],
jumpSurvivePlatforms: gameType === 'jump_survive' ? jumpSurvivePlatforms.slice() : [],
jumpSurvivePlatformArea: gameType === 'jump_survive' ? jumpSurvivePlatformArea.map((r) => r.slice()) : [],
shooterSpawnSlots: gameType === 'space_shooter' ? shooterSpawnSlots.map((r) => r.slice()) : [],
balloonBossPlayerSlots: gameType === 'balloon_boss' ? balloonBossPlayerSlots.map((r) => r.slice()) : [],
balloonBossBossSpawn: gameType === 'balloon_boss' && balloonBossBossSpawn && Number.isFinite(balloonBossBossSpawn.x)
? { x: Math.floor(balloonBossBossSpawn.x), y: Math.floor(balloonBossBossSpawn.y) }
: null,
};
if (backgroundImage) body.backgroundImage = backgroundImage;
body.lanes = gameType === 'frogger' ? lanes : [];
sanitizeGauntletPlayerSpawns();
body.gauntletPlayerSpawns = gameType === 'gauntlet'
? gauntletPlayerSpawns.map((s) => ({ x: s.x, y: s.y }))
: [];
const url = mapId ? BASE + '/api/maps/' + mapId : BASE + '/api/maps';
const method = mapId ? 'PUT' : 'POST';
fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
.then(r => r.json())
.then(data => {
if (data.ok) {
const id = data.mapId || mapId;
statusEl.textContent = 'บันทึกแล้ว รหัสฉาก: ' + id + (showMapInGame ? '' : ' (ไม่โชว์ฉาก) — ออกจากห้องแล้วเข้าใหม่ หรือกด F5 ในหน้าเล่น ถึงจะเห็นผล');
if (!mapId) window.history.replaceState({}, '', 'editor.html' + buildEditorSearch(id));
} else statusEl.textContent = data.error || 'บันทึกไม่ได้';
})
.catch(() => { statusEl.textContent = 'บันทึกไม่ได้'; });
});
const cellColorWrap = document.getElementById('cell-color-wrap');
const cellAlphaEl = document.getElementById('cell-alpha');
const cellAlphaValue = document.getElementById('cell-alpha-value');
if (drawModeEl) drawModeEl.addEventListener('change', () => {
if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
});
if (cellColorWrap && drawModeEl) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
if (cellAlphaEl && cellAlphaValue) cellAlphaEl.addEventListener('input', () => { cellAlphaValue.textContent = cellAlphaEl.value; draw(); });
mapW.addEventListener('change', resize);
mapH.addEventListener('change', resize);
tileSizeEl.addEventListener('change', resize);
if (characterCellsWEl) characterCellsWEl.addEventListener('input', draw);
if (characterCellsHEl) characterCellsHEl.addEventListener('input', draw);
if (mapId) {
fetch(BASE + '/api/maps/' + mapId)
.then(r => r.json())
.then(m => {
width = Math.max(10, Math.min(50, parseInt(m.width, 10) || 20));
height = Math.max(10, Math.min(40, parseInt(m.height, 10) || 15));
tileSize = Math.max(16, Math.min(64, parseInt(m.tileSize, 10) || 32));
const leg = Math.max(1, Math.min(4, m.characterCells || 1));
let cwL = Number(m.characterCellsW);
let chL = Number(m.characterCellsH);
cwL = Number.isFinite(cwL) ? Math.max(1, Math.min(4, Math.floor(cwL))) : leg;
chL = Number.isFinite(chL) ? Math.max(1, Math.min(4, Math.floor(chL))) : leg;
applyCharacterFootprintInputs(cwL, chL);
ground = m.ground || [];
objects = m.objects || [];
ensureObjectsGroundGrids();
blockPlayer = m.blockPlayer ? m.blockPlayer.map(r => r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
interactive = m.interactive && m.interactive.length ? m.interactive.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
startGameArea = m.startGameArea && m.startGameArea.length ? m.startGameArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
spawnArea = m.spawnArea && m.spawnArea.length ? m.spawnArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizTrueArea = m.quizTrueArea && m.quizTrueArea.length ? m.quizTrueArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizFalseArea = m.quizFalseArea && m.quizFalseArea.length ? m.quizFalseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizQuestionArea = m.quizQuestionArea && m.quizQuestionArea.length ? m.quizQuestionArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizCarryHubArea = m.quizCarryHubArea && m.quizCarryHubArea.length ? m.quizCarryHubArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
quizCarryOptionArea = m.quizCarryOptionArea && m.quizCarryOptionArea.length ? m.quizCarryOptionArea.map(r => r && r.slice()) : 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));
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));
ensureJumpSurvivePlatformArea();
shooterSpawnSlots = m.shooterSpawnSlots && m.shooterSpawnSlots.length
? m.shooterSpawnSlots.map((r) => r && r.slice())
: Array(height).fill(0).map(() => Array(width).fill(0));
ensureShooterSpawnSlots();
balloonBossPlayerSlots = m.balloonBossPlayerSlots && m.balloonBossPlayerSlots.length
? m.balloonBossPlayerSlots.map((r) => r && r.slice())
: Array(height).fill(0).map(() => Array(width).fill(0));
ensureBalloonBossPlayerSlots();
if (m.balloonBossBossSpawn && Number.isFinite(Number(m.balloonBossBossSpawn.x)) && Number.isFinite(Number(m.balloonBossBossSpawn.y))) {
balloonBossBossSpawn = { x: Math.floor(Number(m.balloonBossBossSpawn.x)), y: Math.floor(Number(m.balloonBossBossSpawn.y)) };
} else {
balloonBossBossSpawn = null;
}
stackReleaseArea = m.stackReleaseArea && m.stackReleaseArea.length ? m.stackReleaseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
stackLandArea = m.stackLandArea && m.stackLandArea.length ? m.stackLandArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
cellColors = (m.cellColors && m.cellColors.length === height) ? m.cellColors.map(r => r && r.slice ? r.slice() : Array(width).fill(null)) : Array(height).fill(0).map(() => Array(width).fill(null));
showMapInGame = m.showMapInGame !== false;
const showMapEl = document.getElementById('show-map-in-game');
if (showMapEl) showMapEl.checked = showMapInGame;
ensureInteractive();
ensureStartGameArea();
ensureSpawnArea();
backgroundImage = m.backgroundImage || null;
if (backgroundImage) {
backgroundImageImg = new Image();
backgroundImageImg.onload = () => { draw(); };
backgroundImageImg.src = backgroundImage;
} else backgroundImageImg = null;
spawn = m.spawn || { x: 1, y: 1 };
gauntletPlayerSpawns = Array.isArray(m.gauntletPlayerSpawns)
? m.gauntletPlayerSpawns.map((s) => ({ x: Math.floor(Number(s.x)), y: Math.floor(Number(s.y)) }))
.filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y))
: [];
gameType = m.gameType || 'zep';
if (gameTypeEl) gameTypeEl.value = gameType;
lanes = (m.lanes && m.lanes.length) ? m.lanes.map(l => ({ ...l })) : [];
if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = [];
if (gameType === 'frogger') {
ensureLanes();
if (lanes.length !== height) ensureLanes();
}
mapName.value = m.name || ''; mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
sanitizeGauntletPlayerSpawns();
resize(); draw();
toggleFroggerUI();
})
.catch(() => initGrid());
} else { initGrid(); toggleFroggerUI(); }
})();