888 lines
29 KiB
HTML
888 lines
29 KiB
HTML
<!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>
|
||
<button class="tool-btn full" onclick="setSpecial('medSpawn')">💊 Med-Spawn</button>
|
||
</div>
|
||
<div class="legend" style="margin-top:8px">
|
||
<span>Cyan ●</span> = Spielerstart<br>
|
||
<span>Gold ●</span> = Geist-Startpos<br>
|
||
<span>Pink ●</span> = Med-Spawnpunkt
|
||
</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>1–8</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 medSpawns = (typeof MED_SPAWNS !== 'undefined') ? MED_SPAWNS.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()));
|
||
medSpawns.forEach(sp => drawMarker(ctxG, sp.r, sp.c, '#EC4899', 'M'));
|
||
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' :
|
||
mode === 'medSpawn' ? 'Klick: Med-Spawnpunkt hinzufügen / entfernen' :
|
||
'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`;
|
||
}
|
||
} else if (specialMode === 'medSpawn') {
|
||
const idx = medSpawns.findIndex(sp => sp.r === r && sp.c === c);
|
||
if (idx >= 0) {
|
||
medSpawns.splice(idx, 1);
|
||
document.getElementById('info-bar').textContent = `Med-Spawn entfernt bei [${r}, ${c}] | Gesamt: ${medSpawns.length}`;
|
||
} else {
|
||
medSpawns.push({ r, c });
|
||
document.getElementById('info-bar').textContent = `Med-Spawn gesetzt bei [${r}, ${c}] | Gesamt: ${medSpawns.length}`;
|
||
}
|
||
}
|
||
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}];`);
|
||
const ms = medSpawns.map(s => `{r:${s.r},c:${s.c}}`).join(',');
|
||
lines.push(`const MED_SPAWNS = [${ms}];`);
|
||
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>
|