4126 lines
199 KiB
JavaScript
4126 lines
199 KiB
JavaScript
(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 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 '<ul class="editor-hint-bullets">' + items.map(function (html) {
|
||
return '<li>' + html + '</li>';
|
||
}).join('') + '</ul>';
|
||
}
|
||
|
||
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 = '<span class="grid-cell-image-gallery-empty">ยังไม่มีรูป — อัปโหลดด้านล่าง</span>';
|
||
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 = '<thead><tr><th>แถว (y)</th><th>ประเภท</th><th>ความเร็ว</th><th>ทิศทาง</th><th>ระยะห่าง</th></tr></thead><tbody></tbody>';
|
||
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 =
|
||
'<td>' + lane.y + '</td>' +
|
||
'<td><select class="lane-type" data-y="' + lane.y + '">' +
|
||
'<option value="spawn"' + (lane.type === 'spawn' ? ' selected' : '') + '>จุดเกิด</option>' +
|
||
'<option value="safe"' + (lane.type === 'safe' ? ' selected' : '') + '>ปลอดภัย</option>' +
|
||
'<option value="road"' + (lane.type === 'road' ? ' selected' : '') + '>ถนน (รถ)</option>' +
|
||
'<option value="water"' + (lane.type === 'water' ? ' selected' : '') + '>น้ำ (ท่อนซุง)</option>' +
|
||
'<option value="goal"' + (lane.type === 'goal' ? ' selected' : '') + '>ปลายทาง</option>' +
|
||
'</select></td>' +
|
||
'<td><input type="number" class="lane-speed" data-y="' + lane.y + '" min="0.5" max="5" step="0.5" value="' + (lane.speed != null ? lane.speed : 1) + '" ' + (isRoadOrWater ? '' : ' disabled') + '></td>' +
|
||
'<td><select class="lane-dir" data-y="' + lane.y + '" ' + (isRoadOrWater ? '' : ' disabled') + '>' +
|
||
'<option value="1"' + (lane.dir === 1 ? ' selected' : '') + '>ขวา</option>' +
|
||
'<option value="-1"' + (lane.dir === -1 ? ' selected' : '') + '>ซ้าย</option>' +
|
||
'</select></td>' +
|
||
'<td><input type="number" class="lane-spacing" data-y="' + lane.y + '" min="1.5" max="8" step="0.5" value="' + spacingVal + '" title="ระยะห่างรถ/ท่อนซุง (ยิ่งมากยิ่งห่าง)" ' + (isRoadOrWater ? '' : ' disabled') + '></td>';
|
||
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) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ <strong>ระยะห่าง</strong> (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)';
|
||
if (hint) {
|
||
hint.innerHTML = editorHintBulletList([
|
||
'แถวบน (y=0) = <strong>ปลายทาง</strong>',
|
||
'แถวล่าง = <strong>จุดเกิด</strong>',
|
||
'ตั้งถนน/น้ำด้านล่าง — ความเร็ว ทิศทาง ระยะห่างรถหรือท่อนซุง',
|
||
]);
|
||
}
|
||
ensureLanes();
|
||
renderLanesTable();
|
||
draw();
|
||
if (editorFroggerAnimationId != null) cancelAnimationFrame(editorFroggerAnimationId);
|
||
editorFroggerAnimationId = requestAnimationFrame(editorFroggerTick);
|
||
} else if (gt === 'gauntlet') {
|
||
froggerWrap.style.display = 'none';
|
||
if (legEl) legEl.innerHTML = '<strong>พรมแดงสุดท้าย:</strong> ลำดับเกิดกำหนดแค่ <strong>แถว (y)</strong> — ในเกมทุกคนยืน <strong>คอลัมน์ x เดียวกัน</strong> (ซ้ายสุดจากจุดที่วาด) ไม่เฉียง';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead">โหมด <strong>พรมแดงสุดท้าย</strong></p>' + editorHintBulletList([
|
||
'อุปสรรคเลนเฉพาะ<strong>แถวที่มีคน</strong> + เลเซอร์ทุกแถว · ชนแล้วถอยซ้าย',
|
||
'โหมดวาด <strong>เลเซอร์: แถวต้น / แถวปลาย</strong> — กำหนดความสูงแถบเลเซอร์บนแมป (ไม่ต้องเปลี่ยนรูปจาก Admin)',
|
||
'ลำดับเกิด <strong>1–6</strong> หรือช่องฟ้า',
|
||
'กระโดด: <kbd>Space</kbd> · <kbd>W</kbd> · <kbd>↑</kbd>',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'lobby') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = editorHintBulletList([
|
||
'วาดฉาก<strong>ห้องโถง</strong> — รอผู้เล่นและตกแต่งฉาก',
|
||
'<strong>พื้นที่เริ่มเกม (ส้ม)</strong> — host ยืนในโซนนี้ก่อนกดเริ่ม',
|
||
'<strong>พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน)</strong> — ผู้เล่นใหม่สุ่มเกิดในช่องที่วาด',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'quiz') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = editorHintBulletList([
|
||
'<strong>โซนถูก (ฟ้า)</strong> · <strong>โซนผิด (ชมพู)</strong> · <strong>พื้นที่คำถาม (ทอง)</strong>',
|
||
'<strong>พื้นที่สุ่มจุดเกิด (ฟ้าอ่อน)</strong> — ผู้เล่นเข้าห้องสุ่มยืนในโซน',
|
||
'คำถามและเวลา — ตั้งที่ <strong>Admin → คำถามเกม</strong>',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'stack') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead"><strong>Stack</strong> — สลับจังหวะลงตึก</p>' + editorHintBulletList([
|
||
'<strong>จุดปล่อย (ฟ้า)</strong> — แนวสวิง crane ด้านบน',
|
||
'<strong>จุดซ้อนตึก (ชมพู)</strong> — แพลตฟอร์มรองรับบล็อก',
|
||
'ในเกมไม่มีเดิน — กด <kbd>Space</kbd> เพื่อปล่อยบล็อก',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'quiz_carry') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead"><strong>หยิบมาวาง</strong> — หยิบตัวเลือกแล้วไปส่งที่โซน interactive (เขียว)</p>' + editorHintBulletList([
|
||
'<strong>โซนกลาง (ม่วง)</strong> — กำแพง · ถ้าไม่วาดโซนทองด้านล่าง ข้อความคำถามจะโชว์บนโซนม่วงในเกม',
|
||
'<strong>พื้นที่โชว์ข้อความคำถาม (ทอง)</strong> — วาดโซนทองแยกได้ · ในเกมข้อความจะอยู่ตรงโซนที่วาด',
|
||
'<strong>โซน interactive (เขียว)</strong> — ยืนบนหรือชิดขอบแล้วกด <kbd>F</kbd> ส่งคำตอบ (วาดอย่างน้อย 1 ช่อง — ถ้าไม่วาดจะใช้ชิดโซนกลางแทน)',
|
||
'<strong>ตัวเลือก 1–16</strong> — วาดบนกริดให้ตรงลำดับ <code>choices</code> (Admin / แมป)',
|
||
'พรีวิวเอดิเตอร์: ช่วงนับ <strong>3-2-1</strong> — เลือกตำแหน่ง (กลางจอ / บนแมปอัตโนมัติที่โซนทองหรือโซนกลาง / <strong>ตามกริด</strong> โหมดวาด «ตำแหน่งเลข 3-2-1») · ไฮไลต์ข้อถูกด้านบน · ต่อข้อ <code>countdownHighlightSlot</code> (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 = '<p class="editor-hint-lead"><strong>กระโดดให้รอด</strong> — กำแพง + แพลตฟอร์มเลื่อนขึ้นในกรอบจอ</p>' + editorHintBulletList([
|
||
'<strong>กำแพง</strong> = ขอบซ้ายขวา/บน (ชนแล้วตายหรือออกนอกจอ) · <strong>โหมดวาดแพลตฟอร์ม</strong> (ฟ้าอมเขียว) = ท่อนยืนได้ — ตอนเล่นแพลตฟอร์มจะเลื่อนขึ้นในกรอด ไม่ลากทั้งฉาก',
|
||
'<strong>โซนตาย (แดง)</strong> — วาดช่องที่ตัวละคร<strong>แตะแล้วตายทันที</strong> (ลาวา/glitch) · อยู่คงที่ในโลก ไม่เลื่อนไปกับแพลตฟอร์ม',
|
||
'ในเกม: กระโดดสูงขึ้น · แพลตฟอร์มเลื่อนขึ้นแล้ว<strong>วนกลับมาด้านล่าง</strong> (คาบ = ความสูงแมป) · กล้องตามตัว — หลุดขอบบน/ล่างจอ = กลับจุดเกิด',
|
||
'พื้นที่สุ่มเกิด (ฟ้า) + ปุ่มตั้งจุดเกิด — แนะนำสูงหลายแถวเพื่อให้มีที่กระโดด',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'space_shooter') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead"><strong>ยิงยานอวกาศ</strong> — หินตก · ผู้เล่น/บอทยิงขึ้น · สูงสุด 6 คน</p>' + editorHintBulletList([
|
||
'เลือกโหมดวาด <strong>จุดเกิดยาน 1–6</strong> แล้วเลือกหมายเลข P ด้านขวา — คลิกซ้ายวาง · คลิกขวาลบช่อง · ช่องเดียวต่อหมายเลข (วางซ้ำจะย้ายจุด)',
|
||
'ลำดับผู้เล่นในเกม: <strong>คุณก่อน</strong> แล้วตามรหัสผู้เล่นอื่น แล้วบอททดสอบ — ตรงกับ P1, P2, …',
|
||
'แนะนำ <strong>รูปพื้นหลัง</strong> อวกาศ + ปิดโชว์กริดในเกม — เล่น: 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 = '<p class="editor-hint-lead"><strong>ลูกโป้งยิงบอส (Mega Virus)</strong> — สูงสุด 6 คน · บอสกลางจอ</p>' + editorHintBulletList([
|
||
'โหมด <strong>จุดเกิดผู้เล่น 1–6</strong> + เลข P ด้านขวา — คลิกซ้ายวาง · ขวาลบ (เหมือนยิงยาน)',
|
||
'โหมด <strong>จุดเกิดบอส</strong> — คลิกช่อง <strong>หนึ่งช่อง</strong> เป็นตำแหน่งศูนย์กลางบอส (กะโหลก + โล่หกเหลี่ยม)',
|
||
'ในเกม: คุมแบบ<strong>ยานมีแรงเฉื่อย</strong> (กดทิศแล้วค่อยเร่ง ปล่อยแล้วยังไหล มีแรงหน่วง) — ไม่สแนปทันที · Space ยิงมี<strong>ดีเลย์</strong> · บอสยิงกลับ · ลูกโป้งหมด = ตกรอบ',
|
||
'อัปโหลดรูปทาง FTP: โฟลเดอร์ <code>/Game/public/img/MegaVirus</code> — <strong>ไม่มีช่องว่าง</strong> · ชื่อ <code>Mega Virus</code> มักได้ <strong>550</strong>',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else if (gt === 'quiz_battle') {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead"><strong>Quiz Battle</strong> — เส้นทาง + โดมถาม A / B / C</p>' + editorHintBulletList([
|
||
'โหมด <strong>เส้นทางเดิน (ม่วง)</strong> — วาดซิกแซกเหมือนทางในเกม · ถ้ามีอย่างน้อย 1 ช่อง ผู้เล่น<strong>เดินได้เฉพาะบนเส้นทาง</strong> · ไม่วาดเลย = เดินอิสระทั้งแมป',
|
||
'โหมด <strong>โดมคำถาม</strong> — วางบนเส้นทาง (หรือทั่วแมปถ้าไม่ใช้เส้นทาง) · ในเกมกด <kbd>E</kbd>',
|
||
'รูป <strong>พื้นหลัง</strong> เปลี่ยนทีหลังได้ — ตรรกะเส้นทาง/โดมอยู่ที่กริด ไม่ผูกกราฟิก',
|
||
'ข้อสอบจาก <strong>Admin → Quiz Battle</strong> (<code>battleQuizMcq</code>) · แนะนำ <strong>พื้นที่สุ่มจุดเกิด</strong> ให้อยู่บนเส้นทางหรือใกล้จุดเริ่ม',
|
||
]);
|
||
}
|
||
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
||
draw();
|
||
} else {
|
||
froggerWrap.style.display = 'none';
|
||
if (hint) {
|
||
hint.innerHTML = '<p class="editor-hint-lead">เลือกโหมดด้านบน — แต่ละโหมดวาดคนละแบบ</p>' + editorHintBulletList([
|
||
'<strong>ZEP</strong> — เดินอิสระบนแผนที่',
|
||
'<strong>กบข้ามถนน</strong> — ตารางแถว / ถนน น้ำ',
|
||
'<strong>พรมแดงสุดท้าย</strong> — กระโดดหลบ (ไม่ใช่ตารางแถวแบบกบ)',
|
||
'<strong>Stack</strong> — จุดปล่อย (ฟ้า) + จุดซ้อน (ชมพู)',
|
||
'<strong>กระโดดให้รอด</strong> — แพลตฟอร์มรอดชีวิต',
|
||
'<strong>ยิงยานอวกาศ</strong> — หินตก · จุดเกิด P1–P6',
|
||
'<strong>ลูกโป้งยิงบอส</strong> — จุดเกิดผู้เล่น + บอส · ดีเลย์ยิง · ลูกโป้ง = ชีวิต',
|
||
'<strong>Quiz Battle</strong> — โดม + กด <kbd>E</kbd> ตอบ A B C',
|
||
'<strong>ห้องโถง</strong> — พื้นที่เริ่มเกม (ส้ม) · <strong>ตอบคำถาม</strong> — โซนถูก–ผิด + คำถาม',
|
||
'<strong>หยิบมาวาง</strong> — โซนกลาง + ตัวเลือก 1–16 · <strong>ฟ้าอ่อน</strong> = สุ่มจุดเกิด',
|
||
]);
|
||
}
|
||
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 (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 === '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 = '<option value="">— เลือกฉากเดิม —</option>';
|
||
(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 = '<option value="">— เลือกฉากเดิม —</option>'; });
|
||
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,
|
||
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;
|
||
}
|
||
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(); }
|
||
})();
|