(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 '
โหมด พรมแดงสุดท้าย
' + 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(); } })();