Files
kaltaquise-gamification/Cyst_Kid/level-designer.html
2026-04-16 08:14:20 +02:00

873 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cyst-Kid — Level Designer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0a0a1a;
color: #fff;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 16px;
gap: 12px;
}
h1 {
font-family: 'Press Start 2P', 'Courier New', monospace;
color: #8B5CF6;
font-size: 18px;
letter-spacing: 2px;
text-shadow: 0 0 12px #8B5CF6;
}
.layout {
display: flex;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
}
/* ---- Canvas ---- */
#canvas-wrap {
position: relative;
cursor: crosshair;
border: 2px solid #8B5CF6;
box-shadow: 0 0 20px rgba(139,92,246,.4);
}
#grid { display: block; }
#overlay { position: absolute; top: 0; left: 0; cursor: crosshair; }
/* ---- Sidebar ---- */
.sidebar {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 200px;
max-width: 240px;
}
.panel {
background: #111133;
border: 1px solid #333366;
border-radius: 6px;
padding: 10px;
}
.panel h3 {
font-size: 10px;
color: #A78BFA;
letter-spacing: 1px;
margin-bottom: 8px;
text-transform: uppercase;
}
/* ---- Palette ---- */
.palette {
display: flex;
flex-direction: column;
gap: 4px;
}
.tile-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
border: 2px solid transparent;
border-radius: 4px;
background: #0d0d2a;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
color: #ccc;
transition: border-color .15s, background .15s;
width: 100%;
text-align: left;
}
.tile-btn:hover { background: #1a1a40; }
.tile-btn.active { border-color: #8B5CF6; background: #1e1e4a; color: #fff; }
.tile-swatch {
width: 18px; height: 18px;
border-radius: 2px;
flex-shrink: 0;
border: 1px solid rgba(255,255,255,.2);
}
.tile-key {
margin-left: auto;
font-size: 9px;
color: #666;
background: #222;
padding: 1px 4px;
border-radius: 3px;
}
/* ---- Tool buttons ---- */
.tool-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.tool-btn {
padding: 6px 4px;
font-size: 10px;
font-family: 'Courier New', monospace;
background: #0d0d2a;
border: 2px solid #333366;
border-radius: 4px;
color: #ccc;
cursor: pointer;
text-align: center;
transition: border-color .15s, background .15s;
}
.tool-btn:hover { background: #1a1a40; border-color: #8B5CF6; color: #fff; }
.tool-btn.active { background: #1e1e4a; border-color: #8B5CF6; color: #fff; }
.tool-btn.danger { border-color: #7f1d1d; color: #f87171; }
.tool-btn.danger:hover { background: #3b0000; border-color: #ef4444; }
.tool-btn.success { border-color: #14532d; color: #4ade80; }
.tool-btn.success:hover { background: #003b0e; border-color: #22c55e; }
.tool-btn.full { grid-column: span 2; }
/* ---- Info ---- */
#info-bar {
font-size: 10px;
color: #888;
min-height: 16px;
}
#dot-count {
font-size: 10px;
color: #A78BFA;
}
/* ---- Export modal ---- */
#export-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,.75);
z-index: 100;
align-items: center;
justify-content: center;
}
#export-modal.open { display: flex; }
.modal-box {
background: #111133;
border: 2px solid #8B5CF6;
border-radius: 8px;
padding: 20px;
max-width: 720px;
width: 95vw;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-box h2 { color: #A78BFA; font-size: 13px; letter-spacing: 1px; }
#export-code {
flex: 1;
background: #0a0a1a;
color: #4ade80;
font-family: 'Courier New', monospace;
font-size: 11px;
padding: 12px;
border: 1px solid #333;
border-radius: 4px;
resize: none;
min-height: 300px;
overflow: auto;
}
.modal-actions { display: flex; gap: 8px; }
.modal-close {
padding: 8px 16px;
background: #0d0d2a;
border: 2px solid #333366;
border-radius: 4px;
color: #ccc;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.modal-close:hover { border-color: #8B5CF6; }
.modal-copy {
padding: 8px 16px;
background: #14532d;
border: 2px solid #22c55e;
border-radius: 4px;
color: #4ade80;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.modal-copy:hover { background: #166534; }
/* ---- Legend ---- */
.legend { font-size: 9px; color: #666; line-height: 1.8; }
.legend span { color: #888; }
</style>
</head>
<body>
<h1>CYST-KID · LEVEL DESIGNER</h1>
<div class="layout">
<!-- Canvas area -->
<div>
<div id="canvas-wrap">
<canvas id="grid"></canvas>
<canvas id="overlay"></canvas>
</div>
<div style="margin-top:6px; display:flex; justify-content:space-between; padding:0 2px;">
<span id="info-bar">hover over a tile to inspect</span>
<span id="dot-count"></span>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Tile palette -->
<div class="panel">
<h3>Tile-Palette</h3>
<div class="palette" id="palette"></div>
</div>
<!-- Tools -->
<div class="panel">
<h3>Werkzeuge</h3>
<div class="tool-grid">
<button class="tool-btn active" id="tool-paint" onclick="setTool('paint')">✏️ Malen</button>
<button class="tool-btn" id="tool-fill" onclick="setTool('fill')">🪣 Füllen</button>
<button class="tool-btn" id="tool-pick" onclick="setTool('pick')">💉 Picker</button>
<button class="tool-btn" id="tool-line" onclick="setTool('line')">📏 Linie</button>
<button class="tool-btn full" id="tool-rect" onclick="setTool('rect')">⬜ Rechteck</button>
</div>
</div>
<!-- Map size -->
<div class="panel">
<h3>Map-Größe</h3>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:6px; align-items:center; font-size:10px; color:#aaa; margin-bottom:6px;">
<label for="input-cols">Breite (Spalten)</label>
<input id="input-cols" type="number" min="4" max="128"
style="background:#0a0a1a;border:1px solid #333366;color:#fff;padding:3px 6px;border-radius:3px;font-family:monospace;font-size:11px;width:100%">
<label for="input-rows">Höhe (Zeilen)</label>
<input id="input-rows" type="number" min="4" max="128"
style="background:#0a0a1a;border:1px solid #333366;color:#fff;padding:3px 6px;border-radius:3px;font-family:monospace;font-size:11px;width:100%">
</div>
<button class="tool-btn full" style="width:100%" onclick="resizeMap()">↔ Größe anwenden</button>
<div class="legend" style="margin-top:6px">Neue Felder werden mit Wand gefüllt.<br>Verkleinern schneidet ab.</div>
</div>
<!-- Actions -->
<div class="panel">
<h3>Aktionen</h3>
<div class="tool-grid">
<button class="tool-btn" onclick="undo()">↩ Undo</button>
<button class="tool-btn" onclick="redo()">↪ Redo</button>
<button class="tool-btn success full" onclick="openExport()">📋 Exportieren</button>
<button class="tool-btn full" onclick="loadDefault()">↺ Reset Map</button>
<button class="tool-btn danger full" onclick="clearAll()">✕ Alles löschen</button>
</div>
</div>
<!-- Special positions -->
<div class="panel">
<h3>Spezial-Positionen</h3>
<div class="tool-grid">
<button class="tool-btn" onclick="setSpecial('player')">🧑 Spieler-Start</button>
<button class="tool-btn" onclick="setSpecial('ghost')">👻 Geist-Haus</button>
</div>
<div class="legend" style="margin-top:8px">
<span>Cyan ●</span> = Spielerstart<br>
<span>Gold ●</span> = Geist-Startpos
</div>
</div>
<!-- Info -->
<div class="panel">
<h3>Steuerung</h3>
<div class="legend">
<span>LMB</span> = Malen / Tool<br>
<span>RMB</span> = Löschen (→ dot)<br>
<span>Shift+drag</span> = Linie<br>
<span>18</span> = Tile wählen<br>
<span>Ctrl+Z</span> = Undo<br>
<span>Ctrl+Y</span> = Redo<br>
<span>P</span> = Picker<br>
<span>F</span> = Füllen
</div>
</div>
</div>
</div>
<!-- Export modal -->
<div id="export-modal">
<div class="modal-box">
<h2>MAP-EXPORT — in constants.js einfügen</h2>
<textarea id="export-code" readonly spellcheck="false"></textarea>
<div class="modal-actions">
<button class="modal-copy" onclick="copyExport()">Kopieren</button>
<button class="modal-close" onclick="closeExport()">Schließen</button>
</div>
</div>
</div>
<!-- Load constants so we can pull the MAP and colour constants -->
<script src="js/constants.js"></script>
<script>
// ============================================================
// TILE DEFINITIONS
// ============================================================
const TILE_DEFS = [
{ id: 0, label: 'Dot (Punkt)', color: '#333355', dot: '#FFF', key: '1' },
{ id: 1, label: 'Wall (Wand)', color: '#8B5CF6', border: '#A78BFA', key: '2' },
{ id: 2, label: 'Empty (Leer)', color: '#111133', key: '3' },
{ id: 3, label: 'Ghost-Wall', color: '#6D28D9', key: '4' },
{ id: 4, label: 'Ghost-Door', color: '#FFD700', key: '5' },
{ id: 5, label: 'Tunnel', color: '#1a3a1a', border: '#4ade80', key: '6' },
{ id: 8, label: 'Ghost-Inside', color: '#1a1a55', key: '7' },
];
const TILE_ID_TO_DEF = {};
TILE_DEFS.forEach(d => TILE_ID_TO_DEF[d.id] = d);
// ============================================================
// STATE
// ============================================================
const SCALE = 18; // px per tile in editor
let cols = MAP[0].length;
let rows = MAP.length;
// Deep copy of the game MAP
let map = MAP.map(row => [...row]);
// Undo / redo stacks (store JSON snapshots)
let undoStack = [];
let redoStack = [];
// Player start and ghost house positions (mirroring constants)
let playerStart = { c: PL0.c, r: PL0.r };
let ghostHomes = PH.map(p => ({ c: p.c, r: p.r }));
let selectedTile = 1; // currently selected tile id
let currentTool = 'paint';
let isPainting = false;
let lineStart = null;
let rectStart = null;
let specialMode = null; // 'player' | 'ghost' | null
let ghostSetIndex = 0;
// ============================================================
// CANVAS SETUP
// ============================================================
const cvGrid = document.getElementById('grid');
const cvOver = document.getElementById('overlay');
const ctxG = cvGrid.getContext('2d');
const ctxO = cvOver.getContext('2d');
cvGrid.width = cvOver.width = cols * SCALE;
cvGrid.height = cvOver.height = rows * SCALE;
// ============================================================
// BUILD PALETTE
// ============================================================
function buildPalette() {
const el = document.getElementById('palette');
TILE_DEFS.forEach(def => {
const btn = document.createElement('button');
btn.className = 'tile-btn' + (def.id === selectedTile ? ' active' : '');
btn.id = 'tile-btn-' + def.id;
btn.onclick = () => selectTile(def.id);
const swatch = document.createElement('span');
swatch.className = 'tile-swatch';
swatch.style.background = def.color;
if (def.border) swatch.style.borderColor = def.border;
const label = document.createElement('span');
label.textContent = def.label;
const key = document.createElement('span');
key.className = 'tile-key';
key.textContent = def.key;
btn.append(swatch, label, key);
el.appendChild(btn);
});
}
function selectTile(id) {
selectedTile = id;
document.querySelectorAll('.tile-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('tile-btn-' + id);
if (btn) btn.classList.add('active');
}
// ============================================================
// DRAW
// ============================================================
function drawTile(ctx, r, c, tileId) {
const x = c * SCALE, y = r * SCALE;
const def = TILE_ID_TO_DEF[tileId];
if (!def) {
ctx.fillStyle = '#ff00ff';
ctx.fillRect(x, y, SCALE, SCALE);
return;
}
ctx.fillStyle = def.color;
ctx.fillRect(x, y, SCALE, SCALE);
// Wall edge highlight
if (tileId === 1) {
ctx.strokeStyle = '#A78BFA';
ctx.lineWidth = 0.5;
ctx.strokeRect(x + .5, y + .5, SCALE - 1, SCALE - 1);
}
// Dot
if (tileId === 0) {
ctx.fillStyle = '#FFF';
const ds = 3;
ctx.fillRect(x + SCALE / 2 - ds / 2, y + SCALE / 2 - ds / 2, ds, ds);
}
// Ghost door
if (tileId === 4) {
ctx.fillStyle = '#FFD700';
ctx.fillRect(x + 1, y + SCALE * .35, SCALE - 2, SCALE * .3);
}
// Tunnel stripe
if (tileId === 5) {
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1;
ctx.strokeRect(x + .5, y + .5, SCALE - 1, SCALE - 1);
}
// Ghost wall
if (tileId === 3) {
ctx.strokeStyle = '#9F7AEA';
ctx.lineWidth = 1;
ctx.strokeRect(x + 1, y + 1, SCALE - 2, SCALE - 2);
}
}
function resizeCanvases() {
cvGrid.width = cvOver.width = cols * SCALE;
cvGrid.height = cvOver.height = rows * SCALE;
}
function drawGrid() {
ctxG.clearRect(0, 0, cvGrid.width, cvGrid.height);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
drawTile(ctxG, r, c, map[r][c]);
}
}
// Grid lines (subtle)
ctxG.strokeStyle = 'rgba(255,255,255,0.05)';
ctxG.lineWidth = 0.5;
for (let r = 0; r <= rows; r++) {
ctxG.beginPath();
ctxG.moveTo(0, r * SCALE);
ctxG.lineTo(cols * SCALE, r * SCALE);
ctxG.stroke();
}
for (let c = 0; c <= cols; c++) {
ctxG.beginPath();
ctxG.moveTo(c * SCALE, 0);
ctxG.lineTo(c * SCALE, rows * SCALE);
ctxG.stroke();
}
// Special markers
drawMarker(ctxG, playerStart.r, playerStart.c, '#00DDFF', '★');
ghostHomes.forEach((gh, i) => drawMarker(ctxG, gh.r, gh.c, '#FFD700', (i + 1).toString()));
updateDotCount();
}
function drawMarker(ctx, r, c, color, label) {
const x = c * SCALE + SCALE / 2;
const y = r * SCALE + SCALE / 2;
ctx.save();
ctx.fillStyle = color;
ctx.globalAlpha = 0.85;
ctx.beginPath();
ctx.arc(x, y, SCALE * 0.38, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = '#000';
ctx.font = `bold ${SCALE * 0.5}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, x, y + 1);
ctx.restore();
}
function drawOverlay(r, c, preview) {
ctxO.clearRect(0, 0, cvOver.width, cvOver.height);
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
// Preview tiles (line / rect)
if (preview && preview.length) {
preview.forEach(([pr, pc]) => {
ctxO.save();
ctxO.globalAlpha = 0.55;
drawTile(ctxO, pr, pc, selectedTile);
ctxO.restore();
});
}
// Hover highlight
ctxO.strokeStyle = 'rgba(255,255,255,0.8)';
ctxO.lineWidth = 1.5;
ctxO.strokeRect(c * SCALE + 0.75, r * SCALE + 0.75, SCALE - 1.5, SCALE - 1.5);
}
function updateDotCount() {
let dots = 0;
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
if (map[r][c] === 0) dots++;
document.getElementById('dot-count').textContent = `Dots: ${dots} | ${cols}×${rows}`;
}
// ============================================================
// UNDO / REDO
// ============================================================
function snapshot() {
undoStack.push(JSON.stringify(map));
if (undoStack.length > 100) undoStack.shift();
redoStack = [];
}
function undo() {
if (!undoStack.length) return;
redoStack.push(JSON.stringify(map));
map = JSON.parse(undoStack.pop());
drawGrid();
}
function redo() {
if (!redoStack.length) return;
undoStack.push(JSON.stringify(map));
map = JSON.parse(redoStack.pop());
drawGrid();
}
// ============================================================
// EDIT OPERATIONS
// ============================================================
function paintCell(r, c, tid) {
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
if (map[r][c] === tid) return;
map[r][c] = tid;
drawTile(ctxG, r, c, tid);
}
function floodFill(r, c, newId) {
const oldId = map[r][c];
if (oldId === newId) return;
snapshot();
const stack = [[r, c]];
const visited = new Set();
while (stack.length) {
const [cr, cc] = stack.pop();
const key = cr * cols + cc;
if (visited.has(key)) continue;
if (cr < 0 || cr >= rows || cc < 0 || cc >= cols) continue;
if (map[cr][cc] !== oldId) continue;
visited.add(key);
map[cr][cc] = newId;
stack.push([cr - 1, cc], [cr + 1, cc], [cr, cc - 1], [cr, cc + 1]);
}
drawGrid();
}
function getLineCells(r0, c0, r1, c1) {
const cells = [];
const dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0);
const sr = r0 < r1 ? 1 : -1, sc = c0 < c1 ? 1 : -1;
let err = dr - dc;
let r = r0, c = c0;
while (true) {
cells.push([r, c]);
if (r === r1 && c === c1) break;
const e2 = 2 * err;
if (e2 > -dc) { err -= dc; r += sr; }
if (e2 < dr) { err += dr; c += sc; }
}
return cells;
}
function getRectCells(r0, c0, r1, c1) {
const cells = [];
const rMin = Math.min(r0, r1), rMax = Math.max(r0, r1);
const cMin = Math.min(c0, c1), cMax = Math.max(c0, c1);
for (let r = rMin; r <= rMax; r++)
for (let c = cMin; c <= cMax; c++)
cells.push([r, c]);
return cells;
}
function applyLineCells(cells) {
cells.forEach(([r, c]) => paintCell(r, c, selectedTile));
}
// ============================================================
// TOOLS
// ============================================================
function setTool(t) {
currentTool = t;
lineStart = null; rectStart = null;
['paint', 'fill', 'pick', 'line', 'rect'].forEach(id => {
const el = document.getElementById('tool-' + id);
if (el) el.classList.toggle('active', id === t);
});
}
// ============================================================
// SPECIAL POSITIONS
// ============================================================
function setSpecial(mode) {
specialMode = mode;
ghostSetIndex = 0;
document.getElementById('info-bar').textContent =
mode === 'player'
? 'Klick: Spieler-Startposition setzen'
: 'Klick: Geist-Position 1 von 4 setzen';
}
// ============================================================
// MOUSE / POINTER EVENTS
// ============================================================
function getTilePos(e) {
const rect = cvOver.getBoundingClientRect();
return {
r: Math.floor((e.clientY - rect.top) / SCALE),
c: Math.floor((e.clientX - rect.left) / SCALE)
};
}
cvOver.addEventListener('contextmenu', e => e.preventDefault());
cvOver.addEventListener('mousemove', e => {
const { r, c } = getTilePos(e);
const inBounds = r >= 0 && r < rows && c >= 0 && c < cols;
if (!inBounds) { drawOverlay(-1, -1); return; }
const tDef = TILE_ID_TO_DEF[map[r][c]];
document.getElementById('info-bar').textContent =
`[${r}, ${c}] → ${tDef ? tDef.label : '?'} (id=${map[r][c]})`;
let preview = null;
if (currentTool === 'line' && lineStart) {
preview = getLineCells(lineStart.r, lineStart.c, r, c);
} else if (currentTool === 'rect' && rectStart) {
preview = getRectCells(rectStart.r, rectStart.c, r, c);
}
drawOverlay(r, c, preview);
// Paint drag
if (isPainting && (currentTool === 'paint')) {
const tid = (e.buttons & 2) ? 0 : selectedTile;
paintCell(r, c, tid);
drawTile(ctxG, r, c, map[r][c]);
}
});
cvOver.addEventListener('mousedown', e => {
e.preventDefault();
const { r, c } = getTilePos(e);
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
// Special placement mode
if (specialMode) {
if (specialMode === 'player') {
playerStart = { r, c };
specialMode = null;
document.getElementById('info-bar').textContent = `Spielerstart → [${r}, ${c}]`;
} else if (specialMode === 'ghost') {
ghostHomes[ghostSetIndex] = { r, c };
ghostSetIndex++;
if (ghostSetIndex >= 4) {
specialMode = null;
document.getElementById('info-bar').textContent = 'Alle 4 Geist-Positionen gesetzt.';
} else {
document.getElementById('info-bar').textContent =
`Geist-Position ${ghostSetIndex + 1} von 4 setzen`;
}
}
drawGrid();
return;
}
const isRMB = e.button === 2;
if (currentTool === 'pick') {
selectTile(map[r][c]);
return;
}
if (currentTool === 'fill') {
floodFill(r, c, isRMB ? 0 : selectedTile);
return;
}
if (currentTool === 'line') {
if (!lineStart) {
lineStart = { r, c };
} else {
snapshot();
applyLineCells(getLineCells(lineStart.r, lineStart.c, r, c));
lineStart = null;
drawGrid();
}
return;
}
if (currentTool === 'rect') {
if (!rectStart) {
rectStart = { r, c };
} else {
snapshot();
applyLineCells(getRectCells(rectStart.r, rectStart.c, r, c));
rectStart = null;
drawGrid();
}
return;
}
// Paint tool
snapshot();
isPainting = true;
paintCell(r, c, isRMB ? 0 : selectedTile);
drawGrid();
});
cvOver.addEventListener('mouseup', () => {
if (isPainting) drawGrid();
isPainting = false;
});
cvOver.addEventListener('mouseleave', () => {
ctxO.clearRect(0, 0, cvOver.width, cvOver.height);
isPainting = false;
});
// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================
document.addEventListener('keydown', e => {
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
if (e.ctrlKey || e.metaKey) {
if (e.key === 'z') { e.preventDefault(); undo(); return; }
if (e.key === 'y') { e.preventDefault(); redo(); return; }
}
// Tile shortcuts
TILE_DEFS.forEach(def => { if (e.key === def.key) selectTile(def.id); });
if (e.key === 'p' || e.key === 'P') setTool('pick');
if (e.key === 'f' || e.key === 'F') setTool('fill');
if (e.key === 'l' || e.key === 'L') setTool('line');
if (e.key === 'r' || e.key === 'R') setTool('rect');
if (e.key === 'Escape') { lineStart = null; rectStart = null; specialMode = null; }
});
// ============================================================
// ACTIONS
// ============================================================
function clearAll() {
if (!confirm('Gesamte Map löschen (alle Tiles auf 1 = Wand)?')) return;
snapshot();
map = Array.from({ length: rows }, () => new Array(cols).fill(1));
drawGrid();
}
function loadDefault() {
if (!confirm('Map auf den Original-Stand zurücksetzen?')) return;
snapshot();
map = MAP.map(row => [...row]);
cols = map[0].length;
rows = map.length;
document.getElementById('input-cols').value = cols;
document.getElementById('input-rows').value = rows;
resizeCanvases();
drawGrid();
}
function resizeMap() {
const newCols = Math.max(4, Math.min(128, parseInt(document.getElementById('input-cols').value) || cols));
const newRows = Math.max(4, Math.min(128, parseInt(document.getElementById('input-rows').value) || rows));
if (newCols === cols && newRows === rows) return;
snapshot();
// Build new map: keep existing tiles where they fit, fill new space with wall (1)
const newMap = Array.from({ length: newRows }, (_, r) =>
Array.from({ length: newCols }, (_, c) =>
(r < rows && c < cols) ? map[r][c] : 1
)
);
map = newMap;
cols = newCols;
rows = newRows;
// Clamp special positions to new bounds
playerStart.r = Math.min(playerStart.r, rows - 1);
playerStart.c = Math.min(playerStart.c, cols - 1);
ghostHomes = ghostHomes.map(g => ({
r: Math.min(g.r, rows - 1),
c: Math.min(g.c, cols - 1)
}));
resizeCanvases();
drawGrid();
}
// ============================================================
// EXPORT
// ============================================================
function buildExportCode() {
const lines = [];
lines.push(`const COLS = ${cols};`);
lines.push(`const ROWS = ${rows};`);
lines.push('');
lines.push('/* 0=dot 1=wall 2=empty 3=ghostWall 4=ghostDoor 5=tunnel 8=ghostInside */');
lines.push('const MAP = [');
map.forEach((row, r) => {
lines.push('[' + row.join(',') + '],//' + r);
});
lines.push('];');
lines.push('');
lines.push(`const PL0 = {c:${playerStart.c},r:${playerStart.r}};`);
const ph = ghostHomes.map(g => `{c:${g.c},r:${g.r}}`).join(',');
lines.push(`const PH = [${ph}];`);
return lines.join('\n');
}
function openExport() {
document.getElementById('export-code').value = buildExportCode();
document.getElementById('export-modal').classList.add('open');
}
function closeExport() {
document.getElementById('export-modal').classList.remove('open');
}
function copyExport() {
const ta = document.getElementById('export-code');
ta.select();
navigator.clipboard.writeText(ta.value).then(() => {
const btn = document.querySelector('.modal-copy');
const orig = btn.textContent;
btn.textContent = '✓ Kopiert!';
setTimeout(() => btn.textContent = orig, 1500);
});
}
// Close modal on backdrop click
document.getElementById('export-modal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeExport();
});
// ============================================================
// BOOTSTRAP
// ============================================================
buildPalette();
document.getElementById('input-cols').value = cols;
document.getElementById('input-rows').value = rows;
drawGrid();
</script>
</body>
</html>