(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 ''; } 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 = 'แถว (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'); 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) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ ระยะห่าง (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)'; if (hint) { hint.innerHTML = editorHintBulletList([ 'แถวบน (y=0) = ปลายทาง', 'แถวล่าง = จุดเกิด', 'ตั้งถนน/น้ำด้านล่าง — ความเร็ว ทิศทาง ระยะห่างรถหรือท่อนซุง', ]); } 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.innerHTML = '

โหมด พรมแดงสุดท้าย

' + editorHintBulletList([ 'อุปสรรคเลนเฉพาะแถวที่มีคน + เลเซอร์ทุกแถว · ชนแล้วถอยซ้าย', 'ลำดับเกิด 1–6 หรือช่องฟ้า', 'กระโดด: Space · W · ', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'lobby') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = editorHintBulletList([ 'วาดฉากห้องโถง — รอผู้เล่นและตกแต่งฉาก', 'พื้นที่เริ่มเกม (ส้ม) — host ยืนในโซนนี้ก่อนกดเริ่ม', 'พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน) — ผู้เล่นใหม่สุ่มเกิดในช่องที่วาด', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'quiz') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = editorHintBulletList([ 'โซนถูก (ฟ้า) · โซนผิด (ชมพู) · พื้นที่คำถาม (ทอง)', 'พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน) — ผู้เล่นเข้าห้องสุ่มยืนในโซน', 'คำถามและเวลา — ตั้งที่ Admin → คำถามเกม', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'stack') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

Stack — สลับจังหวะลงตึก

' + editorHintBulletList([ 'จุดปล่อย (ฟ้า) — แนวสวิง crane ด้านบน', 'จุดซ้อนตึก (ชมพู) — แพลตฟอร์มรองรับบล็อก', 'ในเกมไม่มีเดิน — กด Space เพื่อปล่อยบล็อก', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'quiz_carry') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

หยิบมาวาง — หยิบตัวเลือกแล้วไปส่งที่โซน interactive (เขียว)

' + editorHintBulletList([ 'โซนกลาง (ม่วง) — กำแพง · ถ้าไม่วาดโซนทองด้านล่าง ข้อความคำถามจะโชว์บนโซนม่วงในเกม', 'พื้นที่โชว์ข้อความคำถาม (ทอง) — วาดโซนทองแยกได้ · ในเกมข้อความจะอยู่ตรงโซนที่วาด', 'โซน interactive (เขียว) — ยืนบนหรือชิดขอบแล้วกด F ส่งคำตอบ (วาดอย่างน้อย 1 ช่อง — ถ้าไม่วาดจะใช้ชิดโซนกลางแทน)', 'ตัวเลือก 1–16 — ตรงกับลำดับ choices ใน Admin หรือแมป', 'เล่นพร้อมกันได้ · คำถามจากแมปหรือ Admin', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'jump_survive') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

กระโดดให้รอด — กำแพง + แพลตฟอร์มเลื่อนขึ้นในกรอบจอ

' + editorHintBulletList([ 'กำแพง = ขอบซ้ายขวา/บน (ชนแล้วตายหรือออกนอกจอ) · โหมดวาดแพลตฟอร์ม (ฟ้าอมเขียว) = ท่อนยืนได้ — ตอนเล่นแพลตฟอร์มจะเลื่อนขึ้นในกรอด ไม่ลากทั้งฉาก', 'ในเกม: กระโดดสูงขึ้น · แพลตฟอร์มเลื่อนขึ้นแล้ววนกลับมาด้านล่าง (คาบ = ความสูงแมป) · กล้องตามตัว — หลุดขอบบน/ล่างจอ = กลับจุดเกิด', 'พื้นที่สุ่มเกิด (ฟ้า) + ปุ่มตั้งจุดเกิด — แนะนำสูงหลายแถวเพื่อให้มีที่กระโดด', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'space_shooter') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

ยิงยานอวกาศ — หินตก · ผู้เล่น/บอทยิงขึ้น · สูงสุด 6 คน

' + editorHintBulletList([ 'เลือกโหมดวาด จุดเกิดยาน 1–6 แล้วเลือกหมายเลข P ด้านขวา — คลิกซ้ายวาง · คลิกขวาลบช่อง · ช่องเดียวต่อหมายเลข (วางซ้ำจะย้ายจุด)', 'ลำดับผู้เล่นในเกม: คุณก่อน แล้วตามรหัสผู้เล่นอื่น แล้วบอททดสอบ — ตรงกับ P1, P2, …', 'แนะนำ รูปพื้นหลัง อวกาศ + ปิดโชว์กริดในเกม — เล่น: 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 = '

ลูกโป้งยิงบอส (Mega Virus) — สูงสุด 6 คน · บอสกลางจอ

' + editorHintBulletList([ 'โหมด จุดเกิดผู้เล่น 1–6 + เลข P ด้านขวา — คลิกซ้ายวาง · ขวาลบ (เหมือนยิงยาน)', 'โหมด จุดเกิดบอส — คลิกช่อง หนึ่งช่อง เป็นตำแหน่งศูนย์กลางบอส (กะโหลก + โล่หกเหลี่ยม)', 'ในเกม: คุมแบบยานมีแรงเฉื่อย (กดทิศแล้วค่อยเร่ง ปล่อยแล้วยังไหล มีแรงหน่วง) — ไม่สแนปทันที · Space ยิงมีดีเลย์ · บอสยิงกลับ · ลูกโป้งหมด = ตกรอบ', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else if (gt === 'quiz_battle') { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

Quiz Battle — เส้นทาง + โดมถาม A / B / C

' + editorHintBulletList([ 'โหมด เส้นทางเดิน (ม่วง) — วาดซิกแซกเหมือนทางในเกม · ถ้ามีอย่างน้อย 1 ช่อง ผู้เล่นเดินได้เฉพาะบนเส้นทาง · ไม่วาดเลย = เดินอิสระทั้งแมป', 'โหมด โดมคำถาม — วางบนเส้นทาง (หรือทั่วแมปถ้าไม่ใช้เส้นทาง) · ในเกมกด E', 'รูป พื้นหลัง เปลี่ยนทีหลังได้ — ตรรกะเส้นทาง/โดมอยู่ที่กริด ไม่ผูกกราฟิก', 'ข้อสอบจาก Admin → Quiz Battle (battleQuizMcq) · แนะนำ พื้นที่สุ่มจุดเกิด ให้อยู่บนเส้นทางหรือใกล้จุดเริ่ม', ]); } if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; } draw(); } else { froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

เลือกโหมดด้านบน — แต่ละโหมดวาดคนละแบบ

' + editorHintBulletList([ 'ZEP — เดินอิสระบนแผนที่', 'กบข้ามถนน — ตารางแถว / ถนน น้ำ', 'พรมแดงสุดท้าย — กระโดดหลบ (ไม่ใช่ตารางแถวแบบกบ)', 'Stack — จุดปล่อย (ฟ้า) + จุดซ้อน (ชมพู)', 'กระโดดให้รอด — แพลตฟอร์มรอดชีวิต', 'ยิงยานอวกาศ — หินตก · จุดเกิด P1–P6', 'ลูกโป้งยิงบอส — จุดเกิดผู้เล่น + บอส · ดีเลย์ยิง · ลูกโป้ง = ชีวิต', 'Quiz Battle — โดม + กด E ตอบ A B C', 'ห้องโถง — พื้นที่เริ่มเกม (ส้ม) · ตอบคำถาม — โซนถูก–ผิด + คำถาม', 'หยิบมาวาง — โซนกลาง + ตัวเลือก 1–16 · ฟ้าอ่อน = สุ่มจุดเกิด', ]); } 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 = ''; (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') && 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(); } })();