(function () {
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/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 characterCellsEl = document.getElementById('character-cells');
const drawModeEl = document.getElementById('draw-mode');
const gameTypeEl = document.getElementById('game-type');
const statusEl = document.getElementById('editor-status');
let tileSize = 32, width = 20, height = 15, characterCells = 1;
let gameType = 'zep';
let lanes = [];
let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 };
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [];
let cellColors = [];
let gauntletPlayerSpawns = [];
let showMapInGame = true;
let backgroundImage = null;
let backgroundImageImg = null;
let isSpawnMode = false;
let editorFroggerAnimationId = null;
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 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);
}
}
}
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 = '| แถว (y) | ประเภท | ความเร็ว | ทิศทาง | ระยะห่าง |
|---|
';
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 =
'' + lane.y + ' | ' +
' | ' +
' | ' +
' | ' +
' | ';
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');
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';
if (gt === 'frogger') {
froggerWrap.style.display = 'block';
if (legEl) legEl.innerHTML = 'แถวบน (y=0) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ ระยะห่าง (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)';
if (hint) { hint.style.display = 'inline'; hint.innerHTML = 'แถวบน = ปลายทาง · แถวล่าง = จุดเกิด · ตั้งถนน/น้ำด้านล่าง'; }
ensureLanes();
renderLanesTable();
draw();
if (editorFroggerAnimationId != null) cancelAnimationFrame(editorFroggerAnimationId);
editorFroggerAnimationId = requestAnimationFrame(editorFroggerTick);
} else if (gt === 'gauntlet') {
froggerWrap.style.display = 'none';
if (legEl) legEl.innerHTML = 'พรมแดงสุดท้าย: ลำดับเกิดกำหนดแค่ แถว (y) — ในเกมทุกคนยืน คอลัมน์ x เดียวกัน (ซ้ายสุดจากจุดที่วาด) ไม่เฉียง';
if (hint) {
hint.style.display = 'inline';
hint.innerHTML = 'โหมด พรมแดง — อุปสรรคเลนเฉพาะแถวที่มีคน + เลเซอร์ทุกแถว · ชนถอยซ้าย · ลำดับเกิด 1–6 / ช่องฟ้า · Space / W / ↑ กระโดด';
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'lobby') {
froggerWrap.style.display = 'none';
if (hint) { hint.style.display = 'inline'; hint.innerHTML = 'วาดห้องโถง — พื้นที่เริ่มเกม (ส้ม) = host ยืนก่อนกดเริ่ม · พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน) = ผู้เล่นใหม่สุ่มเกิดในช่องนั้น'; }
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'quiz') {
froggerWrap.style.display = 'none';
if (hint) {
hint.style.display = 'inline';
hint.innerHTML = 'วาดโซนถูก (ฟ้า) / โซนผิด (ชมพู) / พื้นที่โชว์คำถาม (ทอง) · พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน) = ผู้เล่นเข้าห้องสุ่มยืนในช่องนั้น · คำถามและเวลาตั้งที่ Admin → คำถามเกม';
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else if (gt === 'stack') {
froggerWrap.style.display = 'none';
if (hint) {
hint.style.display = 'inline';
hint.innerHTML = 'Stack — กดสลับจังหวะให้บล็อกลงซ้อน · ไม่ใช้แถวกบ · วาดกำแพง/โซน + รูปพื้นหลัง · เล่นจริงยังทำต่อในเครื่องเล่น';
}
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
draw();
} else {
froggerWrap.style.display = 'none';
if (hint) { hint.style.display = 'inline'; hint.innerHTML = 'กบข้ามถนน = ตารางแถว · พรมแดงสุดท้าย = ไม่มีตารางแถว · สลับลงตึก = Stack · ห้องโถง = ฉากรอผู้เล่น · ตอบคำถาม = โซนถูก/ผิด + คำถาม · พื้นที่สุ่มจุดเกิด = สุ่มยืนในช่องฟ้าอ่อน'; }
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';
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse' || drawModeEl.value === 'quizQuestion') && gt !== 'quiz') {
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));
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') lanes = [];
gauntletPlayerSpawns = [];
characterCells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : 1;
mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
if (characterCellsEl) characterCellsEl.value = characterCells;
resize(); draw();
toggleFroggerUI();
}
function resize() {
canvas.width = width * tileSize;
canvas.height = height * tileSize;
sanitizeGauntletPlayerSpawns();
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 usesLanes = isFrogger;
if (usesLanes) ensureLanes();
ensureInteractive();
ensureStartGameArea();
ensureSpawnArea();
if (gtDraw === 'quiz') ensureQuizAreas();
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)';
}
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 (spawn.x === x && spawn.y === y) {
const cells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : characterCells;
const cx = tx + tileSize / 2;
const cy = ty + tileSize / 2;
const size = tileSize * cells;
ctx.fillStyle = 'rgba(158,206,106,0.35)';
ctx.fillRect(cx - size / 2, cy - size / 2, size, size);
ctx.strokeStyle = '#9ece6a';
ctx.lineWidth = 2;
ctx.strokeRect(cx - size / 2, cy - size / 2, size, size);
ctx.fillStyle = 'rgba(158,206,106,0.7)';
ctx.beginPath();
ctx.arc(cx, cy, Math.min(tileSize / 3, size / 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) — backdrop ฉาก · เกมซ้อนบล็อกยังไม่เปิดใน play', 8, 20);
}
const cells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : characterCells;
ctx.fillStyle = '#c0caf5';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('ขนาดตัวละคร: ' + cells + ' ช่อง', 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') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
ensureQuizAreas();
quizQuestionArea[y][x] = left ? 1 : 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(SERVER + '/api/maps')
.then(r => r.json())
.then(list => {
mapLoadEl.innerHTML = '';
(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 = ''; });
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' || drawModeEl.value === 'quizQuestion') && gameType !== 'quiz') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn' && gameType !== 'gauntlet') 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();
});
}
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') ? 6 : 4;
fetch(SERVER + '/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 q = 'play.html?space=' + encodeURIComponent(sid)
+ '&nick=' + encodeURIComponent(nick)
+ '&map=' + encodeURIComponent(id)
+ '&preview=1&defaultChar=1'
+ (editorEmbed ? '&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();
characterCells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : 1;
const showMapInGameEl = document.getElementById('show-map-in-game');
showMapInGame = showMapInGameEl ? showMapInGameEl.checked : true;
const body = {
name, gameType, width, height, tileSize, characterCells, ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
quizTrueArea, quizFalseArea, quizQuestionArea,
};
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 ? SERVER + '/api/maps/' + mapId : SERVER + '/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 (characterCellsEl) characterCellsEl.addEventListener('input', draw);
if (mapId) {
fetch(SERVER + '/api/maps/' + mapId)
.then(r => r.json())
.then(m => {
width = m.width; height = m.height; tileSize = m.tileSize || 32; characterCells = Math.max(1, Math.min(4, m.characterCells || 1));
ground = m.ground || [];
objects = m.objects || [];
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));
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') lanes = [];
if (gameType === 'frogger') {
ensureLanes();
if (lanes.length !== height) ensureLanes();
}
mapName.value = m.name || ''; mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
if (characterCellsEl) characterCellsEl.value = characterCells;
sanitizeGauntletPlayerSpawns();
resize(); draw();
toggleFroggerUI();
})
.catch(() => initGrid());
} else { initGrid(); toggleFroggerUI(); }
})();