(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 = []; /** jump_survive: กริด 0/1 — ช่องที่ 1 = โซนตาย (ชน hitbox แล้วตายทันทีในเกม) */ let jumpSurviveHazardArea = []; /** กริด 0 / 1–3 = รูปแพลตฟอร์มจาก jumpSurvivePlatformTiles ใน game-timing */ let jumpSurvivePlatformVariantArea = []; /** จาก GET /api/game-timing — แสดงในแคนวาสเอดิเตอร์ตามช่อง variant */ let editorJumpSurvivePlatformTilesCfg = [ { url: '', w: 0, h: 0 }, { url: '', w: 0, h: 0 }, { url: '', w: 0, h: 0 }, ]; let editorJumpSurvivePlatformTileImgs = [null, null, null]; let editorJumpSurvivePlatformTimingGen = 0; 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 customizeSpot = null; // จุด "ห้องแต่งตัว" ในฉาก (เดินไปกด F) — วางเองในeditor let hostConsoleSpot = null; // จุด "ตั้งค่าโฮสต์" (Host Console) — วางใน editor 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'; } /** Stack Tower ภารกิจ (mnn93hpi) — HUD embed + พื้นหลังแนวตั้ง intro/loop */ const EDITOR_STACK_TOWER_CAM_MAP_ID = 'mnn93hpi'; const EDITOR_STACK_TOWER_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png'; const EDITOR_STACK_TOWER_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png'; let stackTowerBgScrollConfig = { enabled: false, speedPxPerSec: 0, scrollDirection: 'down', introImage: null, loopImage: null, stepEveryLayers: 0, stepScrollPx: 0, stepAnimMs: 520, releaseGapWorldPx: null, }; let stackTowerBgScrollIntroImg = null; let stackTowerBgScrollLoopImg = null; let stackTowerBgScrollPx = 0; function isEditorStackTowerMissionMap() { const gt = gameTypeEl ? gameTypeEl.value : gameType; return mapId === EDITOR_STACK_TOWER_CAM_MAP_ID && gt === 'stack'; } function syncEditorStackTowerVerticalBgBlockVisibility() { const block = document.getElementById('editor-stack-tower-vertical-bg-block'); if (!block) return; block.hidden = !isEditorStackTowerMissionMap(); } function syncStackTowerCamBlockVisibilityOnly() { syncEditorStackTowerVerticalBgBlockVisibility(); if (!isEditorStackTowerMissionMap()) stopStackTowerBgScrollRaf(); } function stopStackTowerBgScrollRaf() { /* เคยใช้ RAF — เก็บชื่อให้จุดเรียกเดิมไม่พัง */ } function isEditorStackTowerBgDrawing() { if (!isEditorStackTowerMissionMap() || !stackTowerBgScrollConfig.enabled) return false; const a = stackTowerBgScrollIntroImg; const b = stackTowerBgScrollLoopImg; return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); } function loadStackTowerBgScrollImagePair(introSrc, loopSrc, onDone) { stopStackTowerBgScrollRaf(); stackTowerBgScrollIntroImg = null; stackTowerBgScrollLoopImg = null; let pending = 2; const doneOne = () => { pending--; if (pending <= 0 && typeof onDone === 'function') onDone(); }; const im1 = new Image(); im1.onload = () => { stackTowerBgScrollIntroImg = im1; doneOne(); }; im1.onerror = () => { doneOne(); }; im1.src = introSrc; const im2 = new Image(); im2.onload = () => { stackTowerBgScrollLoopImg = im2; doneOne(); }; im2.onerror = () => { doneOne(); }; im2.src = loopSrc; } function syncStackTowerBgScrollUiFromConfig() { const en = document.getElementById('editor-stack-tower-vbg-enabled'); const sp = document.getElementById('editor-stack-tower-vbg-speed'); const dir = document.getElementById('editor-stack-tower-vbg-direction'); const stL = document.getElementById('editor-stack-tower-vbg-step-layers'); const stPx = document.getElementById('editor-stack-tower-vbg-step-px'); const stMs = document.getElementById('editor-stack-tower-vbg-step-anim-ms'); const relG = document.getElementById('editor-stack-tower-vbg-release-gap'); if (en) en.checked = !!stackTowerBgScrollConfig.enabled; if (sp) sp.value = String(stackTowerBgScrollConfig.speedPxPerSec != null ? stackTowerBgScrollConfig.speedPxPerSec : 0); if (dir) dir.value = stackTowerBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up'; if (stL) stL.value = String(stackTowerBgScrollConfig.stepEveryLayers != null ? stackTowerBgScrollConfig.stepEveryLayers : 0); if (stPx) stPx.value = String(stackTowerBgScrollConfig.stepScrollPx != null ? stackTowerBgScrollConfig.stepScrollPx : 0); if (stMs) stMs.value = String(stackTowerBgScrollConfig.stepAnimMs != null ? stackTowerBgScrollConfig.stepAnimMs : 520); if (relG) { const g = stackTowerBgScrollConfig.releaseGapWorldPx; relG.value = g != null && Number.isFinite(Number(g)) ? String(Math.floor(Number(g))) : ''; } } function syncStackTowerBgScrollInitialToBottom() { const intro = stackTowerBgScrollIntroImg; 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)); const rawDownLike = stackTowerBgScrollConfig.scrollDirection === 'down'; const flowDown = !rawDownLike; if (flowDown) { stackTowerBgScrollPx = 0; } else { stackTowerBgScrollPx = Math.max(0, drawHIntro - chR); } } function foldStackTowerBgScrollSViewEditor(S, drawHIntro, drawHLoop) { const dH = Math.max(1, Math.round(Number(drawHLoop) || 1)); const dI = Math.max(0, Math.round(Number(drawHIntro) || 0)); const s = Number(S) || 0; if (s < dI) return s; return s - Math.floor((s - dI) / dH) * dH; } function drawStackTowerScrollingBackground(cw, ch) { const intro = stackTowerBgScrollIntroImg; const loop = stackTowerBgScrollLoopImg; 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 = foldStackTowerBgScrollSViewEditor(stackTowerBgScrollPx, drawHIntro, drawHLoop); const vp0 = S; const vp1 = S + chR; const yLimit = vp1 + drawHLoop * 96; const rawDownLike = stackTowerBgScrollConfig.scrollDirection === 'down'; const flowDown = !rawDownLike; 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 + n * 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 applyStackTowerBgScrollFromMap(m) { stopStackTowerBgScrollRaf(); stackTowerBgScrollIntroImg = null; stackTowerBgScrollLoopImg = null; stackTowerBgScrollPx = 0; if (!isEditorStackTowerMissionMap()) { stackTowerBgScrollConfig = { enabled: false, speedPxPerSec: 0, scrollDirection: 'down', introImage: null, loopImage: null, stepEveryLayers: 0, stepScrollPx: 0, stepAnimMs: 520, releaseGapWorldPx: null, }; return; } const raw = m && m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll : {}; const spNum = Number(raw.speedPxPerSec); const sp = Number.isFinite(spNum) ? Math.max(0, Math.min(400, Math.floor(spNum))) : 0; const sd = String(raw.scrollDirection || raw.direction || 'down').toLowerCase(); const sel = Math.floor(Number(raw.stepEveryLayers)); const stepEveryLayers = Number.isFinite(sel) ? Math.max(0, Math.min(200, sel)) : 0; const ssp = Math.floor(Number(raw.stepScrollPx)); const stepScrollPx = Number.isFinite(ssp) ? Math.max(0, Math.min(8000, ssp)) : 0; const sam = Math.floor(Number(raw.stepAnimMs)); const stepAnimMs = Number.isFinite(sam) ? Math.max(120, Math.min(4000, sam)) : 520; const rg = Number(raw.releaseGapWorldPx); const releaseGapWorldPx = Number.isFinite(rg) && rg >= 0 ? Math.min(800, Math.floor(rg)) : null; stackTowerBgScrollConfig = { enabled: raw.enabled === true, 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, stepEveryLayers, stepScrollPx, stepAnimMs, releaseGapWorldPx, }; const introSrc = stackTowerBgScrollConfig.introImage || EDITOR_STACK_TOWER_BG_DEFAULT_INTRO; const loopSrc = stackTowerBgScrollConfig.loopImage || EDITOR_STACK_TOWER_BG_DEFAULT_LOOP; loadStackTowerBgScrollImagePair(introSrc, loopSrc, () => { syncStackTowerBgScrollUiFromConfig(); syncStackTowerBgScrollInitialToBottom(); draw(); }); } function readStackTowerBgScrollForSave() { if (!isEditorStackTowerMissionMap()) return undefined; const en = document.getElementById('editor-stack-tower-vbg-enabled'); const spEl = document.getElementById('editor-stack-tower-vbg-speed'); const spNum = Number(spEl && spEl.value); const sp = Number.isFinite(spNum) ? Math.max(0, Math.min(400, Math.floor(spNum))) : 0; const dirEl = document.getElementById('editor-stack-tower-vbg-direction'); const stLEl = document.getElementById('editor-stack-tower-vbg-step-layers'); const stPxEl = document.getElementById('editor-stack-tower-vbg-step-px'); const stMsEl = document.getElementById('editor-stack-tower-vbg-step-anim-ms'); const relEl = document.getElementById('editor-stack-tower-vbg-release-gap'); const selRaw = Number(stLEl && stLEl.value); const stepEveryLayers = Number.isFinite(selRaw) ? Math.max(0, Math.min(200, Math.floor(selRaw))) : 0; const sspRaw = Number(stPxEl && stPxEl.value); const stepScrollPx = Number.isFinite(sspRaw) ? Math.max(0, Math.min(8000, Math.floor(sspRaw))) : 0; const samRaw = Number(stMsEl && stMsEl.value); const stepAnimMs = Number.isFinite(samRaw) ? Math.max(120, Math.min(4000, Math.floor(samRaw))) : 520; const relStr = relEl && String(relEl.value).trim(); const relNum = relStr === '' ? null : Number(relStr); const releaseGapWorldPx = relNum != null && Number.isFinite(relNum) && relNum >= 0 ? Math.min(800, Math.floor(relNum)) : null; const out = { enabled: !!(en && en.checked), speedPxPerSec: sp, scrollDirection: dirEl && dirEl.value === 'down' ? 'down' : 'up', stepEveryLayers, stepScrollPx, stepAnimMs, }; if (releaseGapWorldPx != null) out.releaseGapWorldPx = releaseGapWorldPx; if (stackTowerBgScrollConfig.introImage) out.introImage = stackTowerBgScrollConfig.introImage; if (stackTowerBgScrollConfig.loopImage) out.loopImage = stackTowerBgScrollConfig.loopImage; return out; } function applyStackTowerCameraFollowFromMap(m) { stopStackTowerBgScrollRaf(); syncEditorStackTowerVerticalBgBlockVisibility(); if (!isEditorStackTowerMissionMap()) { stackTowerBgScrollConfig = { enabled: false, speedPxPerSec: 0, scrollDirection: 'down', introImage: null, loopImage: null, stepEveryLayers: 0, stepScrollPx: 0, stepAnimMs: 520, releaseGapWorldPx: null, }; stackTowerBgScrollIntroImg = null; stackTowerBgScrollLoopImg = null; stackTowerBgScrollPx = 0; return; } applyStackTowerBgScrollFromMap(m || {}); } function readStackTowerCameraFollowForSave() { if (!isEditorStackTowerMissionMap()) return undefined; return { enabled: false, pxPerLayer: 12, maxPx: 260 }; } /** พื้นหลังแนวนอน start + วน 2–4 + finish — เฉพาะแมป Last Light mno9kb07 (Editor + map JSON: gauntletCrownRunwayBg) */ const EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID = 'mno9kb07'; const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png'; const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2 = BASE + '/img/editor-bg-mno9kb07/loop2.png'; const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3 = BASE + '/img/editor-bg-mno9kb07/loop3.png'; const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4 = BASE + '/img/editor-bg-mno9kb07/loop4.png'; const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH = BASE + '/img/editor-bg-mno9kb07/finish.png'; let editorGauntletRunwayBgConfig = { enabled: true, speedPxPerSec: 48, runwaySpawnAlignFrac: 0.32, startImage: null, loopImage2: null, loopImage3: null, loopImage4: null, finishImage: null, }; let editorGauntletRunwayBgImgs = [null, null, null, null, null]; let editorGauntletRunwayBgScrollPx = 0; let editorGauntletRunwayBgRaf = null; let editorGauntletRunwayBgLastTs = 0; /** ชื่อไฟล์ล่าสุดที่ผู้ใช้เลือก (ช่อง input จะว่างหลังอ่านไฟล์ — ใช้โชว์สถานะแทน) */ let editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '', }; const RUNWAY_PICK_MAX_W = 2048; const RUNWAY_PICK_MAX_H = 1152; const RUNWAY_PICK_COMPRESS_MIN_LEN = 650000; /** ลดขนาด data URL ก่อนเก็บใน JSON — กันบันทึกไม่ขึ้นเมื่อรวมแมปใหญ่เกิน client_max_body_size */ function compressRunwayDataUrlIfLarge(dataUrl, done) { if (typeof dataUrl !== 'string' || dataUrl.indexOf('data:image') !== 0) { done(dataUrl); return; } const im = new Image(); im.onload = () => { try { const nw = im.naturalWidth; const nh = im.naturalHeight; const needShrink = dataUrl.length > RUNWAY_PICK_COMPRESS_MIN_LEN || nw > RUNWAY_PICK_MAX_W || nh > RUNWAY_PICK_MAX_H; if (!needShrink || nw < 1 || nh < 1) { done(dataUrl); return; } let w = nw; let h = nh; const sc = Math.min(1, RUNWAY_PICK_MAX_W / w, RUNWAY_PICK_MAX_H / h); w = Math.max(1, Math.round(w * sc)); h = Math.max(1, Math.round(h * sc)); const c = document.createElement('canvas'); c.width = w; c.height = h; const g = c.getContext('2d'); if (!g) { done(dataUrl); return; } g.imageSmoothingEnabled = true; g.drawImage(im, 0, 0, w, h); let best = c.toDataURL('image/jpeg', 0.84); try { const wu = c.toDataURL('image/webp', 0.84); if (wu.length < best.length) best = wu; } catch (e1) { /* webp ไม่รองรับ */ } done(best.length < dataUrl.length ? best : dataUrl); } catch (e) { done(dataUrl); } }; im.onerror = () => { done(dataUrl); }; im.src = dataUrl; } function isEditorGauntletRunwayBgMap() { const gt = gameTypeEl ? gameTypeEl.value : gameType; return mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID && gt === 'gauntlet'; } function stopEditorGauntletRunwayBgRaf() { if (editorGauntletRunwayBgRaf != null) { cancelAnimationFrame(editorGauntletRunwayBgRaf); editorGauntletRunwayBgRaf = null; } editorGauntletRunwayBgLastTs = 0; } function syncEditorGauntletRunwayBgBlockVisibility() { const block = document.getElementById('editor-gauntlet-runway-bg-block'); if (!block) return; /** โชว์ทันทีจากรหัสใน URL — ไม่ผูกกับค่า gameType ตอนก่อน fetch (เดิมเป็น zep ทำให้บล็อกถูกซ่อนจนเหมือนไม่มีฟีเจอร์) */ const show = mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID; if (!show) stopEditorGauntletRunwayBgRaf(); block.hidden = !show; } /** Admin embed: แสดง HUD จำลองแบบ play (mnn93hpi / cyber SCORE + TIME + SYSTEM INTEGRITY) เมื่อโหมด Stack */ function syncEditorStackHudMock() { const el = document.getElementById('editor-stack-hud-mock'); if (!el || !editorEmbed) return; const gt = gameTypeEl ? gameTypeEl.value : gameType; const show = gt === 'stack'; el.classList.toggle('is-hidden', !show); el.setAttribute('aria-hidden', show ? 'false' : 'true'); } function editorGauntletRunwayBgEligibleFromMap(m) { const gt = (m && m.gameType != null && m.gameType !== '') ? m.gameType : (gameTypeEl ? gameTypeEl.value : gameType); return mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID && gt === 'gauntlet'; } function editorGauntletRunwayBgStripWidthsAtCh(chR) { const ch0 = Math.max(1, Math.round(chR)); const wOf = (im) => { if (!im || !im.complete || !im.naturalWidth || !im.naturalHeight) return 0; return Math.max(1, Math.round(im.naturalWidth * (ch0 / im.naturalHeight))); }; return { w0: wOf(editorGauntletRunwayBgImgs[0]), w1: wOf(editorGauntletRunwayBgImgs[1]), w2: wOf(editorGauntletRunwayBgImgs[2]), w3: wOf(editorGauntletRunwayBgImgs[3]), }; } function isEditorGauntletRunwayBgDrawing() { if (!isEditorGauntletRunwayBgMap() || !editorGauntletRunwayBgConfig.enabled) return false; for (let i = 0; i < 5; i++) { const im = editorGauntletRunwayBgImgs[i]; if (!im || !im.complete || !im.naturalWidth) return false; } return true; } function drawRunwayHSliceEditor(ctx, img, worldX0, segW, scrollLeft, cwR, chR) { if (!img || !img.complete || !img.naturalWidth || !img.naturalHeight || segW <= 0) return; const natW = img.naturalWidth; const natH = img.naturalHeight; const v0 = scrollLeft; const v1 = scrollLeft + cwR; const o0 = Math.max(worldX0, v0); const o1 = Math.min(worldX0 + segW, v1); if (o1 <= o0) return; const u0 = ((o0 - worldX0) / segW) * natW; const u1 = ((o1 - worldX0) / segW) * natW; const dx = o0 - scrollLeft; const dw = o1 - o0; ctx.drawImage(img, u0, 0, u1 - u0, natH, dx, 0, dw, chR); } function drawEditorGauntletRunwayScrollingBackground(cw, ch) { const start = editorGauntletRunwayBgImgs[0]; const L2 = editorGauntletRunwayBgImgs[1]; const L3 = editorGauntletRunwayBgImgs[2]; const L4 = editorGauntletRunwayBgImgs[3]; const cwR = Math.max(1, Math.round(cw)); const chR = Math.max(1, Math.round(ch)); const { w0, w1, w2, w3 } = editorGauntletRunwayBgStripWidthsAtCh(chR); const cycle = w1 + w2 + w3; const S = editorGauntletRunwayBgScrollPx; ctx.save(); ctx.imageSmoothingEnabled = true; ctx.fillStyle = '#1a1b26'; ctx.fillRect(0, 0, cwR, chR); if (w0 > 0) drawRunwayHSliceEditor(ctx, start, 0, w0, S, cwR, chR); if (cycle <= 0) { ctx.restore(); return; } const ws = [w1, w2, w3]; const ims = [L2, L3, L4]; const uTotal = Math.max(0, S - w0); let cycN = Math.floor(uTotal / cycle); let uRem = uTotal - cycN * cycle; let si = 0; while (si < 3 && uRem >= ws[si]) { uRem -= ws[si]; si++; } if (si >= 3) { cycN++; si = 0; uRem = 0; } let acc = 0; for (let k = 0; k < si; k++) acc += ws[k]; let curLeft = w0 + cycN * cycle + acc; let guard = 0; let idx = si; while (curLeft < S + cwR && guard++ < 500) { drawRunwayHSliceEditor(ctx, ims[idx], curLeft, ws[idx], S, cwR, chR); curLeft += ws[idx]; idx = (idx + 1) % 3; } ctx.restore(); } function editorGauntletRunwayBgTick(ts) { editorGauntletRunwayBgRaf = null; if (!isEditorGauntletRunwayBgDrawing()) return; if (!editorGauntletRunwayBgLastTs) editorGauntletRunwayBgLastTs = ts; const dt = Math.min(64, Math.max(0, ts - editorGauntletRunwayBgLastTs)); editorGauntletRunwayBgLastTs = ts; editorGauntletRunwayBgScrollPx += (editorGauntletRunwayBgConfig.speedPxPerSec || 48) * (dt / 1000); draw(); editorGauntletRunwayBgRaf = requestAnimationFrame(editorGauntletRunwayBgTick); } function startEditorGauntletRunwayBgRaf() { if (!isEditorGauntletRunwayBgDrawing()) return; stopEditorGauntletRunwayBgRaf(); editorGauntletRunwayBgLastTs = 0; editorGauntletRunwayBgRaf = requestAnimationFrame(editorGauntletRunwayBgTick); } function loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, onDone) { stopEditorGauntletRunwayBgRaf(); editorGauntletRunwayBgImgs = [null, null, null, null, null]; let pending = 5; const doneOne = () => { pending--; if (pending <= 0 && typeof onDone === 'function') onDone(); }; const srcs = [s0, s1, s2, s3, s4]; for (let i = 0; i < 5; i++) { const im = new Image(); const idx = i; im.onload = () => { editorGauntletRunwayBgImgs[idx] = im; doneOne(); }; im.onerror = () => { doneOne(); }; im.src = srcs[i]; } } function applyGauntletCrownRunwayBgFromMap(m) { syncEditorGauntletRunwayBgBlockVisibility(); if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) { editorGauntletRunwayBgConfig = { enabled: true, speedPxPerSec: 48, runwaySpawnAlignFrac: 0.32, startImage: null, loopImage2: null, loopImage3: null, loopImage4: null, finishImage: null, }; editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' }; stopEditorGauntletRunwayBgRaf(); editorGauntletRunwayBgImgs = [null, null, null, null, null]; return; } editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' }; const raw = m && m.gauntletCrownRunwayBg && typeof m.gauntletCrownRunwayBg === 'object' ? m.gauntletCrownRunwayBg : {}; const sp = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 48)); const afRaw = Number(raw.runwaySpawnAlignFrac); const af = Number.isFinite(afRaw) ? Math.max(0.02, Math.min(0.95, afRaw)) : 0.32; editorGauntletRunwayBgConfig = { enabled: raw.enabled !== false, speedPxPerSec: sp, runwaySpawnAlignFrac: af, startImage: typeof raw.startImage === 'string' && raw.startImage.length ? raw.startImage : null, loopImage2: typeof raw.loopImage2 === 'string' && raw.loopImage2.length ? raw.loopImage2 : null, loopImage3: typeof raw.loopImage3 === 'string' && raw.loopImage3.length ? raw.loopImage3 : null, loopImage4: typeof raw.loopImage4 === 'string' && raw.loopImage4.length ? raw.loopImage4 : null, finishImage: typeof raw.finishImage === 'string' && raw.finishImage.length ? raw.finishImage : null, }; const eligible = editorGauntletRunwayBgEligibleFromMap(m || {}); if (!editorGauntletRunwayBgConfig.enabled) { stopEditorGauntletRunwayBgRaf(); editorGauntletRunwayBgImgs = [null, null, null, null, null]; syncEditorGauntletRunwayBgUiFromConfig(); draw(); return; } const s0 = editorGauntletRunwayBgConfig.startImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START; const s1 = editorGauntletRunwayBgConfig.loopImage2 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2; const s2 = editorGauntletRunwayBgConfig.loopImage3 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3; const s3 = editorGauntletRunwayBgConfig.loopImage4 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4; const s4 = editorGauntletRunwayBgConfig.finishImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH; editorGauntletRunwayBgScrollPx = 0; loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, () => { syncEditorGauntletRunwayBgUiFromConfig(); draw(); stopEditorGauntletRunwayBgRaf(); if (eligible && editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf(); }); } function syncEditorGauntletRunwayBgUiFromConfig() { const en = document.getElementById('editor-gauntlet-runway-bg-enabled'); const sp = document.getElementById('editor-gauntlet-runway-bg-speed'); const af = document.getElementById('editor-gauntlet-runway-bg-align-frac'); if (en) en.checked = !!editorGauntletRunwayBgConfig.enabled; if (sp) sp.value = String(editorGauntletRunwayBgConfig.speedPxPerSec || 48); if (af) { const v = editorGauntletRunwayBgConfig.runwaySpawnAlignFrac; af.value = String(Number.isFinite(v) ? Math.max(0.02, Math.min(0.95, v)) : 0.32); } syncEditorGauntletRunwayBgUploadSummary(); syncEditorGauntletRunwayBgThumbnails(); } function syncEditorGauntletRunwayBgThumbnails() { if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) return; const keys = ['startImage', 'loopImage2', 'loopImage3', 'loopImage4', 'finishImage']; const defaults = [ EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH, ]; const labels = ['START', 'LOOP', 'LOOP', 'LOOP', 'FINISH']; for (let i = 0; i < 5; i++) { const el = document.getElementById('editor-gauntlet-runway-bg-prev-' + i); if (!el) continue; const custom = editorGauntletRunwayBgConfig[keys[i]]; const src = (typeof custom === 'string' && custom.length) ? custom : defaults[i]; el.alt = 'Runway ' + labels[i] + ' — slot ' + (i + 1); el.src = src; } } function describeRunwaySlotStoredValue(val, fileName) { if (!val || typeof val !== 'string' || !val.length) { return { th: 'ยังไม่อัปโหลด — เกมใช้พื้นสีชั่วคราว', en: 'Not set — game uses soft color strip' }; } if (val.startsWith('data:image')) { const fn = (fileName && String(fileName).trim()) ? ` · ${fileName.trim()}` : ''; return { th: `อัปโหลดแล้ว (เก็บในฉาก)${fn}`, en: `Saved in map (data URL)${fn}` }; } const short = val.length > 56 ? (val.slice(0, 54) + '…') : val; return { th: `ตั้งจากแมป/URL: ${short}`, en: `From map/URL: ${short}` }; } function syncEditorGauntletRunwayBgUploadSummary() { const el = document.getElementById('editor-gauntlet-runway-bg-upload-summary'); if (!el) return; if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) { el.textContent = ''; return; } const keys = ['startImage', 'loopImage2', 'loopImage3', 'loopImage4', 'finishImage']; const labels = ['1 เริ่ม', '2 ลูป', '3 ลูป', '4 ลูป', '5 จบ']; const parts = []; for (let i = 0; i < keys.length; i++) { const d = describeRunwaySlotStoredValue(editorGauntletRunwayBgConfig[keys[i]], editorGauntletRunwayBgFileNames[keys[i]]); parts.push(`${labels[i]}: ${d.th} / ${d.en}`); } const tail = '\n\nพรีวิวรูปแถวด้านบน = สิ่งที่เกมจะใช้ (รวมรูปเซิร์ฟถ้าช่องว่าง)\n' + 'Thumbnails above = what the game uses (bundled defaults if slot empty).\n\n' + 'หลังเลือกไฟล์ ช่องจะกลับเป็น «No file chosen» เป็นปกติของเบราว์เซอร์\n' + 'After pick, the file box clears — use thumbnails + this text.\n\n' + 'ต้องกด บันทึกฉาก ถึงจะไปโชว์ในเกมจริง · Click Save scene for play mode.'; el.textContent = 'สถานะรูปรันเวย์ · Runway images\n' + parts.join('\n') + tail; } function readGauntletCrownRunwayBgForSave() { /** บันทึกจากรหัสแมป — ไม่ผูกกับ dropdown โหมดอย่างเดียว (เดิมถ้าไม่ใช่ gauntlet จะไม่ส่ง → รีเฟรชแล้วหาย) · ส่งครบทุกคีย์เพราะ PUT แทนที่ทั้ง object */ if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) return undefined; const en = document.getElementById('editor-gauntlet-runway-bg-enabled'); const spEl = document.getElementById('editor-gauntlet-runway-bg-speed'); const sp = Math.max(8, Math.min(400, Math.floor(Number(spEl && spEl.value)) || 48)); const s = (k) => { const v = editorGauntletRunwayBgConfig[k]; return (typeof v === 'string' && v.length) ? v : null; }; const afEl = document.getElementById('editor-gauntlet-runway-bg-align-frac'); const afv = Math.max(0.02, Math.min(0.95, Number(afEl && afEl.value) || 0.32)); return { enabled: !!(en && en.checked), speedPxPerSec: sp, runwaySpawnAlignFrac: afv, startImage: s('startImage'), loopImage2: s('loopImage2'), loopImage3: s('loopImage3'), loopImage4: s('loopImage4'), finishImage: s('finishImage'), }; } 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); } } } ensureJumpSurvivePlatformVariantArea(); } function ensureJumpSurvivePlatformVariantArea() { if (!jumpSurvivePlatformVariantArea.length || jumpSurvivePlatformVariantArea.length !== height) { const existing = jumpSurvivePlatformVariantArea.slice().map((r) => r && r.slice()); jumpSurvivePlatformVariantArea = []; for (let y = 0; y < height; y++) { const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0); jumpSurvivePlatformVariantArea.push(row); } } else { for (let y = 0; y < height; y++) { if (!jumpSurvivePlatformVariantArea[y] || jumpSurvivePlatformVariantArea[y].length !== width) { jumpSurvivePlatformVariantArea[y] = Array(width).fill(0); } } } for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (jumpSurvivePlatformArea[y] && jumpSurvivePlatformArea[y][x] === 1) { let v = jumpSurvivePlatformVariantArea[y][x]; if (!Number.isFinite(v) || v < 1) jumpSurvivePlatformVariantArea[y][x] = 1; else if (v > 3) jumpSurvivePlatformVariantArea[y][x] = 3; } else { jumpSurvivePlatformVariantArea[y][x] = 0; } } } } function editorJumpSurviveVariantAtCell(x, y) { if (!jumpSurvivePlatformVariantArea[y]) return 1; let v = Math.floor(Number(jumpSurvivePlatformVariantArea[y][x])); if (!Number.isFinite(v) || v < 1) v = 1; if (v > 3) v = 3; return v; } function ensureJumpSurviveHazardArea() { if (!jumpSurviveHazardArea.length || jumpSurviveHazardArea.length !== height) { const existing = jumpSurviveHazardArea.slice().map((r) => r && r.slice()); jumpSurviveHazardArea = []; for (let y = 0; y < height; y++) { const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0); jumpSurviveHazardArea.push(row); } } else { for (let y = 0; y < height; y++) { if (!jumpSurviveHazardArea[y] || jumpSurviveHazardArea[y].length !== width) { jumpSurviveHazardArea[y] = Array(width).fill(0); } } } } function normalizeEditorJumpSurvivePlatformTileUrl(v) { let s = String(v || '').trim().slice(0, 500); if (!s) return ''; if (!/^https?:\/\//i.test(s) && s.charAt(0) !== '/') s = '/' + s.replace(/^\/+/, ''); return s; } function normalizeEditorJumpSurvivePlatformTilesFromTiming(data) { const legacy = normalizeEditorJumpSurvivePlatformTileUrl(data.jumpSurvivePlatformTileUrl); const arr = Array.isArray(data.jumpSurvivePlatformTiles) ? data.jumpSurvivePlatformTiles : []; const out = []; for (let i = 0; i < 3; i++) { const o = arr[i] && typeof arr[i] === 'object' ? arr[i] : {}; let url = normalizeEditorJumpSurvivePlatformTileUrl(o.url); if (i === 0 && !url && legacy) url = legacy; let ww = Number(o.w); let hh = Number(o.h); if (!Number.isFinite(ww) || ww <= 0) ww = 0; else ww = Math.max(8, Math.min(4096, Math.round(ww))); if (!Number.isFinite(hh) || hh <= 0) hh = 0; else hh = Math.max(8, Math.min(4096, Math.round(hh))); out.push({ url, w: ww, h: hh }); } return out; } function refreshEditorJumpSurvivePlatformTileFromTiming() { editorJumpSurvivePlatformTimingGen += 1; const gen = editorJumpSurvivePlatformTimingGen; editorJumpSurvivePlatformTileImgs = [null, null, null]; fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (!data || gen !== editorJumpSurvivePlatformTimingGen) return; const gtNow = gameTypeEl ? gameTypeEl.value : gameType; if (gtNow !== 'jump_survive') return; editorJumpSurvivePlatformTilesCfg = normalizeEditorJumpSurvivePlatformTilesFromTiming(data); let pending = 0; for (let i = 0; i < 3; i++) { const u = editorJumpSurvivePlatformTilesCfg[i].url; if (!u) continue; pending += 1; const idx = i; const im = new Image(); im.onload = function () { if (gen !== editorJumpSurvivePlatformTimingGen) return; editorJumpSurvivePlatformTileImgs[idx] = im; pending -= 1; if (pending <= 0) draw(); }; im.onerror = function () { if (gen !== editorJumpSurvivePlatformTimingGen) return; pending -= 1; if (pending <= 0) draw(); }; im.src = u; } if (pending === 0) draw(); }) .catch(() => {}); } 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 lobbySpawnFootprintFitsInEditor(x, y) { const fp = readCharacterFootprintInputs(); const effW = Math.max(1, Math.min(fp.cw, width - x)); const effH = Math.max(1, Math.min(fp.ch, height - y)); for (let ty = y; ty < y + effH; ty++) { for (let tx = x; tx < x + effW; tx++) { if (objects[ty] && objects[ty][tx] === 1) return false; } } return true; } 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 || !lobbySpawnFootprintFitsInEditor(x, y)) { 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() { syncEditorGauntletRunwayBgBlockVisibility(); syncStackTowerCamBlockVisibilityOnly(); 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 (drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'jumpSurvivePlatform1'; 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') { refreshEditorJumpSurvivePlatformTileFromTiming(); froggerWrap.style.display = 'none'; if (hint) { hint.innerHTML = '

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

' + editorHintBulletList([ 'กำแพง = ขอบซ้ายขวา/บน (ชนแล้วตายหรือออกนอกจอ) · โหมดวาดแพลตฟอร์ม (ฟ้าอมเขียว) = ท่อนยืนได้ — ตอนเล่นแพลตฟอร์มจะเลื่อนขึ้นในกรอด ไม่ลากทั้งฉาก', 'โซนตาย (แดง) — วาดช่องที่ตัวละครแตะแล้วตายทันที (ลาวา/glitch) · อยู่คงที่ในโลก ไม่เลื่อนไปกับแพลตฟอร์ม', 'ในเกม: กระโดดสูงขึ้น · แพลตฟอร์มเลื่อนขึ้นแล้ววนกลับมาด้านล่าง (คาบ = ความสูงแมป) · กล้องตามตัว — หลุดขอบบน/ล่างจอ = กลับจุดเกิด', 'พื้นที่สุ่มเกิด (ฟ้า) + ปุ่มตั้งจุดเกิด — แนะนำสูงหลายแถวเพื่อให้มีที่กระโดด', ]); } 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 ยิงมีดีเลย์ · บอสยิงกลับ · ลูกโป้งหมด = ตกรอบ', 'อัปโหลดรูปทาง FTP: โฟลเดอร์ /Game/public/img/MegaVirusไม่มีช่องว่าง · ชื่อ Mega Virus มักได้ 550', ]); } 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'); const shJump = gt === 'space_shooter' || gt === 'jump_survive'; if (shOpt) { shOpt.hidden = !shJump; if (!shJump && drawModeEl && drawModeEl.value === 'shooterSpawnPaint') drawModeEl.value = 'wall'; } if (shWrap) shWrap.style.display = shJump ? '' : 'none'; if (btnClrSh) btnClrSh.hidden = !shJump; 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 jPlat1 = document.getElementById('draw-mode-option-jump-platform-1'); const jPlat2 = document.getElementById('draw-mode-option-jump-platform-2'); const jPlat3 = document.getElementById('draw-mode-option-jump-platform-3'); const jHazOpt = document.getElementById('draw-mode-option-jump-hazard'); const showJumpPlat = gt === 'jump_survive'; if (jPlat1) jPlat1.hidden = !showJumpPlat; if (jPlat2) jPlat2.hidden = !showJumpPlat; if (jPlat3) jPlat3.hidden = !showJumpPlat; if (!showJumpPlat && drawModeEl && /^jumpSurvivePlatform\d$/.test(drawModeEl.value)) drawModeEl.value = 'wall'; if (jHazOpt) { jHazOpt.hidden = gt !== 'jump_survive'; if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurviveHazard') drawModeEl.value = 'wall'; } if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gt !== 'space_shooter' && gt !== 'jump_survive') 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)); jumpSurvivePlatformVariantArea = Array(height).fill(0).map(() => Array(width).fill(0)); jumpSurviveHazardArea = 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(); ensureJumpSurviveHazardArea(); } if (gtDraw === 'space_shooter') ensureShooterSpawnSlots(); if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots(); if (isEditorScrollBgDrawing()) { drawEditorScrollingBackground(canvas.width, canvas.height); } else if (isEditorGauntletRunwayBgDrawing()) { drawEditorGauntletRunwayScrollingBackground(canvas.width, canvas.height); } else if (isEditorStackTowerBgDrawing()) { drawStackTowerScrollingBackground(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) { const vHere = editorJumpSurviveVariantAtCell(x, y); const isRunContinuation = x > 0 && jumpSurvivePlatformArea[y][x - 1] === 1 && editorJumpSurviveVariantAtCell(x - 1, y) === vHere; if (!isRunContinuation) { let runLen = 1; while (x + runLen < width && jumpSurvivePlatformArea[y][x + runLen] === 1 && editorJumpSurviveVariantAtCell(x + runLen, y) === vHere) runLen += 1; const cfg = editorJumpSurvivePlatformTilesCfg[vHere - 1] || { url: '', w: 0, h: 0 }; const im = editorJumpSurvivePlatformTileImgs[vHere - 1]; const segWpx = runLen * tileSize; let dw = runLen > 1 ? segWpx : (cfg.w > 0 ? cfg.w : tileSize - 6); let dh = cfg.h > 0 ? cfg.h : tileSize - 6; if (runLen === 1) { const maxDim = tileSize * 4; if (dw > maxDim) dw = maxDim; if (dh > maxDim) dh = maxDim; } const dx = tx; const dy = ty + tileSize - dh - 2; if (im && im.complete && im.naturalWidth > 0) { ctx.drawImage(im, dx, dy, dw, dh); ctx.strokeStyle = 'rgba(160, 255, 220, 0.92)'; ctx.lineWidth = 2; ctx.strokeRect(dx, dy, dw, dh); ctx.lineWidth = 1; } else { const innerW = runLen > 1 ? segWpx - 4 : tileSize - 6; const innerH = tileSize - 6; ctx.fillStyle = 'rgba(125, 235, 200, 0.48)'; ctx.fillRect(tx + 3, ty + 3, innerW, innerH); ctx.strokeStyle = 'rgba(160, 255, 220, 0.92)'; ctx.lineWidth = 2; ctx.strokeRect(tx + 3, ty + 3, innerW, innerH); ctx.lineWidth = 1; } } } if (gtDraw === 'jump_survive' && jumpSurviveHazardArea[y] && jumpSurviveHazardArea[y][x] === 1) { ctx.fillStyle = 'rgba(220, 40, 60, 0.52)'; ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.strokeStyle = 'rgba(255, 100, 120, 0.95)'; ctx.lineWidth = 2; ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.lineWidth = 1; ctx.fillStyle = 'rgba(40, 0, 8, 0.88)'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('!', tx + tileSize / 2, ty + tileSize / 2 + 4); ctx.textAlign = 'left'; } if ((gtDraw === 'space_shooter' || gtDraw === 'jump_survive') && 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 (customizeSpot && customizeSpot.x === x && customizeSpot.y === y) { ctx.fillStyle = 'rgba(34, 211, 238, 0.42)'; ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.strokeStyle = 'rgba(120, 230, 255, 0.95)'; ctx.lineWidth = 2; ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.lineWidth = 1; ctx.fillStyle = '#eafaff'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('แต่งตัว', tx + tileSize / 2, ty + tileSize / 2 + 4); ctx.textAlign = 'left'; } if (hostConsoleSpot && hostConsoleSpot.x === x && hostConsoleSpot.y === y) { ctx.fillStyle = 'rgba(236, 72, 153, 0.42)'; ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.strokeStyle = 'rgba(255, 140, 200, 0.95)'; ctx.lineWidth = 2; ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4); ctx.lineWidth = 1; ctx.fillStyle = '#fff0f8'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('โฮสต์', 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 fpP = readCharacterFootprintInputs(); const effW = Math.max(1, Math.min(fpP.cw, width - s.x)); const effH = Math.max(1, Math.min(fpP.ch, height - s.y)); const tx0 = s.x * tileSize; const ty0 = s.y * tileSize; const rw = effW * tileSize; const rh = effH * tileSize; ctx.fillStyle = 'rgba(0, 120, 180, 0.28)'; ctx.fillRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2); ctx.strokeStyle = '#7dcfff'; ctx.lineWidth = 2; ctx.strokeRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2); const cx = tx0 + rw / 2; const cy = ty0 + rh - tileSize * 0.35; 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 === 'jumpSurvivePlatform1' || drawModeEl.value === 'jumpSurvivePlatform2' || drawModeEl.value === 'jumpSurvivePlatform3') { if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'jump_survive') return; const slot = drawModeEl.value === 'jumpSurvivePlatform1' ? 1 : drawModeEl.value === 'jumpSurvivePlatform2' ? 2 : 3; ensureJumpSurvivePlatformArea(); if (left) { jumpSurvivePlatformArea[y][x] = 1; jumpSurvivePlatformVariantArea[y][x] = slot; } else { jumpSurvivePlatformArea[y][x] = 0; jumpSurvivePlatformVariantArea[y][x] = 0; } } else if (drawModeEl.value === 'jumpSurviveHazard') { if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'jump_survive') return; ensureJumpSurviveHazardArea(); jumpSurviveHazardArea[y][x] = left ? 1 : 0; } else if (drawModeEl.value === 'shooterSpawnPaint') { const gtSh = (gameTypeEl ? gameTypeEl.value : gameType); if (gtSh !== 'space_shooter' && gtSh !== 'jump_survive') 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 = (gtSh === 'jump_survive' ? 'Jump P' : 'ยาน P') + slot + ' → ช่อง (' + x + ',' + y + ')'; } else { shooterSpawnSlots[y][x] = 0; if (statusEl) statusEl.textContent = (gtSh === 'jump_survive' ? 'ลบ Jump P' : 'ลบจุดเกิดยาน') + ' ที่ช่อง (' + 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 === 'customizeSpot') { if (objects[y][x] === 1) { if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดห้องแต่งตัวไม่ได้'; return; } if (left) { customizeSpot = { x: x, y: y }; if (statusEl) statusEl.textContent = 'จุดห้องแต่งตัว → ช่อง (' + x + ',' + y + ')'; } else { customizeSpot = null; if (statusEl) statusEl.textContent = 'ลบจุดห้องแต่งตัว'; } return; } else if (drawModeEl.value === 'hostConsoleSpot') { if (objects[y][x] === 1) { if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดตั้งค่าโฮสต์ไม่ได้'; return; } if (left) { hostConsoleSpot = { x: x, y: y }; if (statusEl) statusEl.textContent = 'จุดตั้งค่าโฮสต์ → ช่อง (' + x + ',' + y + ')'; } else { hostConsoleSpot = 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(); }); }); } })(); (function wireEditorGauntletRunwayBgUi() { const en = document.getElementById('editor-gauntlet-runway-bg-enabled'); const sp = document.getElementById('editor-gauntlet-runway-bg-speed'); if (en) { en.addEventListener('change', () => { editorGauntletRunwayBgConfig.enabled = !!en.checked; if (editorGauntletRunwayBgConfig.enabled) { applyGauntletCrownRunwayBgFromMap({ gameType: 'gauntlet', gauntletCrownRunwayBg: { enabled: true, speedPxPerSec: editorGauntletRunwayBgConfig.speedPxPerSec, runwaySpawnAlignFrac: editorGauntletRunwayBgConfig.runwaySpawnAlignFrac, startImage: editorGauntletRunwayBgConfig.startImage, loopImage2: editorGauntletRunwayBgConfig.loopImage2, loopImage3: editorGauntletRunwayBgConfig.loopImage3, loopImage4: editorGauntletRunwayBgConfig.loopImage4, finishImage: editorGauntletRunwayBgConfig.finishImage, }, }); } else { stopEditorGauntletRunwayBgRaf(); editorGauntletRunwayBgImgs = [null, null, null, null, null]; draw(); } }); } if (sp) { sp.addEventListener('change', () => { editorGauntletRunwayBgConfig.speedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(sp.value)) || 48)); }); } const afIn = document.getElementById('editor-gauntlet-runway-bg-align-frac'); if (afIn) { afIn.addEventListener('change', () => { editorGauntletRunwayBgConfig.runwaySpawnAlignFrac = Math.max(0.02, Math.min(0.95, Number(afIn.value) || 0.32)); }); } function wireFile(id, key) { const el = document.getElementById(id); if (!el) return; el.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const pickedName = file.name || ''; const r = new FileReader(); r.onload = () => { compressRunwayDataUrlIfLarge(r.result, (finalUrl) => { editorGauntletRunwayBgConfig[key] = finalUrl; editorGauntletRunwayBgFileNames[key] = pickedName; syncEditorGauntletRunwayBgThumbnails(); const s0 = editorGauntletRunwayBgConfig.startImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START; const s1 = editorGauntletRunwayBgConfig.loopImage2 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2; const s2 = editorGauntletRunwayBgConfig.loopImage3 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3; const s3 = editorGauntletRunwayBgConfig.loopImage4 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4; const s4 = editorGauntletRunwayBgConfig.finishImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH; editorGauntletRunwayBgScrollPx = 0; loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, () => { syncEditorGauntletRunwayBgUploadSummary(); syncEditorGauntletRunwayBgThumbnails(); draw(); if (editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf(); }); }); }; r.readAsDataURL(file); e.target.value = ''; }); } wireFile('editor-gauntlet-runway-bg-start-file', 'startImage'); wireFile('editor-gauntlet-runway-bg-loop2-file', 'loopImage2'); wireFile('editor-gauntlet-runway-bg-loop3-file', 'loopImage3'); wireFile('editor-gauntlet-runway-bg-loop4-file', 'loopImage4'); wireFile('editor-gauntlet-runway-bg-finish-file', 'finishImage'); const btnR = document.getElementById('editor-gauntlet-runway-bg-reset-assets'); if (btnR) { btnR.addEventListener('click', () => { editorGauntletRunwayBgConfig.startImage = null; editorGauntletRunwayBgConfig.loopImage2 = null; editorGauntletRunwayBgConfig.loopImage3 = null; editorGauntletRunwayBgConfig.loopImage4 = null; editorGauntletRunwayBgConfig.finishImage = null; editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' }; editorGauntletRunwayBgScrollPx = 0; loadEditorGauntletRunwayBgImages( EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4, EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH, () => { syncEditorGauntletRunwayBgUiFromConfig(); draw(); if (editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf(); }, ); }); } })(); syncEditorGauntletRunwayBgBlockVisibility(); syncEditorStackHudMock(); if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({}); if (isEditorGauntletRunwayBgMap()) applyGauntletCrownRunwayBgFromMap({}); if (isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({}); else stopStackTowerBgScrollRaf(); (function wireStackTowerVerticalBgUi() { const en = document.getElementById('editor-stack-tower-vbg-enabled'); const sp = document.getElementById('editor-stack-tower-vbg-speed'); if (en) { en.addEventListener('change', () => { stackTowerBgScrollConfig.enabled = !!en.checked; if (stackTowerBgScrollConfig.enabled) syncStackTowerBgScrollInitialToBottom(); draw(); }); } if (sp) { sp.addEventListener('change', () => { const n = Number(sp.value); stackTowerBgScrollConfig.speedPxPerSec = Number.isFinite(n) ? Math.max(0, Math.min(400, Math.floor(n))) : 0; }); } const dirSel = document.getElementById('editor-stack-tower-vbg-direction'); if (dirSel) { dirSel.addEventListener('change', () => { stackTowerBgScrollConfig.scrollDirection = dirSel.value === 'down' ? 'down' : 'up'; syncStackTowerBgScrollInitialToBottom(); draw(); }); } const stL = document.getElementById('editor-stack-tower-vbg-step-layers'); if (stL) { stL.addEventListener('change', () => { const n = Number(stL.value); stackTowerBgScrollConfig.stepEveryLayers = Number.isFinite(n) ? Math.max(0, Math.min(200, Math.floor(n))) : 0; }); } const stPx = document.getElementById('editor-stack-tower-vbg-step-px'); if (stPx) { stPx.addEventListener('change', () => { const n = Number(stPx.value); stackTowerBgScrollConfig.stepScrollPx = Number.isFinite(n) ? Math.max(0, Math.min(8000, Math.floor(n))) : 0; }); } const stMs = document.getElementById('editor-stack-tower-vbg-step-anim-ms'); if (stMs) { stMs.addEventListener('change', () => { const n = Number(stMs.value); stackTowerBgScrollConfig.stepAnimMs = Number.isFinite(n) ? Math.max(120, Math.min(4000, Math.floor(n))) : 520; }); } const relG = document.getElementById('editor-stack-tower-vbg-release-gap'); if (relG) { relG.addEventListener('change', () => { const s = String(relG.value).trim(); if (s === '') stackTowerBgScrollConfig.releaseGapWorldPx = null; else { const n = Number(s); stackTowerBgScrollConfig.releaseGapWorldPx = Number.isFinite(n) && n >= 0 ? Math.min(800, Math.floor(n)) : null; } }); } const introF = document.getElementById('editor-stack-tower-vbg-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 = () => { stackTowerBgScrollConfig.introImage = r.result; loadStackTowerBgScrollImagePair(stackTowerBgScrollConfig.introImage, stackTowerBgScrollConfig.loopImage || EDITOR_STACK_TOWER_BG_DEFAULT_LOOP, () => { syncStackTowerBgScrollInitialToBottom(); draw(); }); }; r.readAsDataURL(file); e.target.value = ''; }); } const loopF = document.getElementById('editor-stack-tower-vbg-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 = () => { stackTowerBgScrollConfig.loopImage = r.result; loadStackTowerBgScrollImagePair(stackTowerBgScrollConfig.introImage || EDITOR_STACK_TOWER_BG_DEFAULT_INTRO, stackTowerBgScrollConfig.loopImage, () => { syncStackTowerBgScrollInitialToBottom(); draw(); }); }; r.readAsDataURL(file); e.target.value = ''; }); } const btnR = document.getElementById('editor-stack-tower-vbg-reset-assets'); if (btnR) { btnR.addEventListener('click', () => { stackTowerBgScrollConfig.introImage = null; stackTowerBgScrollConfig.loopImage = null; loadStackTowerBgScrollImagePair(EDITOR_STACK_TOWER_BG_DEFAULT_INTRO, EDITOR_STACK_TOWER_BG_DEFAULT_LOOP, () => { syncStackTowerBgScrollUiFromConfig(); syncStackTowerBgScrollInitialToBottom(); draw(); }); }); } })(); document.getElementById('btn-new').addEventListener('click', initGrid); document.getElementById('btn-spawn').addEventListener('click', () => { isSpawnMode = true; document.getElementById('btn-spawn').textContent = 'คลิกช่องที่ต้องการ'; }); if (gameTypeEl) gameTypeEl.addEventListener('change', () => { gameType = gameTypeEl.value; syncEditorGauntletRunwayBgBlockVisibility(); syncEditorStackTowerVerticalBgBlockVisibility(); if (!isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({}); if (mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) { stopEditorGauntletRunwayBgRaf(); if (isEditorGauntletRunwayBgMap() && editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf(); draw(); } 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 && /^jumpSurvivePlatform\d$/.test(drawModeEl.value) && 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' && gameType !== 'jump_survive') 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(); syncEditorStackHudMock(); 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(); ensureJumpSurviveHazardArea(); 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: (() => { ensureLobbyPlayerSpawnsLength(); sanitizeLobbyPlayerSpawnsInEditor(); let hasP = false; for (let pi = 0; pi < 6; pi++) { if (lobbyPlayerSpawns[pi]) { hasP = true; break; } } if (hasP) return 'slots6'; 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()) : [], jumpSurvivePlatformVariantArea: gameType === 'jump_survive' ? jumpSurvivePlatformVariantArea.map((r) => r.slice()) : [], jumpSurviveHazardArea: gameType === 'jump_survive' ? jumpSurviveHazardArea.map((r) => r.slice()) : [], shooterSpawnSlots: (gameType === 'space_shooter' || gameType === 'jump_survive') ? 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, customizeSpot: customizeSpot && Number.isFinite(customizeSpot.x) ? { x: Math.floor(customizeSpot.x), y: Math.floor(customizeSpot.y) } : null, hostConsoleSpot: hostConsoleSpot && Number.isFinite(hostConsoleSpot.x) ? { x: Math.floor(hostConsoleSpot.x), y: Math.floor(hostConsoleSpot.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; const stcSave = readStackTowerCameraFollowForSave(); if (stcSave !== undefined) body.stackTowerCameraFollow = stcSave; const stBgSave = readStackTowerBgScrollForSave(); if (stBgSave !== undefined) body.stackTowerBgScroll = stBgSave; const gcrSave = readGauntletCrownRunwayBgForSave(); if (gcrSave !== undefined) body.gauntletCrownRunwayBg = gcrSave; 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'; let payloadStr; try { payloadStr = JSON.stringify(body); } catch (err) { if (statusEl) statusEl.textContent = 'แปลงฉากเป็น JSON ไม่ได้ — รูป/ข้อมูลใหญ่เกิน · Cannot stringify map JSON'; return; } const payloadBytes = (typeof TextEncoder !== 'undefined') ? new TextEncoder().encode(payloadStr).length : new Blob([payloadStr]).size; const maxSaveBytes = 78 * 1024 * 1024; if (payloadBytes > maxSaveBytes) { if (statusEl) { statusEl.textContent = 'ข้อมูลฉาก ~' + Math.round(payloadBytes / (1024 * 1024)) + 'MB ใหญ่เกิน ' + Math.round(maxSaveBytes / (1024 * 1024)) + 'MB — ลดรูปรันเวย์/คลังกริด หรือให้แอดมินเพิ่ม client_max_body_size · Map payload too large'; } return; } fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: payloadStr }) .then((r) => { if (!r.ok) { return r.text().then((t) => { const hint = r.status === 413 ? ' (413 = body ใหญ่เกิน nginx — เพิ่ม client_max_body_size ใน /Game/api/ หรือลดรูป)' : ''; throw new Error('HTTP ' + r.status + hint + (t ? ' ' + t.slice(0, 120) : '')); }); } return 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((err) => { if (statusEl) { statusEl.textContent = 'บันทึกไม่ได้' + (err && err.message ? ' — ' + err.message : '') + ' · ถ้าเป็น 413 ให้ reload nginx หลังเพิ่ม client_max_body_size'; } }); }); 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)); jumpSurvivePlatformVariantArea = m.jumpSurvivePlatformVariantArea && m.jumpSurvivePlatformVariantArea.length ? m.jumpSurvivePlatformVariantArea.map((r) => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0)); ensureJumpSurvivePlatformArea(); jumpSurviveHazardArea = m.jumpSurviveHazardArea && m.jumpSurviveHazardArea.length ? m.jumpSurviveHazardArea.map((r) => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0)); ensureJumpSurviveHazardArea(); 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; } if (m.customizeSpot && Number.isFinite(Number(m.customizeSpot.x)) && Number.isFinite(Number(m.customizeSpot.y))) { customizeSpot = { x: Math.floor(Number(m.customizeSpot.x)), y: Math.floor(Number(m.customizeSpot.y)) }; } else { customizeSpot = null; } if (m.hostConsoleSpot && Number.isFinite(Number(m.hostConsoleSpot.x)) && Number.isFinite(Number(m.hostConsoleSpot.y))) { hostConsoleSpot = { x: Math.floor(Number(m.hostConsoleSpot.x)), y: Math.floor(Number(m.hostConsoleSpot.y)) }; } else { hostConsoleSpot = 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; syncEditorGauntletRunwayBgBlockVisibility(); syncEditorStackHudMock(); applyGauntletCrownRunwayBgFromMap(m); applyStackTowerCameraFollowFromMap(m); 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(); syncEditorStackHudMock(); } })();