Added kidney_labe and Cyste_kid

This commit is contained in:
verboomp
2026-04-16 08:14:20 +02:00
parent aa66c030f8
commit 9cc8ac8cad
40 changed files with 6762 additions and 0 deletions

View File

@@ -0,0 +1,872 @@
<!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>