851 lines
43 KiB
JavaScript
851 lines
43 KiB
JavaScript
(function () {
|
|
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
|
|
const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/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 characterCellsEl = document.getElementById('character-cells');
|
|
const drawModeEl = document.getElementById('draw-mode');
|
|
const gameTypeEl = document.getElementById('game-type');
|
|
const statusEl = document.getElementById('editor-status');
|
|
|
|
let tileSize = 32, width = 20, height = 15, characterCells = 1;
|
|
let gameType = 'zep';
|
|
let lanes = [];
|
|
let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 };
|
|
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [];
|
|
let cellColors = [];
|
|
let gauntletPlayerSpawns = [];
|
|
let showMapInGame = true;
|
|
let backgroundImage = null;
|
|
let backgroundImageImg = null;
|
|
let isSpawnMode = false;
|
|
let editorFroggerAnimationId = null;
|
|
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
function sanitizeGauntletPlayerSpawns() {
|
|
gauntletPlayerSpawns = gauntletPlayerSpawns
|
|
.filter((s) => s && Number.isFinite(s.x) && Number.isFinite(s.y) &&
|
|
s.x >= 0 && s.x < width && s.y >= 0 && s.y < height &&
|
|
!(objects[s.y] && objects[s.y][s.x] === 1))
|
|
.map((s) => ({ x: Math.floor(s.x), y: Math.floor(s.y) }))
|
|
.slice(0, 6);
|
|
}
|
|
|
|
function ensureSpawnArea() {
|
|
if (!spawnArea.length || spawnArea.length !== height) {
|
|
const existing = spawnArea.slice().map(r => r && r.slice());
|
|
spawnArea = [];
|
|
for (let y = 0; y < height; y++) {
|
|
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
|
|
spawnArea.push(row);
|
|
}
|
|
} else {
|
|
for (let y = 0; y < height; y++) {
|
|
if (!spawnArea[y] || spawnArea[y].length !== width) spawnArea[y] = Array(width).fill(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildDefaultLanes() {
|
|
const arr = [];
|
|
for (let y = 0; y < height; y++) {
|
|
if (y === 0) arr.push({ y: 0, type: 'goal' });
|
|
else if (y === height - 1) arr.push({ y: height - 1, type: 'spawn' });
|
|
else arr.push({ y, type: 'safe', speed: 1, dir: 1 });
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function ensureLanes() {
|
|
if (lanes.length !== height) {
|
|
const existing = lanes.slice();
|
|
lanes = [];
|
|
for (let y = 0; y < height; y++) {
|
|
const old = existing.find(l => l.y === y);
|
|
if (old) lanes.push({ ...old, y });
|
|
else lanes.push(y === 0 ? { y: 0, type: 'goal' } : y === height - 1 ? { y: height - 1, type: 'spawn' } : { y, type: 'safe', speed: 1, dir: 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
function getVehiclePositionsEditor(lane, timeMs) {
|
|
if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return [];
|
|
const speed = (lane.speed != null ? lane.speed : 1) * 0.05;
|
|
const dir = lane.dir === -1 ? -1 : 1;
|
|
const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5);
|
|
const period = width + 2;
|
|
const count = Math.max(2, Math.floor(period / spacing));
|
|
const positions = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const p = i * spacing + (timeMs * speed * dir) / 60;
|
|
const pNorm = ((p % period) + period) % period;
|
|
const vx = pNorm - 1;
|
|
positions.push(Math.max(-1, Math.min(width, vx)));
|
|
}
|
|
return positions;
|
|
}
|
|
|
|
function renderLanesTable() {
|
|
const wrap = document.getElementById('frogger-lanes-table');
|
|
const froggerWrap = document.getElementById('frogger-lanes-wrap');
|
|
if (!wrap || !froggerWrap) return;
|
|
ensureLanes();
|
|
wrap.innerHTML = '';
|
|
const table = document.createElement('table');
|
|
table.innerHTML = '<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() {
|
|
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');
|
|
if (!froggerWrap) return;
|
|
const gt = gameTypeEl ? gameTypeEl.value : 'zep';
|
|
if (dqTrue) dqTrue.hidden = gt !== 'quiz';
|
|
if (dqFalse) dqFalse.hidden = gt !== 'quiz';
|
|
if (dqQ) dqQ.hidden = gt !== 'quiz';
|
|
if (gt === 'frogger') {
|
|
froggerWrap.style.display = 'block';
|
|
if (legEl) legEl.innerHTML = 'แถวบน (y=0) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ <strong>ระยะห่าง</strong> (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)';
|
|
if (hint) { hint.style.display = 'inline'; hint.innerHTML = 'แถวบน = ปลายทาง · แถวล่าง = จุดเกิด · ตั้งถนน/น้ำด้านล่าง'; }
|
|
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.style.display = 'inline';
|
|
hint.innerHTML = 'โหมด <strong>พรมแดง</strong> — อุปสรรคเลนเฉพาะ<strong>แถวที่มีคน</strong> + เลเซอร์ทุกแถว · ชนถอยซ้าย · ลำดับเกิด 1–6 / ช่องฟ้า · <strong>Space / W / ↑</strong> กระโดด';
|
|
}
|
|
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
|
draw();
|
|
} else if (gt === 'lobby') {
|
|
froggerWrap.style.display = 'none';
|
|
if (hint) { hint.style.display = 'inline'; hint.innerHTML = 'วาด<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.style.display = 'inline';
|
|
hint.innerHTML = 'วาด<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.style.display = 'inline';
|
|
hint.innerHTML = '<strong>Stack</strong> — กดสลับจังหวะให้บล็อกลงซ้อน · ไม่ใช้แถวกบ · วาดกำแพง/โซน + รูปพื้นหลัง · เล่นจริงยังทำต่อในเครื่องเล่น';
|
|
}
|
|
if (editorFroggerAnimationId != null) { cancelAnimationFrame(editorFroggerAnimationId); editorFroggerAnimationId = null; }
|
|
draw();
|
|
} else {
|
|
froggerWrap.style.display = 'none';
|
|
if (hint) { hint.style.display = 'inline'; hint.innerHTML = '<strong>กบข้ามถนน</strong> = ตารางแถว · <strong>พรมแดงสุดท้าย</strong> = ไม่มีตารางแถว · <strong>สลับลงตึก</strong> = Stack · <strong>ห้องโถง</strong> = ฉากรอผู้เล่น · <strong>ตอบคำถาม</strong> = โซนถูก/ผิด + คำถาม · <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';
|
|
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse' || drawModeEl.value === 'quizQuestion') && gt !== 'quiz') {
|
|
drawModeEl.value = 'wall';
|
|
}
|
|
}
|
|
|
|
function initGrid() {
|
|
width = Math.max(10, Math.min(50, parseInt(mapW.value, 10) || 20));
|
|
height = Math.max(10, Math.min(40, parseInt(mapH.value, 10) || 15));
|
|
tileSize = Math.max(16, Math.min(64, parseInt(tileSizeEl.value, 10) || 32));
|
|
ground = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
objects = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
blockPlayer = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
interactive = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
startGameArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
spawnArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
quizTrueArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
quizFalseArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
quizQuestionArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
|
cellColors = Array(height).fill(0).map(() => Array(width).fill(null));
|
|
for (let i = 0; i < width; i++) { objects[0][i] = 1; objects[height - 1][i] = 1; }
|
|
for (let i = 0; i < height; i++) { objects[i][0] = 1; objects[i][width - 1] = 1; }
|
|
spawn = { x: 1, y: 1 };
|
|
if (gameType === 'frogger') { lanes = buildDefaultLanes(); ensureLanes(); }
|
|
if (gameType === 'gauntlet' || gameType === 'stack') lanes = [];
|
|
gauntletPlayerSpawns = [];
|
|
characterCells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : 1;
|
|
mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
|
|
if (characterCellsEl) characterCellsEl.value = characterCells;
|
|
resize(); draw();
|
|
toggleFroggerUI();
|
|
}
|
|
|
|
function resize() {
|
|
canvas.width = width * tileSize;
|
|
canvas.height = height * tileSize;
|
|
sanitizeGauntletPlayerSpawns();
|
|
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 usesLanes = isFrogger;
|
|
if (usesLanes) ensureLanes();
|
|
ensureInteractive();
|
|
ensureStartGameArea();
|
|
ensureSpawnArea();
|
|
if (gtDraw === 'quiz') ensureQuizAreas();
|
|
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)';
|
|
}
|
|
for (let x = 0; x < width; x++) {
|
|
const tx = x * tileSize, ty = y * tileSize;
|
|
if (objects[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(65,72,104,0.9)';
|
|
ctx.fillRect(tx, ty, tileSize, tileSize);
|
|
ctx.strokeStyle = '#565f89';
|
|
ctx.strokeRect(tx, ty, tileSize, tileSize);
|
|
} else {
|
|
const cellColor = cellColors[y] && cellColors[y][x];
|
|
if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(tx, ty, tileSize, tileSize); }
|
|
else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(tx, ty, tileSize, tileSize); }
|
|
else if (!backgroundImageImg || !backgroundImageImg.complete) {
|
|
ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335';
|
|
ctx.fillRect(tx, ty, tileSize, tileSize);
|
|
}
|
|
}
|
|
if (blockPlayer[y] && blockPlayer[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(255,180,80,0.5)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(255,180,80,0.9)';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
}
|
|
if (interactive[y] && interactive[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(158,206,106,0.45)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = '#9ece6a';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
}
|
|
if (startGameArea[y] && startGameArea[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(255, 158, 100, 0.5)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(255, 120, 60, 0.95)';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.lineWidth = 1;
|
|
}
|
|
if (spawnArea[y] && spawnArea[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(122, 162, 247, 0.38)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(122, 162, 247, 0.92)';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.lineWidth = 1;
|
|
}
|
|
if (gtDraw === 'quiz') {
|
|
if (quizQuestionArea[y] && quizQuestionArea[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(255, 214, 102, 0.42)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(224, 185, 70, 0.95)';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
}
|
|
if (quizTrueArea[y] && quizTrueArea[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(86, 202, 255, 0.5)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(122, 220, 255, 0.95)';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
}
|
|
if (quizFalseArea[y] && quizFalseArea[y][x] === 1) {
|
|
ctx.fillStyle = 'rgba(247, 118, 190, 0.5)';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
ctx.strokeStyle = 'rgba(255, 130, 200, 0.95)';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
|
}
|
|
}
|
|
if (spawn.x === x && spawn.y === y) {
|
|
const cells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : characterCells;
|
|
const cx = tx + tileSize / 2;
|
|
const cy = ty + tileSize / 2;
|
|
const size = tileSize * cells;
|
|
ctx.fillStyle = 'rgba(158,206,106,0.35)';
|
|
ctx.fillRect(cx - size / 2, cy - size / 2, size, size);
|
|
ctx.strokeStyle = '#9ece6a';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(cx - size / 2, cy - size / 2, size, size);
|
|
ctx.fillStyle = 'rgba(158,206,106,0.7)';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, Math.min(tileSize / 3, size / 4), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#9ece6a';
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
if (isFrogger && lanes.length) {
|
|
const timeMs = Date.now();
|
|
for (let y = 0; y < height; y++) {
|
|
const lane = lanes.find(l => l.y === y);
|
|
if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue;
|
|
const positions = getVehiclePositionsEditor(lane, timeMs);
|
|
const isRoad = lane.type === 'road';
|
|
for (let i = 0; i < positions.length; i++) {
|
|
const vx = positions[i];
|
|
const tx = vx * tileSize, ty = y * tileSize;
|
|
ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355';
|
|
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, (tileSize - 4) * 0.7);
|
|
ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a';
|
|
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, (tileSize - 4) * 0.7);
|
|
}
|
|
}
|
|
ctx.fillStyle = '#7aa2f7';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('โหมดกบข้ามถนน (จำลองรถ/ท่อนซุง)', 8, 20);
|
|
}
|
|
if (isGauntlet) {
|
|
ctx.fillStyle = '#f7768e';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(gauntletPlayerSpawns.length
|
|
? 'พรมแดง — ลำดับ 1–6 จากโหมด «ลำดับเกิด» (คลิกซ้ายตามลำดับ)'
|
|
: 'พรมแดง — ช่องฟ้า = ลำดับ 1→6 อัตโนมัติ (y บน→ล่าง) หรือใช้โหมดลำดับเกิดกำหนดเอง', 8, 20);
|
|
const drawGNum = (s, num) => {
|
|
const cx = s.x * tileSize + tileSize / 2;
|
|
const cy = s.y * tileSize + tileSize / 2 + 4;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy - 3, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(String(num), cx, cy + 2);
|
|
ctx.textAlign = 'left';
|
|
};
|
|
if (gauntletPlayerSpawns.length) {
|
|
for (let i = 0; i < gauntletPlayerSpawns.length; i++) {
|
|
const s = gauntletPlayerSpawns[i];
|
|
if (!s || s.x < 0 || s.x >= width || s.y < 0 || s.y >= height) continue;
|
|
if (objects[s.y][s.x] === 1) continue;
|
|
drawGNum(s, i + 1);
|
|
}
|
|
} else {
|
|
ensureSpawnArea();
|
|
const gsSlots = [];
|
|
for (let yy = 0; yy < height; yy++) {
|
|
for (let xx = 0; xx < width; xx++) {
|
|
if (spawnArea[yy] && spawnArea[yy][xx] === 1 && objects[yy][xx] !== 1) gsSlots.push({ x: xx, y: yy });
|
|
}
|
|
}
|
|
gsSlots.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
for (let i = 0; i < Math.min(6, gsSlots.length); i++) drawGNum(gsSlots[i], i + 1);
|
|
}
|
|
}
|
|
if (gtDraw === 'lobby') {
|
|
ctx.fillStyle = '#bb9af7';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('โหมดห้องโถง (ฉากรอผู้เล่น)', 8, 20);
|
|
}
|
|
if (gtDraw === 'quiz') {
|
|
ctx.fillStyle = '#f7768e';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('โหมดตอบคำถาม (ถูก/ผิด)', 8, 20);
|
|
}
|
|
if (isStack) {
|
|
ctx.fillStyle = '#7dcfff';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('โหมดสลับจังหวะลงตึก (Stack) — backdrop ฉาก · เกมซ้อนบล็อกยังไม่เปิดใน play', 8, 20);
|
|
}
|
|
const cells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : characterCells;
|
|
ctx.fillStyle = '#c0caf5';
|
|
ctx.font = '12px sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('ขนาดตัวละคร: ' + cells + ' ช่อง', 8, canvas.height - 8);
|
|
}
|
|
|
|
function editorFroggerTick() {
|
|
const gt = gameTypeEl ? gameTypeEl.value : '';
|
|
if (gt !== 'frogger') return;
|
|
draw();
|
|
editorFroggerAnimationId = requestAnimationFrame(editorFroggerTick);
|
|
}
|
|
|
|
function getCellColorWithAlpha() {
|
|
const colorEl = document.getElementById('cell-color');
|
|
const alphaEl = document.getElementById('cell-alpha');
|
|
if (!colorEl) return null;
|
|
const hex = colorEl.value;
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
const a = alphaEl ? parseInt(alphaEl.value, 10) / 100 : 1;
|
|
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
|
}
|
|
|
|
function setCell(x, y, left) {
|
|
if (drawModeEl.value === 'gauntletPlayerSpawn') {
|
|
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
|
if (gt !== 'gauntlet') return;
|
|
if (objects[y][x] === 1) {
|
|
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดไม่ได้';
|
|
return;
|
|
}
|
|
if (left) {
|
|
if (gauntletPlayerSpawns.some((s) => s.x === x && s.y === y)) return;
|
|
if (gauntletPlayerSpawns.length >= 6) {
|
|
if (statusEl) statusEl.textContent = 'ครบ 6 จุดแล้ว — คลิกขวาที่ช่องเพื่อถอด หรือปุ่มล้างลำดับ';
|
|
return;
|
|
}
|
|
gauntletPlayerSpawns.push({ x, y });
|
|
if (statusEl) statusEl.textContent = 'พรมแดง: ผู้เล่น ' + gauntletPlayerSpawns.length + ' = ช่อง (' + x + ',' + y + ')';
|
|
} else {
|
|
const before = gauntletPlayerSpawns.length;
|
|
gauntletPlayerSpawns = gauntletPlayerSpawns.filter((s) => !(s.x === x && s.y === y));
|
|
if (statusEl) {
|
|
statusEl.textContent = before > gauntletPlayerSpawns.length ? 'ถอดจุดเกิดที่ช่องนี้แล้ว' : 'ไม่มีจุดเกิดที่ช่องนี้';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (drawModeEl.value === 'wall') {
|
|
objects[y][x] = left ? 1 : 0;
|
|
} else if (drawModeEl.value === 'interactive') {
|
|
if (!interactive[y]) interactive[y] = Array(width).fill(0);
|
|
interactive[y][x] = left ? 1 : 0;
|
|
} else if (drawModeEl.value === 'spawnArea') {
|
|
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
|
if (gt === 'frogger') return;
|
|
ensureSpawnArea();
|
|
spawnArea[y][x] = left ? 1 : 0;
|
|
} else if (drawModeEl.value === 'startGame') {
|
|
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
|
if (gt !== 'lobby') return;
|
|
ensureStartGameArea();
|
|
startGameArea[y][x] = left ? 1 : 0;
|
|
} else if (drawModeEl.value === 'quizTrue') {
|
|
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
|
|
ensureQuizAreas();
|
|
quizTrueArea[y][x] = left ? 1 : 0;
|
|
if (left) quizFalseArea[y][x] = 0;
|
|
} else if (drawModeEl.value === 'quizFalse') {
|
|
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
|
|
ensureQuizAreas();
|
|
quizFalseArea[y][x] = left ? 1 : 0;
|
|
if (left) quizTrueArea[y][x] = 0;
|
|
} else if (drawModeEl.value === 'quizQuestion') {
|
|
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
|
|
ensureQuizAreas();
|
|
quizQuestionArea[y][x] = left ? 1 : 0;
|
|
} else if (drawModeEl.value === 'color') {
|
|
if (!cellColors[y]) cellColors[y] = Array(width).fill(null);
|
|
cellColors[y][x] = left ? getCellColorWithAlpha() : null;
|
|
} else {
|
|
if (!blockPlayer[y]) blockPlayer[y] = Array(width).fill(0);
|
|
blockPlayer[y][x] = left ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
const { x, y } = getCell(e);
|
|
if (isSpawnMode) {
|
|
if (objects[y][x] === 0) { spawn = { x, y }; draw(); statusEl.textContent = 'จุดเกิด: ' + x + ',' + y; }
|
|
isSpawnMode = false;
|
|
document.getElementById('btn-spawn').textContent = 'ตั้งจุดเกิด';
|
|
return;
|
|
}
|
|
setCell(x, y, e.button === 0);
|
|
draw();
|
|
});
|
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
if (e.buttons !== 1 && e.buttons !== 2) return;
|
|
const { x, y } = getCell(e);
|
|
setCell(x, y, e.buttons === 1);
|
|
draw();
|
|
});
|
|
|
|
if (mapLoadEl) {
|
|
fetch(SERVER + '/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();
|
|
});
|
|
|
|
document.getElementById('btn-new').addEventListener('click', initGrid);
|
|
document.getElementById('btn-spawn').addEventListener('click', () => {
|
|
isSpawnMode = true;
|
|
document.getElementById('btn-spawn').textContent = 'คลิกช่องที่ต้องการ';
|
|
});
|
|
if (gameTypeEl) gameTypeEl.addEventListener('change', () => {
|
|
gameType = gameTypeEl.value;
|
|
if (drawModeEl && drawModeEl.value === 'startGame' && gameType !== 'lobby') drawModeEl.value = 'wall';
|
|
if (drawModeEl && drawModeEl.value === 'spawnArea' && gameType === 'frogger') drawModeEl.value = 'wall';
|
|
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse' || drawModeEl.value === 'quizQuestion') && gameType !== 'quiz') drawModeEl.value = 'wall';
|
|
if (drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
|
|
toggleFroggerUI();
|
|
});
|
|
|
|
const btnClearGauntletSpawns = document.getElementById('btn-clear-gauntlet-spawns');
|
|
if (btnClearGauntletSpawns) {
|
|
btnClearGauntletSpawns.addEventListener('click', () => {
|
|
gauntletPlayerSpawns = [];
|
|
if (statusEl) statusEl.textContent = 'ล้างลำดับเกิดพรมแดง (1–6) แล้ว';
|
|
draw();
|
|
});
|
|
}
|
|
|
|
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') ? 6 : 4;
|
|
fetch(SERVER + '/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 q = 'play.html?space=' + encodeURIComponent(sid)
|
|
+ '&nick=' + encodeURIComponent(nick)
|
|
+ '&map=' + encodeURIComponent(id)
|
|
+ '&preview=1&defaultChar=1'
|
|
+ (editorEmbed ? '&editorEmbed=1' : '');
|
|
if (statusEl) statusEl.textContent = 'เปิดหน้าทดสอบเล่นในแท็บใหม่แล้ว — ปิดแท็บเมื่อเล่นเสร็จ';
|
|
window.open(BASE + '/' + q, '_blank', 'noopener,noreferrer');
|
|
})
|
|
.catch(function () {
|
|
if (statusEl) statusEl.textContent = 'สร้างห้องทดสอบไม่ได้';
|
|
});
|
|
}
|
|
|
|
try { window.gameEditorOpenPlayTest = openPlayTestMap; } catch (e) { /* ignore */ }
|
|
|
|
var btnPlayTest = document.getElementById('btn-play-test');
|
|
if (btnPlayTest) btnPlayTest.addEventListener('click', openPlayTestMap);
|
|
|
|
document.getElementById('btn-save').addEventListener('click', () => {
|
|
const name = (mapName.value || '').trim() || 'ฉากใหม่';
|
|
gameType = gameTypeEl ? (gameTypeEl.value || 'zep') : 'zep';
|
|
if (gameType === 'frogger') ensureLanes();
|
|
ensureInteractive();
|
|
ensureStartGameArea();
|
|
ensureSpawnArea();
|
|
ensureQuizAreas();
|
|
characterCells = characterCellsEl ? Math.max(1, Math.min(4, parseInt(characterCellsEl.value, 10) || 1)) : 1;
|
|
const showMapInGameEl = document.getElementById('show-map-in-game');
|
|
showMapInGame = showMapInGameEl ? showMapInGameEl.checked : true;
|
|
const body = {
|
|
name, gameType, width, height, tileSize, characterCells, ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
|
|
quizTrueArea, quizFalseArea, quizQuestionArea,
|
|
};
|
|
if (backgroundImage) body.backgroundImage = backgroundImage;
|
|
body.lanes = gameType === 'frogger' ? lanes : [];
|
|
sanitizeGauntletPlayerSpawns();
|
|
body.gauntletPlayerSpawns = gameType === 'gauntlet'
|
|
? gauntletPlayerSpawns.map((s) => ({ x: s.x, y: s.y }))
|
|
: [];
|
|
const url = mapId ? SERVER + '/api/maps/' + mapId : SERVER + '/api/maps';
|
|
const method = mapId ? 'PUT' : 'POST';
|
|
fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
const id = data.mapId || mapId;
|
|
statusEl.textContent = 'บันทึกแล้ว รหัสฉาก: ' + id + (showMapInGame ? '' : ' (ไม่โชว์ฉาก) — ออกจากห้องแล้วเข้าใหม่ หรือกด F5 ในหน้าเล่น ถึงจะเห็นผล');
|
|
if (!mapId) window.history.replaceState({}, '', 'editor.html' + buildEditorSearch(id));
|
|
} else statusEl.textContent = data.error || 'บันทึกไม่ได้';
|
|
})
|
|
.catch(() => { statusEl.textContent = 'บันทึกไม่ได้'; });
|
|
});
|
|
|
|
const cellColorWrap = document.getElementById('cell-color-wrap');
|
|
const cellAlphaEl = document.getElementById('cell-alpha');
|
|
const cellAlphaValue = document.getElementById('cell-alpha-value');
|
|
if (drawModeEl) drawModeEl.addEventListener('change', () => {
|
|
if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
|
});
|
|
if (cellColorWrap && drawModeEl) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
|
if (cellAlphaEl && cellAlphaValue) cellAlphaEl.addEventListener('input', () => { cellAlphaValue.textContent = cellAlphaEl.value; draw(); });
|
|
|
|
mapW.addEventListener('change', resize);
|
|
mapH.addEventListener('change', resize);
|
|
tileSizeEl.addEventListener('change', resize);
|
|
if (characterCellsEl) characterCellsEl.addEventListener('input', draw);
|
|
|
|
if (mapId) {
|
|
fetch(SERVER + '/api/maps/' + mapId)
|
|
.then(r => r.json())
|
|
.then(m => {
|
|
width = m.width; height = m.height; tileSize = m.tileSize || 32; characterCells = Math.max(1, Math.min(4, m.characterCells || 1));
|
|
ground = m.ground || [];
|
|
objects = m.objects || [];
|
|
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));
|
|
cellColors = (m.cellColors && m.cellColors.length === height) ? m.cellColors.map(r => r && r.slice ? r.slice() : Array(width).fill(null)) : Array(height).fill(0).map(() => Array(width).fill(null));
|
|
showMapInGame = m.showMapInGame !== false;
|
|
const showMapEl = document.getElementById('show-map-in-game');
|
|
if (showMapEl) showMapEl.checked = showMapInGame;
|
|
ensureInteractive();
|
|
ensureStartGameArea();
|
|
ensureSpawnArea();
|
|
backgroundImage = m.backgroundImage || null;
|
|
if (backgroundImage) {
|
|
backgroundImageImg = new Image();
|
|
backgroundImageImg.onload = () => { draw(); };
|
|
backgroundImageImg.src = backgroundImage;
|
|
} else backgroundImageImg = null;
|
|
spawn = m.spawn || { x: 1, y: 1 };
|
|
gauntletPlayerSpawns = Array.isArray(m.gauntletPlayerSpawns)
|
|
? m.gauntletPlayerSpawns.map((s) => ({ x: Math.floor(Number(s.x)), y: Math.floor(Number(s.y)) }))
|
|
.filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y))
|
|
: [];
|
|
gameType = m.gameType || 'zep';
|
|
if (gameTypeEl) gameTypeEl.value = gameType;
|
|
lanes = (m.lanes && m.lanes.length) ? m.lanes.map(l => ({ ...l })) : [];
|
|
if (gameType === 'gauntlet' || gameType === 'stack') lanes = [];
|
|
if (gameType === 'frogger') {
|
|
ensureLanes();
|
|
if (lanes.length !== height) ensureLanes();
|
|
}
|
|
mapName.value = m.name || ''; mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
|
|
if (characterCellsEl) characterCellsEl.value = characterCells;
|
|
sanitizeGauntletPlayerSpawns();
|
|
resize(); draw();
|
|
toggleFroggerUI();
|
|
})
|
|
.catch(() => initGrid());
|
|
} else { initGrid(); toggleFroggerUI(); }
|
|
})();
|