(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 characterCollisionWEl = document.getElementById('character-collision-w'); const characterCollisionHEl = document.getElementById('character-collision-h'); const blockPlayerSeparationWEl = document.getElementById('block-player-separation-w'); const blockPlayerSeparationHEl = document.getElementById('block-player-separation-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', 'carryEmbedCountdown']; 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 readBlockPlayerSeparationWHForSave() { let w = blockPlayerSeparationWEl ? Math.floor(Number(blockPlayerSeparationWEl.value)) : 0; let h = blockPlayerSeparationHEl ? Math.floor(Number(blockPlayerSeparationHEl.value)) : 0; if (!Number.isFinite(w) || w < 0) w = 0; if (!Number.isFinite(h) || h < 0) h = 0; return { w: Math.min(4, w), h: Math.min(4, h) }; } function readCharacterFootprintInputs() { const cw = characterCellsWEl ? clampCharacterFootprint(characterCellsWEl.value) : characterCellsW; const ch = characterCellsHEl ? clampCharacterFootprint(characterCellsHEl.value) : characterCellsH; let colW = characterCollisionWEl ? parseInt(String(characterCollisionWEl.value), 10) : cw; let colH = characterCollisionHEl ? parseInt(String(characterCollisionHEl.value), 10) : ch; if (!Number.isFinite(colW) || colW < 1) colW = cw; if (!Number.isFinite(colH) || colH < 1) colH = ch; colW = Math.max(1, Math.min(cw, Math.floor(colW))); colH = Math.max(1, Math.min(ch, Math.floor(colH))); return { cw, ch, colW, colH }; } function applyCharacterFootprintInputs(cw, ch) { characterCellsW = clampCharacterFootprint(cw); characterCellsH = clampCharacterFootprint(ch); if (characterCellsWEl) characterCellsWEl.value = String(characterCellsW); if (characterCellsHEl) characterCellsHEl.value = String(characterCellsH); if (characterCollisionWEl) { let v = parseInt(String(characterCollisionWEl.value), 10); if (!Number.isFinite(v) || v < 1) v = characterCellsW; characterCollisionWEl.value = String(Math.max(1, Math.min(characterCellsW, v))); } if (characterCollisionHEl) { let v = parseInt(String(characterCollisionHEl.value), 10); if (!Number.isFinite(v) || v < 1) v = characterCellsH; characterCollisionHEl.value = String(Math.max(1, Math.min(characterCellsH, v))); } } let gameType = 'zep'; let lanes = []; let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 }; let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = []; let quizCarryHubArea = [], quizCarryOptionArea = []; /** quiz_carry embed: ช่อง = 1 วางกล่องนับ 3-2-1 บนแมปเมื่อเลือก anchor = grid */ let carryEmbedCountdownArea = []; 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 = []; /** gauntlet: แถว y บน–ล่างของแถบเลเซอร์ (null = เต็มความสูงแมป) */ let gauntletLaserRowStart = null; let gauntletLaserRowEnd = null; /** ล็อบบี้ / ZEP / quiz_carry / ฯลฯ — P1..P6 ตามลำดับ join เมื่อเลือกโหมด slots6 */ let lobbyPlayerSpawns = [null, null, null, null, null, null]; /** 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; /** พื้นหลังเลื่อนแนวตั้ง — เฉพาะแมป mnpz6rkp (Editor) */ const EDITOR_SCROLL_BG_MAP_ID = 'mnpz6rkp'; const EDITOR_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnpz6rkp/intro.png'; const EDITOR_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnpz6rkp/loop.png'; let editorBgScrollConfig = { enabled: false, speedPxPerSec: 56, /** 'up' = ล่าง→บน · 'down' = บน→ล่าง */ scrollDirection: 'up', introImage: null, loopImage: null, }; let editorBgScrollIntroImg = null; let editorBgScrollLoopImg = null; /** viewport top ใน strip (>=0) — เพิ่ม = เลื่อนขึ้น (ล่าง→บน) */ let editorBgScrollPx = 0; let editorBgScrollRaf = null; let editorBgScrollLastTs = 0; function stopEditorBgScrollRaf() { if (editorBgScrollRaf != null) { cancelAnimationFrame(editorBgScrollRaf); editorBgScrollRaf = null; } editorBgScrollLastTs = 0; } function isEditorScrollBgMap() { return mapId === EDITOR_SCROLL_BG_MAP_ID; } function isEditorScrollBgDrawing() { if (!isEditorScrollBgMap() || !editorBgScrollConfig.enabled) return false; const a = editorBgScrollIntroImg; const b = editorBgScrollLoopImg; return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); } function loadEditorBgScrollImagePair(introSrc, loopSrc, onDone) { stopEditorBgScrollRaf(); editorBgScrollIntroImg = null; editorBgScrollLoopImg = null; let pending = 2; const doneOne = () => { pending--; if (pending <= 0 && typeof onDone === 'function') onDone(); }; const im1 = new Image(); im1.onload = () => { editorBgScrollIntroImg = im1; doneOne(); }; im1.onerror = () => { doneOne(); }; im1.src = introSrc; const im2 = new Image(); im2.onload = () => { editorBgScrollLoopImg = im2; doneOne(); }; im2.onerror = () => { doneOne(); }; im2.src = loopSrc; } function applyEditorBgScrollFromMap(m) { if (!isEditorScrollBgMap()) { editorBgScrollConfig = { enabled: false, speedPxPerSec: 56, scrollDirection: 'up', introImage: null, loopImage: null }; stopEditorBgScrollRaf(); editorBgScrollIntroImg = null; editorBgScrollLoopImg = null; return; } const raw = m && m.editorBgScroll && typeof m.editorBgScroll === 'object' ? m.editorBgScroll : {}; const sp = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 56)); const sd = String(raw.scrollDirection || raw.direction || 'up').toLowerCase(); editorBgScrollConfig = { enabled: !!raw.enabled, speedPxPerSec: sp, scrollDirection: (sd === 'down' || sd === 'top' || sd === 'toptobottom') ? 'down' : 'up', introImage: typeof raw.introImage === 'string' && raw.introImage.length ? raw.introImage : null, loopImage: typeof raw.loopImage === 'string' && raw.loopImage.length ? raw.loopImage : null, }; const introSrc = editorBgScrollConfig.introImage || EDITOR_SCROLL_BG_DEFAULT_INTRO; const loopSrc = editorBgScrollConfig.loopImage || EDITOR_SCROLL_BG_DEFAULT_LOOP; loadEditorBgScrollImagePair(introSrc, loopSrc, () => { syncEditorBgScrollUiFromConfig(); syncEditorBgScrollInitialToBottom(); draw(); if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); }); } function syncEditorBgScrollInitialToBottom() { const intro = editorBgScrollIntroImg; if (!intro || !intro.complete || !intro.naturalWidth) return; const cwR = Math.max(1, Math.round(canvas.width)); const chR = Math.max(1, Math.round(canvas.height)); const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); if (editorBgScrollConfig.scrollDirection === 'down') { editorBgScrollPx = 0; } else { editorBgScrollPx = Math.max(0, drawHIntro - chR); } } function syncEditorBgScrollUiFromConfig() { const block = document.getElementById('editor-bg-scroll-block'); if (block) block.hidden = !isEditorScrollBgMap(); const en = document.getElementById('editor-bg-scroll-enabled'); if (en) en.checked = !!editorBgScrollConfig.enabled; const sp = document.getElementById('editor-bg-scroll-speed'); if (sp) sp.value = String(editorBgScrollConfig.speedPxPerSec || 56); const dir = document.getElementById('editor-bg-scroll-direction'); if (dir) dir.value = editorBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up'; } function readEditorBgScrollConfigForSave() { if (!isEditorScrollBgMap()) return null; const en = document.getElementById('editor-bg-scroll-enabled'); const spEl = document.getElementById('editor-bg-scroll-speed'); const sp = Math.max(8, Math.min(400, Math.floor(Number(spEl && spEl.value)) || 56)); const dirEl = document.getElementById('editor-bg-scroll-direction'); const out = { enabled: !!(en && en.checked), speedPxPerSec: sp, scrollDirection: dirEl && dirEl.value === 'down' ? 'down' : 'up', }; if (editorBgScrollConfig.introImage) out.introImage = editorBgScrollConfig.introImage; if (editorBgScrollConfig.loopImage) out.loopImage = editorBgScrollConfig.loopImage; return out; } function drawEditorScrollingBackground(cw, ch) { const intro = editorBgScrollIntroImg; const loop = editorBgScrollLoopImg; if (!intro || !intro.complete || !loop || !loop.complete) return; const cwR = Math.max(1, Math.round(cw)); const chR = Math.max(1, Math.round(ch)); const scaleI = cwR / intro.naturalWidth; const drawHIntro = Math.round(intro.naturalHeight * scaleI); const scaleL = cwR / loop.naturalWidth; const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); const S = editorBgScrollPx; const vp0 = S; const vp1 = S + chR; const yLimit = vp1 + drawHLoop * 2; const flowDown = editorBgScrollConfig.scrollDirection === 'down'; function stripTopToDestY(stripTop, drawH) { if (!flowDown) return stripTop - S; return S + chR - (stripTop + drawH); } ctx.save(); ctx.imageSmoothingEnabled = false; ctx.beginPath(); ctx.rect(0, 0, cwR, chR); ctx.clip(); if (vp0 < 0) { let k = 1; if (vp0 < -drawHLoop * 4) { k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2); } for (; k < 50000; k++) { const y0 = -k * drawHLoop; if (y0 >= vp1 + drawHLoop) break; if (y0 + drawHLoop <= vp0) continue; const dy = Math.round(stripTopToDestY(y0, drawHLoop)); ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); } } if (drawHIntro > vp0 && vp1 > 0) { const dy = Math.round(stripTopToDestY(0, drawHIntro)); ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro); } let y = drawHIntro; if (y < yLimit && vp1 > drawHIntro) { if (vp0 > drawHIntro) { const n = Math.floor((vp0 - drawHIntro) / drawHLoop); y = drawHIntro + Math.max(0, n - 1) * drawHLoop; } while (y < yLimit) { const dy = Math.round(stripTopToDestY(y, drawHLoop)); ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); y += drawHLoop; } } ctx.restore(); } function editorBgScrollTick(ts) { editorBgScrollRaf = null; if (!isEditorScrollBgDrawing()) return; if (!editorBgScrollLastTs) editorBgScrollLastTs = ts; const dt = Math.min(64, Math.max(0, ts - editorBgScrollLastTs)); editorBgScrollLastTs = ts; editorBgScrollPx += (editorBgScrollConfig.speedPxPerSec || 56) * (dt / 1000); draw(); editorBgScrollRaf = requestAnimationFrame(editorBgScrollTick); } function startEditorBgScrollRaf() { if (!isEditorScrollBgDrawing()) return; stopEditorBgScrollRaf(); editorBgScrollLastTs = 0; editorBgScrollRaf = requestAnimationFrame(editorBgScrollTick); } /** คลังรูป + สไปรต์ { i, x, y, w, h } — บันทึก gridImageSprites + gridImageCells (สำหรับรุ่นเก่า) */ let gridImageLibrary = []; let gridImageSprites = []; let gridImageBrushIndex = 0; let pendingHeldLibIndex = -1; const editorGridImageElByIndex = {}; const editorGridImageHeldElByIndex = {}; const GRID_IMG_MAX_WH = 24; function gridLibIdleUrl(entry) { if (entry == null) return ''; if (typeof entry === 'string') return entry; if (typeof entry === 'object' && entry.idle) return String(entry.idle); return ''; } function gridLibHeldUrl(entry) { if (entry == null || typeof entry === 'string') return ''; if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held); return ''; } function normalizeGridLibEntry(raw) { if (raw == null) return null; if (typeof raw === 'string' && raw.length > 0) return { idle: raw, held: null }; if (typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0) { let held = (typeof raw.held === 'string' && raw.held.length > 0) ? raw.held : null; if (held && held === raw.idle) held = null; return { idle: raw.idle, held }; } return null; } function serializeGridLibEntry(entry) { const idle = gridLibIdleUrl(entry); if (!idle) return null; const held = gridLibHeldUrl(entry); if (held && held !== idle) return { idle, held }; return idle; } 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); } } if (!carryEmbedCountdownArea.length || carryEmbedCountdownArea.length !== height) { const existingCd = carryEmbedCountdownArea.slice().map(r => r && r.slice()); carryEmbedCountdownArea = []; for (let y = 0; y < height; y++) { const row = existingCd[y] && existingCd[y].length === width ? existingCd[y].slice() : Array(width).fill(0); carryEmbedCountdownArea.push(row); } } else { for (let y = 0; y < height; y++) { if (!carryEmbedCountdownArea[y] || carryEmbedCountdownArea[y].length !== width) carryEmbedCountdownArea[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 sanitizeGauntletLaserRowsInEditor() { if (gauntletLaserRowStart == null || gauntletLaserRowEnd == null) return; let y0 = Math.floor(Number(gauntletLaserRowStart)); let y1 = Math.floor(Number(gauntletLaserRowEnd)); if (!Number.isFinite(y0)) y0 = 0; if (!Number.isFinite(y1)) y1 = height - 1; y0 = Math.max(0, Math.min(height - 1, y0)); y1 = Math.max(0, Math.min(height - 1, y1)); if (y1 < y0) { const t = y0; y0 = y1; y1 = t; } gauntletLaserRowStart = y0; gauntletLaserRowEnd = y1; } function supportsLobbySpawnPaint(gt) { return gt === 'zep' || gt === 'lobby' || gt === 'quiz' || gt === 'quiz_carry' || gt === 'stack' || gt === 'jump_survive' || gt === 'quiz_battle'; } function ensureLobbyPlayerSpawnsLength() { while (lobbyPlayerSpawns.length < 6) lobbyPlayerSpawns.push(null); lobbyPlayerSpawns.length = 6; } function sanitizeLobbyPlayerSpawnsInEditor() { ensureLobbyPlayerSpawnsLength(); for (let i = 0; i < 6; i++) { const s = lobbyPlayerSpawns[i]; if (!s) continue; const x = Math.floor(Number(s.x)); const y = Math.floor(Number(s.y)); if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= width || y < 0 || y >= height || (objects[y] && objects[y][x] === 1)) { lobbyPlayerSpawns[i] = null; } else { lobbyPlayerSpawns[i] = { x, y }; } } } function syncLobbySpawnAuxUi() { const gt = gameTypeEl ? gameTypeEl.value : gameType; const wrap = document.getElementById('lobby-spawn-wrap'); const slotWrap = document.getElementById('lobby-slot-paint-wrap'); const btnClr = document.getElementById('btn-clear-lobby-spawns'); const modeEl = document.getElementById('lobby-spawn-mode'); const use = supportsLobbySpawnPaint(gt); if (wrap) wrap.style.display = use ? 'flex' : 'none'; const slotsMode = !!(use && modeEl && modeEl.value === 'slots6'); if (slotWrap) slotWrap.style.display = slotsMode ? '' : 'none'; if (btnClr) btnClr.hidden = !slotsMode; } 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 syncGridImageBrushInputCaps() { const wEl = document.getElementById('grid-image-brush-w'); const hEl = document.getElementById('grid-image-brush-h'); const cap = Math.min(GRID_IMG_MAX_WH, width, height); if (wEl) { wEl.max = String(Math.max(1, cap)); wEl.min = '1'; const vw = parseInt(wEl.value, 10); if (!Number.isFinite(vw) || vw < 1) wEl.value = '1'; else if (vw > cap) wEl.value = String(cap); } if (hEl) { hEl.max = String(Math.max(1, cap)); hEl.min = '1'; const vh = parseInt(hEl.value, 10); if (!Number.isFinite(vh) || vh < 1) hEl.value = '1'; else if (vh > cap) hEl.value = String(cap); } } function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) { return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by; } function deriveGridImageCellsFromSprites() { const cells = Array(height).fill(0).map(() => Array(width).fill(null)); gridImageSprites.forEach((s) => { const i = s.i; if (!Number.isFinite(i) || i < 0 || i >= gridImageLibrary.length) return; for (let yy = s.y; yy < s.y + s.h; yy++) { for (let xx = s.x; xx < s.x + s.w; xx++) { if (yy >= 0 && yy < height && xx >= 0 && xx < width) cells[yy][xx] = i; } } }); return cells; } function migrateSpritesFromMapJson(m, mapW, mapH) { if (Array.isArray(m.gridImageSprites) && m.gridImageSprites.length) { return m.gridImageSprites.map((raw) => { const i = Math.max(0, Math.floor(Number(raw.i))); const x = Math.floor(Number(raw.x)); const y = Math.floor(Number(raw.y)); const w = Math.max(1, Math.floor(Number(raw.w)) || 1); const h = Math.max(1, Math.floor(Number(raw.h)) || 1); const b = parseSpriteBindCarryOption(raw.bindCarryOption); const o = { i, x, y, w, h }; if (b != null) o.bindCarryOption = b; return o; }).filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y)); } const libLen = (Array.isArray(m.gridImageLibrary) && m.gridImageLibrary.length) || 0; const gic = m.gridImageCells; const out = []; if (!Array.isArray(gic) || !libLen) return out; const mh = Number.isFinite(mapH) && mapH > 0 ? Math.floor(mapH) : gic.length; const mw = Number.isFinite(mapW) && mapW > 0 ? Math.floor(mapW) : (gic[0] && gic[0].length) || 0; for (let yy = 0; yy < Math.min(gic.length, mh); yy++) { const row = gic[yy]; if (!row) continue; for (let xx = 0; xx < Math.min(row.length, mw || row.length); xx++) { if (row[xx] == null) continue; const iv = parseInt(row[xx], 10); if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue; out.push({ i: iv, x: xx, y: yy, w: 1, h: 1 }); } } return out; } function parseSpriteBindCarryOption(raw) { if (raw == null || raw === '') return null; const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10); if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_EDITOR_OPT_COUNT) return null; return n; } function readSpriteCarryBindFromSelect() { const el = document.getElementById('grid-sprite-carry-bind'); if (!el) return null; const v = String(el.value || '').trim(); if (v === '' || v === 'auto') return null; return parseSpriteBindCarryOption(v); } function clampSpriteToMap(s) { let { i, x, y, w, h, bindCarryOption } = s; if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x + w > width) w = width - x; if (y + h > height) h = height - y; w = Math.max(1, w); h = Math.max(1, h); const out = { i, x, y, w, h }; const b = parseSpriteBindCarryOption(bindCarryOption); if (b != null) out.bindCarryOption = b; return out; } function sanitizeSpritesInPlace() { gridImageSprites = gridImageSprites.map(clampSpriteToMap).filter((sp) => { if (!Number.isFinite(sp.i) || sp.i < 0 || sp.i >= gridImageLibrary.length) return false; if (sp.w < 1 || sp.h < 1) return false; return sp.x < width && sp.y < height && sp.x + sp.w > 0 && sp.y + sp.h > 0; }); } function removeSpritesOverlappingRect(x0, y0, w0, h0) { gridImageSprites = gridImageSprites.filter((sp) => !rectsOverlap(x0, y0, w0, h0, sp.x, sp.y, sp.w, sp.h)); } function findSpriteIndexAtCell(px, py) { for (let k = gridImageSprites.length - 1; k >= 0; k--) { const s = gridImageSprites[k]; if (px >= s.x && py >= s.y && px < s.x + s.w && py < s.y + s.h) return k; } return -1; } function canPlaceSpriteAt(anchorX, anchorY, sw, sh, libIdx) { if (!Number.isFinite(libIdx) || libIdx < 0 || libIdx >= gridImageLibrary.length) return null; const ax = Math.max(0, Math.min(Math.floor(anchorX), width - 1)); const ay = Math.max(0, Math.min(Math.floor(anchorY), height - 1)); const cap = Math.min(GRID_IMG_MAX_WH, width, height); let w = Math.max(1, Math.min(Math.floor(sw) || 1, width - ax, cap)); let h = Math.max(1, Math.min(Math.floor(sh) || 1, height - ay, cap)); for (let yy = ay; yy < ay + h; yy++) { for (let xx = ax; xx < ax + w; xx++) { if (objects[yy][xx] === 1) return null; } } const out = { i: libIdx, x: ax, y: ay, w, h }; const b = readSpriteCarryBindFromSelect(); if (b != null) out.bindCarryOption = b; return out; } function rebuildGridImageGallery() { const gal = document.getElementById('grid-cell-image-gallery'); if (!gal) return; gal.innerHTML = ''; if (!gridImageLibrary.length) { gal.innerHTML = 'ยังไม่มีรูป — อัปโหลดด้านล่าง'; return; } if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = gridImageLibrary.length - 1; gridImageLibrary.forEach((entry, idx) => { const row = document.createElement('div'); row.className = 'grid-cell-image-gallery-row'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'grid-cell-image-gallery-item' + (idx === gridImageBrushIndex ? ' is-selected' : ''); btn.title = 'รูป #' + (idx + 1) + ' (ก่อนหยิบ) — คลิกเลือกแปรง'; btn.dataset.libIndex = String(idx); const thumb = document.createElement('img'); thumb.alt = ''; thumb.src = gridLibIdleUrl(entry); thumb.loading = 'lazy'; btn.appendChild(thumb); const cap = document.createElement('span'); cap.className = 'grid-cell-image-gallery-cap'; cap.textContent = '#' + (idx + 1); btn.appendChild(cap); btn.addEventListener('click', () => { gridImageBrushIndex = idx; Array.from(gal.querySelectorAll('.grid-cell-image-gallery-item')).forEach((el) => { el.classList.toggle('is-selected', el.dataset.libIndex === String(idx)); }); }); const heldSide = document.createElement('div'); heldSide.className = 'grid-cell-image-gallery-held-side'; const heldLabel = document.createElement('span'); heldLabel.className = 'grid-cell-image-gallery-held-label'; heldLabel.textContent = 'หยิบแล้ว'; heldSide.appendChild(heldLabel); const heldUrl = gridLibHeldUrl(entry); if (heldUrl) { const him = document.createElement('img'); him.className = 'grid-cell-image-gallery-held-thumb'; him.alt = ''; him.src = heldUrl; him.loading = 'lazy'; heldSide.appendChild(him); } const btnHeld = document.createElement('button'); btnHeld.type = 'button'; btnHeld.className = 'grid-cell-image-gallery-held-btn'; btnHeld.textContent = heldUrl ? 'เปลี่ยนรูปถือ' : 'ใส่รูปถือ'; btnHeld.title = 'รูปบนแมปเมื่อมีคนหยิบตัวเลือกในช่องนี้ (quiz carry) จนกว่าจะปล่อย'; btnHeld.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); pendingHeldLibIndex = idx; const inp = document.getElementById('grid-cell-image-held-upload'); if (inp) inp.click(); }); heldSide.appendChild(btnHeld); if (heldUrl) { const clr = document.createElement('button'); clr.type = 'button'; clr.className = 'grid-cell-image-gallery-clear-held'; clr.title = 'ลบรูปตอนถือ'; clr.textContent = '×'; clr.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); gridImageLibrary[idx] = { idle: gridLibIdleUrl(gridImageLibrary[idx]), held: null }; delete editorGridImageHeldElByIndex[idx]; rebuildGridImageGallery(); draw(); if (statusEl) statusEl.textContent = 'ลบรูปตอนถือของ #' + (idx + 1) + ' แล้ว'; }); heldSide.appendChild(clr); } row.appendChild(btn); row.appendChild(heldSide); if (heldUrl) { const idleStr = gridLibIdleUrl(entry); if (idleStr && heldUrl === idleStr) { const sameHint = document.createElement('div'); sameHint.className = 'grid-cell-image-gallery-same-hint'; sameHint.textContent = 'รูปก่อนหยิบกับหยิบแล้วเป็นข้อมูลเดียวกัน — อัปโหลดภาพคนละไฟล์สำหรับ «หยิบแล้ว» จึงจะเห็นต่างในเกม'; row.appendChild(sameHint); } } gal.appendChild(row); primeEditorGridImage(idx); primeEditorGridImageHeld(idx); }); } function primeEditorGridImage(idx) { const idle = gridLibIdleUrl(gridImageLibrary[idx]); if (editorGridImageElByIndex[idx] || !idle) return; const im = new Image(); im.onload = () => { draw(); }; im.onerror = () => { delete editorGridImageElByIndex[idx]; }; im.src = idle; editorGridImageElByIndex[idx] = im; } function primeEditorGridImageHeld(idx) { const held = gridLibHeldUrl(gridImageLibrary[idx]); if (!held) { delete editorGridImageHeldElByIndex[idx]; return; } if (editorGridImageHeldElByIndex[idx]) return; const im = new Image(); im.onload = () => { draw(); }; im.onerror = () => { delete editorGridImageHeldElByIndex[idx]; }; im.src = held; editorGridImageHeldElByIndex[idx] = im; } function removeGridImageFromLibraryAt(index) { const i = parseInt(index, 10); if (!Number.isFinite(i) || i < 0 || i >= gridImageLibrary.length) return; gridImageLibrary.splice(i, 1); gridImageSprites = gridImageSprites .map((s) => { if (s.i === i) return null; if (s.i > i) return { ...s, i: s.i - 1 }; return s; }) .filter(Boolean); if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = Math.max(0, gridImageLibrary.length - 1); Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; }); Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; }); sanitizeSpritesInPlace(); rebuildGridImageGallery(); syncGridImageBrushInputCaps(); draw(); if (statusEl) statusEl.textContent = 'ลบรูปออกจากคลังแล้ว'; } 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; }); const carryCdWrap = document.getElementById('editor-quiz-carry-countdown-wrap'); if (carryCdWrap) carryCdWrap.style.display = showCarry ? 'flex' : 'none'; 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([ 'อุปสรรคเลนเฉพาะแถวที่มีคน + เลเซอร์ทุกแถว · ชนแล้วถอยซ้าย', 'โหมดวาด เลเซอร์: แถวต้น / แถวปลาย — กำหนดความสูงแถบเลเซอร์บนแมป (ไม่ต้องเปลี่ยนรูปจาก Admin)', 'ลำดับเกิด 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 / แมป)', 'พรีวิวเอดิเตอร์: ช่วงนับ 3-2-1 — เลือกตำแหน่ง (กลางจอ / บนแมปอัตโนมัติที่โซนทองหรือโซนกลาง / ตามกริด โหมดวาด «ตำแหน่งเลข 3-2-1») · ไฮไลต์ข้อถูกด้านบน · ต่อข้อ countdownHighlightSlot (1–16)', 'เล่นพร้อมกันได้ · คำถามจากแมปหรือ 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 glStartOpt = document.getElementById('draw-mode-option-gauntlet-laser-start'); const glEndOpt = document.getElementById('draw-mode-option-gauntlet-laser-end'); const btnClrGl = document.getElementById('btn-clear-gauntlet-laser-span'); if (glStartOpt) { glStartOpt.hidden = gt !== 'gauntlet'; if (gt !== 'gauntlet' && drawModeEl && drawModeEl.value === 'gauntletLaserStart') drawModeEl.value = 'wall'; } if (glEndOpt) { glEndOpt.hidden = gt !== 'gauntlet'; if (gt !== 'gauntlet' && drawModeEl && drawModeEl.value === 'gauntletLaserEnd') drawModeEl.value = 'wall'; } if (btnClrGl) btnClrGl.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'; const lobbySpawnOpt = document.getElementById('draw-mode-option-lobby-spawn'); if (lobbySpawnOpt) { const useLobby = supportsLobbySpawnPaint(gt); lobbySpawnOpt.hidden = !useLobby; if (!useLobby && drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn') drawModeEl.value = 'wall'; } syncLobbySpawnAuxUi(); } 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)); carryEmbedCountdownArea = Array(height).fill(0).map(() => Array(width).fill(0)); quizBattleDomeArea = Array(height).fill(0).map(() => Array(width).fill(0)); quizBattlePathArea = Array(height).fill(0).map(() => Array(width).fill(0)); 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)); gridImageLibrary = []; gridImageSprites = []; gridImageBrushIndex = 0; Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; }); Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; }); rebuildGridImageGallery(); syncGridImageBrushInputCaps(); 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 = []; gauntletLaserRowStart = null; gauntletLaserRowEnd = null; lobbyPlayerSpawns = [null, null, null, null, null, null]; const lsmInit = document.getElementById('lobby-spawn-mode'); if (lsmInit) lsmInit.value = 'random'; 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(); sanitizeLobbyPlayerSpawnsInEditor(); ensureShooterSpawnSlots(); ensureBalloonBossPlayerSlots(); sanitizeSpritesInPlace(); syncGridImageBrushInputCaps(); 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 (isEditorScrollBgDrawing()) { drawEditorScrollingBackground(canvas.width, canvas.height); } else 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); } } } } gridImageSprites.forEach((sp) => { primeEditorGridImage(sp.i); const gim = editorGridImageElByIndex[sp.i]; if (!gim || !gim.complete || !gim.naturalWidth) return; const tx = sp.x * tileSize; const ty = sp.y * tileSize; const tw = sp.w * tileSize; const th = sp.h * tileSize; ctx.save(); ctx.beginPath(); ctx.rect(tx, ty, tw, th); ctx.clip(); ctx.drawImage(gim, tx, ty, tw, th); ctx.restore(); ctx.strokeStyle = 'rgba(255,255,255,0.22)'; ctx.lineWidth = 1; ctx.strokeRect(tx + 0.5, ty + 0.5, tw - 1, th - 1); }); 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 (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 (carryEmbedCountdownArea[y] && Number(carryEmbedCountdownArea[y][x]) === 1) { ctx.fillStyle = 'rgba(100, 230, 255, 0.32)'; ctx.fillRect(tx + 4, ty + 4, tileSize - 8, tileSize - 8); ctx.strokeStyle = 'rgba(80, 220, 255, 0.88)'; ctx.lineWidth = 2; ctx.strokeRect(tx + 4, ty + 4, tileSize - 8, tileSize - 8); ctx.lineWidth = 1; ctx.fillStyle = '#1a3a44'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('321', tx + tileSize / 2, ty + tileSize / 2 + 3); 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(); const colW0 = Math.max(1, Math.min(fp.cw, fp.colW)); const colH0 = Math.max(1, Math.min(fp.ch, fp.colH)); const colWEff = Math.max(1, Math.min(colW0, effW)); const colHEff = Math.max(1, Math.min(colH0, effH)); if (colWEff < effW || colHEff < effH) { const ox = spawn.x + Math.floor((effW - colWEff) / 2); const oy = spawn.y + (effH - colHEff); const tcx = ox * tileSize; const tcy = oy * tileSize; const trw = colWEff * tileSize; const trh = colHEff * tileSize; ctx.save(); ctx.setLineDash([5, 4]); ctx.strokeStyle = 'rgba(224, 175, 104, 0.95)'; ctx.lineWidth = 2; ctx.strokeRect(tcx + 2, tcy + 2, trw - 4, trh - 4); ctx.restore(); } } 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 (gauntletLaserRowStart != null && gauntletLaserRowEnd != null) { let y0 = Math.max(0, Math.min(height - 1, gauntletLaserRowStart)); let y1 = Math.max(0, Math.min(height - 1, gauntletLaserRowEnd)); if (y1 < y0) { const t = y0; y0 = y1; y1 = t; } const x0 = 2; const x1 = width * tileSize - 2; ctx.save(); ctx.fillStyle = 'rgba(255, 80, 140, 0.12)'; ctx.fillRect(x0, y0 * tileSize, x1 - x0, (y1 - y0 + 1) * tileSize); ctx.strokeStyle = 'rgba(255, 140, 180, 0.9)'; ctx.lineWidth = 2; ctx.setLineDash([7, 5]); ctx.beginPath(); ctx.moveTo(x0, y0 * tileSize + 1); ctx.lineTo(x1, y0 * tileSize + 1); ctx.moveTo(x0, (y1 + 1) * tileSize - 1); ctx.lineTo(x1, (y1 + 1) * tileSize - 1); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = 'rgba(255, 200, 220, 0.95)'; ctx.font = 'bold 11px ui-monospace, Consolas, sans-serif'; ctx.fillText('laser y=' + y0 + '…' + y1, x0 + 4, y0 * tileSize + 14); ctx.restore(); } 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); } if (supportsLobbySpawnPaint(gtDraw)) { ensureLobbyPlayerSpawnsLength(); let anyLobbyP = false; for (let ii = 0; ii < 6; ii++) { if (lobbyPlayerSpawns[ii]) { anyLobbyP = true; break; } } if (anyLobbyP) { const drawLobbyPNum = (s, num) => { if (!s || s.x < 0 || s.x >= width || s.y < 0 || s.y >= height) return; if (objects[s.y][s.x] === 1) return; const cx = s.x * tileSize + tileSize / 2; const cy = s.y * tileSize + tileSize / 2 + 4; ctx.fillStyle = 'rgba(0,80,120,0.55)'; ctx.beginPath(); ctx.arc(cx, cy - 3, 12, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#b4f9f8'; ctx.font = 'bold 13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('P' + String(num), cx, cy + 2); ctx.textAlign = 'left'; }; for (let i = 0; i < 6; i++) { const s = lobbyPlayerSpawns[i]; if (s) drawLobbyPNum(s, i + 1); } } } const fpHud = readCharacterFootprintInputs(); ctx.fillStyle = '#c0caf5'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; const colLine = (fpHud.colW < fpHud.cw || fpHud.colH < fpHud.ch) ? ' · ชนกำแพง ' + fpHud.colW + '×' + fpHud.colH + ' (เท้า/กลาง — เส้นประส้ม · เฉพาะกำแพง; โซนอื่นใช้ ' + fpHud.ch + '×' + fpHud.cw + ')' : ''; ctx.fillText('ขนาดตัวละคร: สูง ' + fpHud.ch + ' × กว้าง ' + fpHud.cw + ' ช่อง (แนวตั้ง×แนวนอน · มุมซ้ายบนจุดเกิด)' + colLine, 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, mouseEv) { if (drawModeEl.value === 'gauntletLaserStart' || drawModeEl.value === 'gauntletLaserEnd') { const gt = gameTypeEl ? gameTypeEl.value : gameType; if (gt !== 'gauntlet') return; if (!left) { gauntletLaserRowStart = null; gauntletLaserRowEnd = null; if (statusEl) statusEl.textContent = 'ล้างช่วงแนวตั้ง laser แล้ว — ใช้เต็มความสูงแมป'; draw(); return; } const yy = Math.max(0, Math.min(height - 1, y)); if (drawModeEl.value === 'gauntletLaserStart') gauntletLaserRowStart = yy; else gauntletLaserRowEnd = yy; sanitizeGauntletLaserRowsInEditor(); if (statusEl) { const a = gauntletLaserRowStart; const b = gauntletLaserRowEnd; statusEl.textContent = (a != null && b != null) ? ('เลเซอร์: แถว ' + a + '–' + b + ' (แนวตั้ง · คลิกขวา = ล้างทั้งช่วง)') : ('เลเซอร์: ตั้งแถว ' + yy + ' แล้ว — เลือกโหมดอีกแถวเพื่อกำหนดขอบอีกด้าน'); } draw(); return; } 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 === 'lobbyPlayerSpawn') { const gt = gameTypeEl ? gameTypeEl.value : gameType; if (!supportsLobbySpawnPaint(gt)) return; const slotEl = document.getElementById('lobby-slot-paint'); const slotOne = parseInt(slotEl && slotEl.value, 10) || 1; const slot = Math.max(0, Math.min(5, slotOne - 1)); ensureLobbyPlayerSpawnsLength(); if (objects[y][x] === 1) { if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดไม่ได้'; return; } if (left) { for (let k = 0; k < 6; k++) { if (k === slot) continue; const o = lobbyPlayerSpawns[k]; if (o && o.x === x && o.y === y) lobbyPlayerSpawns[k] = null; } lobbyPlayerSpawns[slot] = { x, y }; if (statusEl) statusEl.textContent = 'จุดเกิด P' + (slot + 1) + ' = (' + x + ',' + y + ')'; } else { let cleared = false; for (let k = 0; k < 6; k++) { const o = lobbyPlayerSpawns[k]; if (o && o.x === x && o.y === y) { lobbyPlayerSpawns[k] = null; cleared = true; } } if (statusEl) statusEl.textContent = cleared ? 'ถอดจุด P ที่ช่องนี้แล้ว' : 'ไม่มีจุด P ที่ช่องนี้'; } return; } if (drawModeEl.value === 'cellImage') { if (objects[y][x] === 1) { if (statusEl) statusEl.textContent = 'ช่องกำแพง — วางรูปบนกริดไม่ได้'; return; } const gtCell = gameTypeEl ? gameTypeEl.value : gameType; if (left && mouseEv && mouseEv.altKey && gtCell === 'quiz_carry') { const hit = findSpriteIndexAtCell(x, y); if (hit >= 0) { const bind = readSpriteCarryBindFromSelect(); if (bind == null) delete gridImageSprites[hit].bindCarryOption; else gridImageSprites[hit].bindCarryOption = bind; sanitizeSpritesInPlace(); if (statusEl) { statusEl.textContent = bind == null ? 'สไปรต์: ผูกแบบอัตโนมัติ (นับจากช่องโซนตัวเลือกที่ทับสไปรต์)' : ('ผูกสไปรต์กับตัวเลือกข้อ ' + bind + ' แล้ว (Alt+คลิกซ้ายอีกครั้งเปลี่ยนค่าในรายการ)'); } return; } if (statusEl) statusEl.textContent = 'ไม่มีสไปรต์ในช่องนี้ — Alt+คลิกบนพื้นที่ที่มีรูป'; return; } const wEl = document.getElementById('grid-image-brush-w'); const hEl = document.getElementById('grid-image-brush-h'); const bw = Math.max(1, parseInt(wEl && wEl.value, 10) || 1); const bh = Math.max(1, parseInt(hEl && hEl.value, 10) || 1); if (left) { if (!gridImageLibrary.length) { if (statusEl) statusEl.textContent = 'อัปโหลดรูปเข้าคลังก่อน (หรือโหลดฉากที่มีคลังรูป)'; return; } const bi = gridImageBrushIndex; if (bi < 0 || bi >= gridImageLibrary.length) { if (statusEl) statusEl.textContent = 'คลิกเลือกรูปในแกลเลอรีก่อนวาง'; return; } const placed = canPlaceSpriteAt(x, y, bw, bh, bi); if (!placed) { if (statusEl) statusEl.textContent = 'วางไม่ได้ — มีกำแพงในพื้นที่ หรือลดกว้าง/สูง'; return; } removeSpritesOverlappingRect(placed.x, placed.y, placed.w, placed.h); gridImageSprites.push(placed); draw(); if (statusEl) statusEl.textContent = 'วางรูป ' + placed.w + '×' + placed.h + ' ช่องที่ (' + placed.x + ',' + placed.y + ')'; } else { const hit = findSpriteIndexAtCell(x, y); if (hit >= 0) { gridImageSprites.splice(hit, 1); draw(); if (statusEl) statusEl.textContent = 'ลบสไปรต์รูปแล้ว'; } } 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; if (left) carryEmbedCountdownArea[y][x] = 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 === 'carryEmbedCountdown') { if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_carry') return; ensureQuizCarryAreas(); if (left) { carryEmbedCountdownArea[y][x] = 1; quizCarryHubArea[y][x] = 0; quizCarryOptionArea[y][x] = 0; } else { carryEmbedCountdownArea[y][x] = 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; carryEmbedCountdownArea[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; carryEmbedCountdownArea[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, e); 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, e); 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(); }); (function wireEditorBgScrollUi() { if (!isEditorScrollBgMap()) return; syncEditorBgScrollUiFromConfig(); const en = document.getElementById('editor-bg-scroll-enabled'); const sp = document.getElementById('editor-bg-scroll-speed'); if (en) { en.addEventListener('change', () => { editorBgScrollConfig.enabled = !!en.checked; if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); else stopEditorBgScrollRaf(); draw(); }); } if (sp) { sp.addEventListener('change', () => { editorBgScrollConfig.speedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(sp.value)) || 56)); }); } const dirSel = document.getElementById('editor-bg-scroll-direction'); if (dirSel) { dirSel.addEventListener('change', () => { editorBgScrollConfig.scrollDirection = dirSel.value === 'down' ? 'down' : 'up'; syncEditorBgScrollInitialToBottom(); draw(); if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); }); } const introF = document.getElementById('editor-bg-scroll-intro-file'); if (introF) { introF.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const r = new FileReader(); r.onload = () => { editorBgScrollConfig.introImage = r.result; loadEditorBgScrollImagePair(editorBgScrollConfig.introImage, editorBgScrollConfig.loopImage || EDITOR_SCROLL_BG_DEFAULT_LOOP, () => { syncEditorBgScrollInitialToBottom(); draw(); if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); }); }; r.readAsDataURL(file); e.target.value = ''; }); } const loopF = document.getElementById('editor-bg-scroll-loop-file'); if (loopF) { loopF.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const r = new FileReader(); r.onload = () => { editorBgScrollConfig.loopImage = r.result; loadEditorBgScrollImagePair(editorBgScrollConfig.introImage || EDITOR_SCROLL_BG_DEFAULT_INTRO, editorBgScrollConfig.loopImage, () => { syncEditorBgScrollInitialToBottom(); draw(); if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); }); }; r.readAsDataURL(file); e.target.value = ''; }); } const btnR = document.getElementById('editor-bg-scroll-reset-assets'); if (btnR) { btnR.addEventListener('click', () => { editorBgScrollConfig.introImage = null; editorBgScrollConfig.loopImage = null; loadEditorBgScrollImagePair(EDITOR_SCROLL_BG_DEFAULT_INTRO, EDITOR_SCROLL_BG_DEFAULT_LOOP, () => { syncEditorBgScrollUiFromConfig(); syncEditorBgScrollInitialToBottom(); draw(); if (editorBgScrollConfig.enabled) startEditorBgScrollRaf(); }); }); } })(); if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({}); 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 === 'gauntletLaserStart' && gameType !== 'gauntlet') drawModeEl.value = 'wall'; if (drawModeEl && drawModeEl.value === 'gauntletLaserEnd' && 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'; if (drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn' && !supportsLobbySpawnPaint(gameType)) drawModeEl.value = 'wall'; toggleFroggerUI(); syncGridImageBrushInputCaps(); syncDrawModeAuxUi(); }); const btnClearGauntletSpawns = document.getElementById('btn-clear-gauntlet-spawns'); if (btnClearGauntletSpawns) { btnClearGauntletSpawns.addEventListener('click', () => { gauntletPlayerSpawns = []; if (statusEl) statusEl.textContent = 'ล้างลำดับเกิดพรมแดง (1–6) แล้ว'; draw(); }); } const btnClearGauntletLaserSpan = document.getElementById('btn-clear-gauntlet-laser-span'); if (btnClearGauntletLaserSpan) { btnClearGauntletLaserSpan.addEventListener('click', () => { gauntletLaserRowStart = null; gauntletLaserRowEnd = null; if (statusEl) statusEl.textContent = 'ล้างช่วง laser — เลเซอร์ในเกมใช้เต็มความสูงแมปอีกครั้ง'; draw(); }); } const lobbySpawnModeEl = document.getElementById('lobby-spawn-mode'); if (lobbySpawnModeEl) { lobbySpawnModeEl.addEventListener('change', () => { if (lobbySpawnModeEl.value !== 'slots6' && drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn') drawModeEl.value = 'wall'; syncLobbySpawnAuxUi(); draw(); }); } const btnClearLobbySpawns = document.getElementById('btn-clear-lobby-spawns'); if (btnClearLobbySpawns) { btnClearLobbySpawns.addEventListener('click', () => { lobbyPlayerSpawns = [null, null, null, null, null, null]; if (statusEl) statusEl.textContent = 'ล้างจุดเกิด P1–P6 แล้ว'; draw(); }); } const lobbySlotPaintEl = document.getElementById('lobby-slot-paint'); if (lobbySlotPaintEl) { lobbySlotPaintEl.addEventListener('change', () => { 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' + '&_h=' + Date.now(); 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 bpSepSave = readBlockPlayerSeparationWHForSave(); 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, characterCollisionW: fpSave.colW, characterCollisionH: fpSave.colH, blockPlayerSeparationW: bpSepSave.w, blockPlayerSeparationH: bpSepSave.h, ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame, lobbySpawnMode: (() => { const el = document.getElementById('lobby-spawn-mode'); const v = el && el.value; return (v === 'fixed' || v === 'slots6') ? v : 'random'; })(), lobbyPlayerSpawns: (() => { ensureLobbyPlayerSpawnsLength(); sanitizeLobbyPlayerSpawnsInEditor(); return lobbyPlayerSpawns.map((s) => (s && Number.isFinite(s.x) && Number.isFinite(s.y) ? { x: Math.floor(s.x), y: Math.floor(s.y) } : null)); })(), quizTrueArea, quizFalseArea, quizQuestionArea, quizCarryHubArea: gameType === 'quiz_carry' ? quizCarryHubArea.map(r => r.slice()) : [], quizCarryOptionArea: gameType === 'quiz_carry' ? quizCarryOptionArea.map(r => r.slice()) : [], carryEmbedCountdownArea: gameType === 'quiz_carry' ? carryEmbedCountdownArea.map(r => r.slice()) : [], carryEmbedCountdownHighlight: (() => { if (gameType !== 'quiz_carry') return false; const h = document.getElementById('carry-embed-countdown-highlight'); return !h || !!h.checked; })(), carryEmbedCountdownHighlightColor: (() => { if (gameType !== 'quiz_carry') return undefined; const el = document.getElementById('carry-embed-countdown-color'); return (el && el.value) ? el.value : '#ffb44a'; })(), carryEmbedCountdownAnchor: (() => { if (gameType !== 'quiz_carry') return undefined; const el = document.getElementById('carry-embed-countdown-anchor'); const v = el && el.value; if (v === 'grid' || v === 'map' || v === 'screen') return v; return 'map'; })(), 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, gridImageLibrary: gridImageLibrary.map(serializeGridLibEntry).filter((x) => x != null), gridImageSprites: gridImageSprites.map((s) => { const o = { i: s.i, x: s.x, y: s.y, w: s.w, h: s.h }; if (s.bindCarryOption != null) o.bindCarryOption = s.bindCarryOption; return o; }), gridImageCells: deriveGridImageCellsFromSprites(), }; if (backgroundImage) body.backgroundImage = backgroundImage; const ebsSave = readEditorBgScrollConfigForSave(); if (ebsSave) body.editorBgScroll = ebsSave; body.lanes = gameType === 'frogger' ? lanes : []; sanitizeGauntletPlayerSpawns(); body.gauntletPlayerSpawns = gameType === 'gauntlet' ? gauntletPlayerSpawns.map((s) => ({ x: s.x, y: s.y })) : []; if (gameType === 'gauntlet') { sanitizeGauntletLaserRowsInEditor(); if (gauntletLaserRowStart != null && gauntletLaserRowEnd != null) { body.gauntletLaserRowStart = gauntletLaserRowStart; body.gauntletLaserRowEnd = gauntletLaserRowEnd; } else { body.gauntletLaserRowStart = null; body.gauntletLaserRowEnd = null; } } 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 (gameType === 'quiz_carry' && gridImageSprites.length) { const used = new Set(); gridImageSprites.forEach(function (s) { if (s && Number.isFinite(s.i) && s.i >= 0) used.add(Math.floor(s.i)); }); const noHeld = []; used.forEach(function (idx) { const e = gridImageLibrary[idx]; if (!gridLibHeldUrl(e)) noHeld.push(idx); }); if (noHeld.length) { const labels = noHeld.sort(function (a, b) { return a - b; }).map(function (i) { return '#' + (i + 1); }); statusEl.textContent += ' — เตือน (หยิบมาวาง): คลัง ' + labels.join(', ') + ' ยังไม่มีรูป «หยิบแล้ว» — ในเกมรูปบนพื้น/มือจะไม่สลับ (อัปโหลดช่องที่สองในคลัง หรือให้ held ต่างจาก idle)'; } } 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 gridImageToolsWrap = document.getElementById('editor-grid-image-tools'); const cellAlphaEl = document.getElementById('cell-alpha'); const cellAlphaValue = document.getElementById('cell-alpha-value'); function syncDrawModeAuxUi() { if (!drawModeEl) return; if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none'; if (gridImageToolsWrap) gridImageToolsWrap.style.display = drawModeEl.value === 'cellImage' ? 'flex' : 'none'; const bindWrap = document.getElementById('grid-sprite-carry-bind-wrap'); if (bindWrap) { const gt = gameTypeEl ? gameTypeEl.value : gameType; bindWrap.style.display = (drawModeEl.value === 'cellImage' && gt === 'quiz_carry') ? 'flex' : 'none'; } } if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); syncLobbySpawnAuxUi(); }); syncDrawModeAuxUi(); syncLobbySpawnAuxUi(); const gridCellUpload = document.getElementById('grid-cell-image-upload'); if (gridCellUpload) { gridCellUpload.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; if (file.size > 6 * 1024 * 1024) { if (statusEl) statusEl.textContent = 'ไฟล์ใหญ่เกิน 6MB — ลดขนาดก่อนอัปโหลด'; e.target.value = ''; return; } const r = new FileReader(); r.onload = () => { const dataUrl = r.result; if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) { e.target.value = ''; return; } gridImageLibrary.push({ idle: dataUrl, held: null }); gridImageBrushIndex = gridImageLibrary.length - 1; delete editorGridImageElByIndex[gridImageBrushIndex]; primeEditorGridImage(gridImageBrushIndex); rebuildGridImageGallery(); syncGridImageBrushInputCaps(); draw(); if (statusEl) statusEl.textContent = 'เพิ่มรูปเข้าคลังแล้ว — คลิกซ้ายบนกริดเพื่อวาง (ตั้งกว้าง×สูงก่อนได้)'; }; r.readAsDataURL(file); e.target.value = ''; }); } const gridHeldUpload = document.getElementById('grid-cell-image-held-upload'); if (gridHeldUpload) { gridHeldUpload.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; const idx = pendingHeldLibIndex; pendingHeldLibIndex = -1; e.target.value = ''; if (!file || idx < 0 || idx >= gridImageLibrary.length) return; if (file.size > 6 * 1024 * 1024) { if (statusEl) statusEl.textContent = 'ไฟล์ใหญ่เกิน 6MB — ลดขนาดก่อนอัปโหลด'; return; } const r = new FileReader(); r.onload = () => { const dataUrl = r.result; if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) return; const idle = gridLibIdleUrl(gridImageLibrary[idx]); if (dataUrl === idle) { if (statusEl) statusEl.textContent = 'ไฟล์นี้เหมือนรูปก่อนหยิบทุกไบต์ — เลือกภาพอื่นสำหรับ «หยิบแล้ว» ถึงจะเห็นต่างในเกม'; rebuildGridImageGallery(); draw(); return; } gridImageLibrary[idx] = { idle, held: dataUrl }; delete editorGridImageHeldElByIndex[idx]; rebuildGridImageGallery(); draw(); if (statusEl) statusEl.textContent = 'ตั้งรูปตอนถือสำหรับ #' + (idx + 1) + ' แล้ว (quiz carry — แสดงบนแมปจนกว่าจะปล่อย)'; }; r.readAsDataURL(file); }); } const btnRmGridLib = document.getElementById('btn-grid-image-remove-from-lib'); if (btnRmGridLib) { btnRmGridLib.addEventListener('click', () => { if (!gridImageLibrary.length) return; removeGridImageFromLibraryAt(gridImageBrushIndex); }); } const brushWEl0 = document.getElementById('grid-image-brush-w'); const brushHEl0 = document.getElementById('grid-image-brush-h'); if (brushWEl0) brushWEl0.addEventListener('input', () => { syncGridImageBrushInputCaps(); draw(); }); if (brushHEl0) brushHEl0.addEventListener('input', () => { syncGridImageBrushInputCaps(); draw(); }); 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', () => { const fp = readCharacterFootprintInputs(); applyCharacterFootprintInputs(fp.cw, fp.ch); draw(); }); if (characterCellsHEl) characterCellsHEl.addEventListener('input', () => { const fp = readCharacterFootprintInputs(); applyCharacterFootprintInputs(fp.cw, fp.ch); draw(); }); if (characterCollisionWEl) characterCollisionWEl.addEventListener('input', () => { const fp = readCharacterFootprintInputs(); applyCharacterFootprintInputs(fp.cw, fp.ch); draw(); }); if (characterCollisionHEl) characterCollisionHEl.addEventListener('input', () => { const fp = readCharacterFootprintInputs(); applyCharacterFootprintInputs(fp.cw, fp.ch); draw(); }); function clampSepInput(el) { if (!el) return; let v = Math.floor(Number(el.value)); if (!Number.isFinite(v) || v < 0) v = 0; el.value = String(Math.min(4, v)); } if (blockPlayerSeparationWEl) { blockPlayerSeparationWEl.addEventListener('input', () => { clampSepInput(blockPlayerSeparationWEl); draw(); }); } if (blockPlayerSeparationHEl) { blockPlayerSeparationHEl.addEventListener('input', () => { clampSepInput(blockPlayerSeparationHEl); 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); let colWL = Number(m.characterCollisionW); let colHL = Number(m.characterCollisionH); colWL = Number.isFinite(colWL) ? Math.max(1, Math.min(cwL, Math.floor(colWL))) : cwL; colHL = Number.isFinite(colHL) ? Math.max(1, Math.min(chL, Math.floor(colHL))) : chL; if (characterCollisionWEl) characterCollisionWEl.value = String(colWL); if (characterCollisionHEl) characterCollisionHEl.value = String(colHL); if (blockPlayerSeparationWEl || blockPlayerSeparationHEl) { let wS = Math.floor(Number(m.blockPlayerSeparationW)); let hS = Math.floor(Number(m.blockPlayerSeparationH)); if (!Number.isFinite(wS) || wS < 0) { const leg = Math.floor(Number(m.blockPlayerSeparation)); wS = Number.isFinite(leg) && leg >= 0 ? Math.min(4, leg) : 0; } else wS = Math.min(4, wS); if (!Number.isFinite(hS) || hS < 0) { const leg = Math.floor(Number(m.blockPlayerSeparation)); hS = Number.isFinite(leg) && leg >= 0 ? Math.min(4, leg) : 0; } else hS = Math.min(4, hS); if (blockPlayerSeparationWEl) blockPlayerSeparationWEl.value = String(wS); if (blockPlayerSeparationHEl) blockPlayerSeparationHEl.value = String(hS); } 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)); carryEmbedCountdownArea = m.carryEmbedCountdownArea && m.carryEmbedCountdownArea.length ? m.carryEmbedCountdownArea.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)); gridImageLibrary = Array.isArray(m.gridImageLibrary) ? m.gridImageLibrary.map(normalizeGridLibEntry).filter((e) => e && gridLibIdleUrl(e) && gridLibIdleUrl(e).length < 25000000) : []; gridImageSprites = migrateSpritesFromMapJson(m, width, height); sanitizeSpritesInPlace(); gridImageBrushIndex = Math.min(gridImageBrushIndex, Math.max(0, gridImageLibrary.length - 1)); Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; }); Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; }); rebuildGridImageGallery(); syncGridImageBrushInputCaps(); showMapInGame = m.showMapInGame !== false; const showMapEl = document.getElementById('show-map-in-game'); if (showMapEl) showMapEl.checked = showMapInGame; const cdH = document.getElementById('carry-embed-countdown-highlight'); if (cdH) cdH.checked = m.carryEmbedCountdownHighlight !== false; const cdC = document.getElementById('carry-embed-countdown-color'); if (cdC && typeof m.carryEmbedCountdownHighlightColor === 'string') { const hx = m.carryEmbedCountdownHighlightColor.trim(); if (/^#[0-9a-fA-F]{6}$/.test(hx)) cdC.value = hx; } const cdA = document.getElementById('carry-embed-countdown-anchor'); if (cdA) { const a = m.carryEmbedCountdownAnchor; cdA.value = (a === 'grid' || a === 'screen' || a === 'map') ? a : 'map'; } ensureInteractive(); ensureStartGameArea(); ensureSpawnArea(); backgroundImage = m.backgroundImage || null; if (backgroundImage) { backgroundImageImg = new Image(); backgroundImageImg.onload = () => { draw(); }; backgroundImageImg.src = backgroundImage; } else backgroundImageImg = null; applyEditorBgScrollFromMap(m); 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)) : []; const rs = m.gauntletLaserRowStart; const re = m.gauntletLaserRowEnd; if (rs != null && re != null && Number.isFinite(Number(rs)) && Number.isFinite(Number(re))) { gauntletLaserRowStart = Math.floor(Number(rs)); gauntletLaserRowEnd = Math.floor(Number(re)); sanitizeGauntletLaserRowsInEditor(); } else { gauntletLaserRowStart = null; gauntletLaserRowEnd = null; } lobbyPlayerSpawns = [null, null, null, null, null, null]; if (Array.isArray(m.lobbyPlayerSpawns)) { for (let li = 0; li < 6 && li < m.lobbyPlayerSpawns.length; li++) { const c = m.lobbyPlayerSpawns[li]; if (c && c.x != null && c.y != null) { lobbyPlayerSpawns[li] = { x: Math.floor(Number(c.x)), y: Math.floor(Number(c.y)) }; } } } sanitizeLobbyPlayerSpawnsInEditor(); const lsmLoad = document.getElementById('lobby-spawn-mode'); if (lsmLoad) { const mv = m.lobbySpawnMode; lsmLoad.value = (mv === 'fixed' || mv === 'slots6') ? mv : 'random'; } 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(); } })();