Tourismusregion: Karten-Editor reaktiviert (mit Lehrer-Guard)
editor.html + js/editor.js aus der Quelle uebernommen. save_map.php schreibt js/maps-custom.json (mit Auto-Backups) - jetzt abgesichert: nur eingeloggte Lehrpersonen duerfen speichern. Das Spiel laedt die Overrides beim Start (applyMapOverrides). Thomas fixt damit Routen- Punkte und Bauplaetze; die Datei wird danach ins Repo uebernommen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// Speichert Editor-Daten (Routen-Kurven + Bauslots) nach js/maps-custom.json.
|
||||
// Das Spiel lädt diese Datei beim Start und überschreibt damit die
|
||||
// eingebauten Karten-Daten. Nur lokal (XAMPP) gedacht.
|
||||
//
|
||||
// WICHTIG: Vor jedem Überschreiben wird die bisherige Datei automatisch als
|
||||
// Zeitstempel-Backup nach api/mapbackups/ gesichert – so geht nichts verloren.
|
||||
// Ein leeres {} wird abgelehnt, damit versehentliche Resets nicht speichern.
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Plattform-Guard: Schreiben nur für eingeloggte Lehrpersonen.
|
||||
// Pfad funktioniert lokal (App/sims/...) UND auf Prod (Top-Level sims/...).
|
||||
require_once __DIR__ . '/../../../php/config/app.php';
|
||||
require_once __DIR__ . '/../../../php/lib/Database.php';
|
||||
require_once __DIR__ . '/../../../php/lib/Response.php';
|
||||
require_once __DIR__ . '/../../../php/lib/Session.php';
|
||||
Session::requireTeacher();
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data) || count($data) === 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'invalid or empty json']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$target = __DIR__ . '/../js/maps-custom.json';
|
||||
$backupDir = __DIR__ . '/mapbackups';
|
||||
|
||||
// vorhandene, nicht-leere Datei zuerst sichern
|
||||
if (file_exists($target)) {
|
||||
$old = trim((string)file_get_contents($target));
|
||||
if ($old !== '' && $old !== '{}') {
|
||||
if (!is_dir($backupDir)) @mkdir($backupDir, 0777, true);
|
||||
@copy($target, $backupDir . '/maps-custom-' . date('Ymd-His') . '.json');
|
||||
@copy($target, $backupDir . '/maps-custom.prev.json'); // schneller Griff aufs letzte
|
||||
$files = glob($backupDir . '/maps-custom-*.json'); // max. 40 Backups halten
|
||||
if ($files && count($files) > 40) {
|
||||
sort($files);
|
||||
foreach (array_slice($files, 0, count($files) - 40) as $f) @unlink($f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ok = file_put_contents($target, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
echo json_encode(['ok' => (bool)$ok, 'file' => 'js/maps-custom.json', 'backed_up' => is_dir($backupDir)]);
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Karten-Editor – Tourismusregion</title>
|
||||
<style>
|
||||
:root { --green:#4f7f5e; --beige:#ede6d6; --orange:#e8892b; --ink:#2f3e46; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin:0; height:100%; overflow:hidden;
|
||||
font-family:-apple-system,"Segoe UI",Roboto,sans-serif; color:var(--ink); }
|
||||
#cv { position:fixed; inset:0; width:100%; height:100%; background:#e7dcc4; touch-action:none; }
|
||||
#panel { position:fixed; top:10px; left:10px; width:265px; background:rgba(253,251,244,.95);
|
||||
border-radius:14px; box-shadow:0 6px 20px rgba(47,62,70,.2); padding:12px 14px; z-index:10; }
|
||||
h1 { font-size:15px; margin:0 0 8px; }
|
||||
label { display:block; font-size:11px; color:#6b7a70; margin:8px 0 2px; text-transform:uppercase; letter-spacing:.03em; }
|
||||
select, button { width:100%; padding:8px; border-radius:9px; border:1.5px solid #d9cfb4;
|
||||
background:#fff; font-size:13px; font-family:inherit; margin-bottom:2px; }
|
||||
button { cursor:pointer; }
|
||||
button.primary { background:var(--orange); color:#fff; border:none; font-weight:700; }
|
||||
button.mini { width:49%; display:inline-block; }
|
||||
.mode { display:flex; gap:6px; }
|
||||
.mode button { flex:1; }
|
||||
.mode button.active { background:var(--green); color:#fff; border-color:var(--green); }
|
||||
#hint { font-size:11px; line-height:1.45; color:#6b7a70; margin-top:8px; }
|
||||
#status { font-size:12px; margin-top:6px; min-height:16px; font-weight:600; }
|
||||
#out { width:100%; height:70px; font-size:10px; margin-top:6px; display:none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="cv"></canvas>
|
||||
<div id="panel">
|
||||
<h1>🛠️ Karten-Editor</h1>
|
||||
<label>Karte</label>
|
||||
<select id="mapSel"></select>
|
||||
<label>Modus</label>
|
||||
<div class="mode">
|
||||
<button id="modePath" class="active">📍 Pfad</button>
|
||||
<button id="modeSlot">🏗️ Bauslots</button>
|
||||
</div>
|
||||
<div id="pathTools">
|
||||
<label>Route</label>
|
||||
<select id="routeSel"></select>
|
||||
</div>
|
||||
<div id="slotTools" style="display:none">
|
||||
<label>Zone für neue Slots</label>
|
||||
<select id="zoneSel">
|
||||
<option value="standard">Standard (alle Gebäude)</option>
|
||||
<option value="road">Straße (Parkplatz/Bus)</option>
|
||||
<option value="lake">Seeufer (Badesteg)</option>
|
||||
<option value="hang">Hang (Skilift)</option>
|
||||
<option value="berg">Berg (Almhütte/Aussicht)</option>
|
||||
</select>
|
||||
</div>
|
||||
<label>Aktionen</label>
|
||||
<button id="undoBtn" class="mini">↩︎ Rückgängig</button>
|
||||
<button id="clearBtn" class="mini">🗑 Route leeren</button>
|
||||
<button id="resetBtn" class="mini">♻︎ Original laden</button>
|
||||
<button id="saveBtn" class="primary">💾 Speichern (ins Spiel)</button>
|
||||
<div id="status"></div>
|
||||
<textarea id="out" readonly></textarea>
|
||||
<div id="hint">
|
||||
<b>Klick</b> = Punkt/Slot setzen · <b>Ziehen</b> = verschieben ·
|
||||
<b>Doppelklick</b> = löschen · <b>Rechte Maus/2 Finger</b> = Karte schieben ·
|
||||
<b>Mausrad</b> = Zoom.<br>
|
||||
Die orange Linie ist die geglättete Kurve, auf der die Figuren laufen.
|
||||
Speichern schreibt <code>js/maps-custom.json</code> – das Spiel lädt sie
|
||||
beim nächsten Start automatisch.<br>
|
||||
<b>Jede Änderung wird sofort lokal im Browser gesichert</b> und beim Öffnen
|
||||
wiederhergestellt – deine Arbeit geht nicht verloren.
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="js/editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,393 @@
|
||||
// Karten-Editor: legt Pfad-Kurven (Klickpunkte → Catmull-Rom-Glättung) und
|
||||
// Bauslots über die Karten-Artworks. Speichert nach js/maps-custom.json
|
||||
// (api/save_map.php, XAMPP) – das Spiel lädt die Datei beim Start.
|
||||
// Bedienung: Klick = setzen · Ziehen = verschieben · Doppelklick = löschen ·
|
||||
// rechte Maustaste/zweiter Finger = Karte schieben · Mausrad = Zoom.
|
||||
|
||||
import { MAPS, MAP_ORDER } from './maps.js';
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const cv = $('cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
|
||||
let mapId = MAP_ORDER[0];
|
||||
let mode = 'path';
|
||||
let routeId = null;
|
||||
let edit = null; // {routes: {id: [[x,y],…]}, meta: {id:{entry,exit,lake}}, slots: [{x,y,zone}]}
|
||||
let history = [];
|
||||
let cam = { x: 0, y: 0, scale: 0.7 };
|
||||
let drag = null; // {kind:'point'|'slot'|'pan', idx, moved}
|
||||
let mapImg = new Image();
|
||||
let slotImg = new Image();
|
||||
slotImg.src = 'assets/sprites/slot.png';
|
||||
|
||||
const ZONE_COLORS = {
|
||||
standard: '#2f6ea0', road: '#8c6239', lake: '#2e8b8b', hang: '#c9531f', berg: '#6d4fb8',
|
||||
};
|
||||
|
||||
// ---------- Laden ----------
|
||||
async function loadMap(id) {
|
||||
mapId = id;
|
||||
const def = MAPS[id];
|
||||
mapImg = new Image();
|
||||
mapImg.src = def.image;
|
||||
// vorhandene Overrides zuerst, sonst eingebaute Daten
|
||||
let ov = null;
|
||||
try {
|
||||
const r = await fetch('js/maps-custom.json', { cache: 'no-store' });
|
||||
if (r.ok) ov = (await r.json())[id];
|
||||
} catch { /* keine Overrides */ }
|
||||
edit = { routes: {}, meta: {}, slots: [] };
|
||||
for (const [rk, r] of Object.entries(def.routes)) {
|
||||
if (r.reverseOf) continue; // Gegenrichtungen entstehen automatisch
|
||||
edit.meta[rk] = { entry: r.entry, exit: r.exit, lake: !!r.lake };
|
||||
const ctrl = ov?.ctrl?.[rk];
|
||||
edit.routes[rk] = (ctrl || r.points).map(p => [p[0], p[1]]);
|
||||
}
|
||||
edit.slots = (ov?.slots || def.slots).map(s => ({ x: s.x, y: s.y, zone: s.zone }));
|
||||
// Lokaler Zwischenstand geht IMMER vor (verlustsicher gegen Datei-Resets):
|
||||
// Der Editor sichert jede Änderung im Browser – so ist Arbeit nie weg.
|
||||
const local = loadLocal(id);
|
||||
if (local) {
|
||||
for (const rk of Object.keys(edit.routes)) if (local.routes[rk]) edit.routes[rk] = local.routes[rk];
|
||||
if (local.slots) edit.slots = local.slots;
|
||||
setStatus('↩︎ Lokaler Bearbeitungsstand wiederhergestellt.');
|
||||
} else {
|
||||
setStatus('');
|
||||
}
|
||||
routeId = Object.keys(edit.routes)[0];
|
||||
buildRouteSel();
|
||||
history = [];
|
||||
fitCamera();
|
||||
}
|
||||
|
||||
const LKEY = id => `tt_editor_${id}`;
|
||||
function persistLocal() {
|
||||
try { localStorage.setItem(LKEY(mapId), JSON.stringify({ routes: edit.routes, slots: edit.slots, ts: Date.now() })); }
|
||||
catch { /* Speicher voll/Privatmodus – dann ohne Autosave */ }
|
||||
}
|
||||
function loadLocal(id) {
|
||||
try {
|
||||
const raw = localStorage.getItem(LKEY(id));
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
history.push(JSON.stringify({ routes: edit.routes, slots: edit.slots }));
|
||||
if (history.length > 60) history.shift();
|
||||
}
|
||||
|
||||
function undo() {
|
||||
const prev = history.pop();
|
||||
if (!prev) return;
|
||||
const o = JSON.parse(prev);
|
||||
edit.routes = o.routes;
|
||||
edit.slots = o.slots;
|
||||
persistLocal();
|
||||
}
|
||||
|
||||
// ---------- Catmull-Rom-Glättung ----------
|
||||
export function smoothCurve(pts, step = 20) {
|
||||
if (pts.length < 3) return pts.map(p => [Math.round(p[0]), Math.round(p[1])]);
|
||||
const out = [];
|
||||
const P = i => pts[Math.max(0, Math.min(pts.length - 1, i))];
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = P(i - 1), p1 = P(i), p2 = P(i + 1), p3 = P(i + 2);
|
||||
const segLen = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
|
||||
const k = Math.max(2, Math.round(segLen / step));
|
||||
for (let j = 0; j < k; j++) {
|
||||
const t = j / k, t2 = t * t, t3 = t2 * t;
|
||||
out.push([
|
||||
0.5 * ((2 * p1[0]) + (-p0[0] + p2[0]) * t + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3),
|
||||
0.5 * ((2 * p1[1]) + (-p0[1] + p2[1]) * t + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3),
|
||||
]);
|
||||
}
|
||||
}
|
||||
out.push([...pts[pts.length - 1]]);
|
||||
return out.map(p => [Math.round(p[0]), Math.round(p[1])]);
|
||||
}
|
||||
|
||||
// ---------- Speichern ----------
|
||||
async function save() {
|
||||
// bestehende Overrides anderer Karten erhalten
|
||||
let all = {};
|
||||
try {
|
||||
const r = await fetch('js/maps-custom.json', { cache: 'no-store' });
|
||||
if (r.ok) all = await r.json();
|
||||
} catch { /* neu anlegen */ }
|
||||
const routesOut = {};
|
||||
for (const [rk, pts] of Object.entries(edit.routes)) {
|
||||
if (pts.length >= 2) routesOut[rk] = smoothCurve(pts);
|
||||
}
|
||||
all[mapId] = {
|
||||
routes: routesOut, // geglättet – fürs Spiel
|
||||
ctrl: JSON.parse(JSON.stringify(edit.routes)), // Kontrollpunkte – für den Editor
|
||||
slots: edit.slots,
|
||||
};
|
||||
try {
|
||||
const res = await fetch('api/save_map.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(all),
|
||||
});
|
||||
const j = res.ok ? await res.json() : { ok: false };
|
||||
if (j.ok) { persistLocal(); setStatus('✅ Gespeichert (+ lokal gesichert) – Spiel neu laden.'); return; }
|
||||
throw new Error('kein PHP');
|
||||
} catch {
|
||||
// Fallback ohne PHP: JSON zum manuellen Speichern anzeigen
|
||||
const out = $('out');
|
||||
out.style.display = 'block';
|
||||
out.value = JSON.stringify(all, null, 1);
|
||||
out.select();
|
||||
setStatus('⚠️ Kein PHP – JSON manuell als js/maps-custom.json speichern.');
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(msg) { $('status').textContent = msg; }
|
||||
|
||||
// ---------- Kamera / Koordinaten ----------
|
||||
function fitCamera() {
|
||||
const def = MAPS[mapId];
|
||||
cam.scale = Math.min(innerWidth / def.w, innerHeight / def.h) * 0.95;
|
||||
cam.x = (innerWidth - def.w * cam.scale) / 2;
|
||||
cam.y = (innerHeight - def.h * cam.scale) / 2;
|
||||
}
|
||||
const toWorld = (sx, sy) => ({ x: (sx - cam.x) / cam.scale, y: (sy - cam.y) / cam.scale });
|
||||
|
||||
// ---------- Interaktion ----------
|
||||
function hitPoint(w) {
|
||||
const pts = edit.routes[routeId] || [];
|
||||
const r = 14 / cam.scale;
|
||||
for (let i = pts.length - 1; i >= 0; i--) {
|
||||
if (Math.hypot(pts[i][0] - w.x, pts[i][1] - w.y) < r) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function hitSlot(w) {
|
||||
const r = 26 / cam.scale + 14;
|
||||
for (let i = edit.slots.length - 1; i >= 0; i--) {
|
||||
if (Math.hypot(edit.slots[i].x - w.x, edit.slots[i].y - w.y) < r) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const pointers = new Map();
|
||||
cv.addEventListener('pointerdown', e => {
|
||||
cv.setPointerCapture(e.pointerId);
|
||||
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
if (e.button === 2 || pointers.size === 2) { drag = { kind: 'pan' }; return; }
|
||||
const w = toWorld(e.clientX, e.clientY);
|
||||
if (mode === 'path') {
|
||||
const idx = hitPoint(w);
|
||||
drag = idx >= 0 ? { kind: 'point', idx, moved: 0 } : { kind: 'maybeAdd', moved: 0 };
|
||||
} else {
|
||||
const idx = hitSlot(w);
|
||||
drag = idx >= 0 ? { kind: 'slot', idx, moved: 0 } : { kind: 'maybeAdd', moved: 0 };
|
||||
}
|
||||
});
|
||||
cv.addEventListener('pointermove', e => {
|
||||
const p = pointers.get(e.pointerId);
|
||||
if (!p || !drag) return;
|
||||
const dx = e.clientX - p.x, dy = e.clientY - p.y;
|
||||
p.x = e.clientX; p.y = e.clientY;
|
||||
drag.moved = (drag.moved || 0) + Math.abs(dx) + Math.abs(dy);
|
||||
if (drag.kind === 'pan' || (drag.kind === 'maybeAdd' && drag.moved > 8)) {
|
||||
if (drag.kind === 'maybeAdd') drag.kind = 'pan';
|
||||
cam.x += dx; cam.y += dy;
|
||||
} else if (drag.kind === 'point' && drag.moved > 3) {
|
||||
if (!drag.snap) { snapshot(); drag.snap = true; }
|
||||
const w = toWorld(e.clientX, e.clientY);
|
||||
edit.routes[routeId][drag.idx] = [Math.round(w.x), Math.round(w.y)];
|
||||
} else if (drag.kind === 'slot' && drag.moved > 3) {
|
||||
if (!drag.snap) { snapshot(); drag.snap = true; }
|
||||
const w = toWorld(e.clientX, e.clientY);
|
||||
edit.slots[drag.idx].x = Math.round(w.x);
|
||||
edit.slots[drag.idx].y = Math.round(w.y);
|
||||
}
|
||||
});
|
||||
cv.addEventListener('pointerup', e => {
|
||||
pointers.delete(e.pointerId);
|
||||
if (drag?.kind === 'maybeAdd' && (drag.moved || 0) < 8) {
|
||||
const w = toWorld(e.clientX, e.clientY);
|
||||
snapshot();
|
||||
if (mode === 'path') {
|
||||
// an das nähere Ende anfügen bzw. in das nächste Segment einfügen
|
||||
const pts = edit.routes[routeId];
|
||||
insertPoint(pts, [Math.round(w.x), Math.round(w.y)]);
|
||||
} else {
|
||||
edit.slots.push({ x: Math.round(w.x), y: Math.round(w.y), zone: $('zoneSel').value });
|
||||
}
|
||||
}
|
||||
if (drag && drag.kind !== 'pan') persistLocal(); // jede Setzung/Bewegung sichern
|
||||
drag = null;
|
||||
});
|
||||
cv.addEventListener('dblclick', e => {
|
||||
const w = toWorld(e.clientX, e.clientY);
|
||||
if (mode === 'path') {
|
||||
const idx = hitPoint(w);
|
||||
if (idx >= 0 && edit.routes[routeId].length > 2) { snapshot(); edit.routes[routeId].splice(idx, 1); persistLocal(); }
|
||||
} else {
|
||||
const idx = hitSlot(w);
|
||||
if (idx >= 0) { snapshot(); edit.slots.splice(idx, 1); persistLocal(); }
|
||||
}
|
||||
});
|
||||
cv.addEventListener('contextmenu', e => e.preventDefault());
|
||||
cv.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
const f = e.deltaY < 0 ? 1.12 : 0.89;
|
||||
const next = Math.min(3, Math.max(0.25, cam.scale * f));
|
||||
const real = next / cam.scale;
|
||||
cam.x = e.clientX - (e.clientX - cam.x) * real;
|
||||
cam.y = e.clientY - (e.clientY - cam.y) * real;
|
||||
cam.scale = next;
|
||||
}, { passive: false });
|
||||
|
||||
// neuen Punkt sinnvoll einfügen: ans nähere Ende oder mitten ins nächste Segment
|
||||
function insertPoint(pts, p) {
|
||||
if (pts.length < 2) { pts.push(p); return; }
|
||||
let bestSeg = -1, bestD = 40; // nur einfügen, wenn nah an einem Segment
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const d = distToSegment(p, pts[i], pts[i + 1]);
|
||||
if (d < bestD) { bestD = d; bestSeg = i; }
|
||||
}
|
||||
if (bestSeg >= 0) { pts.splice(bestSeg + 1, 0, p); return; }
|
||||
const dStart = Math.hypot(p[0] - pts[0][0], p[1] - pts[0][1]);
|
||||
const dEnd = Math.hypot(p[0] - pts[pts.length - 1][0], p[1] - pts[pts.length - 1][1]);
|
||||
if (dStart < dEnd) pts.unshift(p); else pts.push(p);
|
||||
}
|
||||
function distToSegment(p, a, b) {
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1];
|
||||
const L2 = dx * dx + dy * dy || 1;
|
||||
const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L2));
|
||||
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
||||
}
|
||||
|
||||
// ---------- UI ----------
|
||||
for (const id of MAP_ORDER) {
|
||||
const o = document.createElement('option');
|
||||
o.value = id;
|
||||
o.textContent = `${MAPS[id].icon} ${MAPS[id].name}`;
|
||||
$('mapSel').appendChild(o);
|
||||
}
|
||||
$('mapSel').addEventListener('change', () => loadMap($('mapSel').value));
|
||||
function buildRouteSel() {
|
||||
const sel = $('routeSel');
|
||||
sel.innerHTML = '';
|
||||
for (const rk of Object.keys(edit.routes)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = rk;
|
||||
const m = edit.meta[rk];
|
||||
o.textContent = `${rk} (${m.entry} → ${m.exit})`;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = routeId;
|
||||
}
|
||||
$('routeSel').addEventListener('change', () => { routeId = $('routeSel').value; });
|
||||
$('modePath').addEventListener('click', () => setMode('path'));
|
||||
$('modeSlot').addEventListener('click', () => setMode('slot'));
|
||||
function setMode(m2) {
|
||||
mode = m2;
|
||||
$('modePath').classList.toggle('active', m2 === 'path');
|
||||
$('modeSlot').classList.toggle('active', m2 === 'slot');
|
||||
$('pathTools').style.display = m2 === 'path' ? '' : 'none';
|
||||
$('slotTools').style.display = m2 === 'slot' ? '' : 'none';
|
||||
}
|
||||
$('undoBtn').addEventListener('click', undo);
|
||||
$('clearBtn').addEventListener('click', () => {
|
||||
if (mode === 'path') { snapshot(); edit.routes[routeId] = []; }
|
||||
else { snapshot(); edit.slots = []; }
|
||||
persistLocal();
|
||||
});
|
||||
// „Original laden" – lokalen Stand dieser Karte verwerfen und Defaults holen
|
||||
$('resetBtn')?.addEventListener('click', () => {
|
||||
if (!confirm('Lokalen Bearbeitungsstand dieser Karte verwerfen und die '
|
||||
+ 'gespeicherte/eingebaute Version laden?')) return;
|
||||
try { localStorage.removeItem(LKEY(mapId)); } catch {}
|
||||
loadMap(mapId);
|
||||
});
|
||||
$('saveBtn').addEventListener('click', save);
|
||||
window.addEventListener('resize', resize);
|
||||
function resize() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
cv.width = innerWidth * dpr;
|
||||
cv.height = innerHeight * dpr;
|
||||
}
|
||||
resize();
|
||||
|
||||
// ---------- Zeichnen ----------
|
||||
function draw() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, innerWidth, innerHeight);
|
||||
ctx.fillStyle = '#e7dcc4';
|
||||
ctx.fillRect(0, 0, innerWidth, innerHeight);
|
||||
if (!edit) { requestAnimationFrame(draw); return; }
|
||||
ctx.save();
|
||||
ctx.translate(cam.x, cam.y);
|
||||
ctx.scale(cam.scale, cam.scale);
|
||||
const def = MAPS[mapId];
|
||||
if (mapImg.complete && mapImg.naturalWidth) ctx.drawImage(mapImg, 0, 0, def.w, def.h);
|
||||
|
||||
// Slots
|
||||
for (let i = 0; i < edit.slots.length; i++) {
|
||||
const s = edit.slots[i];
|
||||
if (slotImg.complete && slotImg.naturalWidth) {
|
||||
const w = 78;
|
||||
const h = w * (slotImg.naturalHeight / slotImg.naturalWidth);
|
||||
ctx.drawImage(slotImg, s.x - w / 2, s.y - h / 2, w, h);
|
||||
}
|
||||
ctx.strokeStyle = ZONE_COLORS[s.zone] || '#2f6ea0';
|
||||
ctx.lineWidth = mode === 'slot' ? 3 : 1.5;
|
||||
ctx.setLineDash([6, 5]);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(s.x, s.y, 34, 21, 0, 0, 7);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.fillStyle = ZONE_COLORS[s.zone] || '#2f6ea0';
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(s.zone, s.x, s.y - 26);
|
||||
}
|
||||
|
||||
// Routen: andere blass, aktive mit Punkten + geglätteter Kurve
|
||||
for (const [rk, pts] of Object.entries(edit.routes)) {
|
||||
if (pts.length < 2) continue;
|
||||
const active = rk === routeId && mode === 'path';
|
||||
const sm = smoothCurve(pts);
|
||||
ctx.strokeStyle = active ? 'rgba(232,110,20,0.95)' : 'rgba(47,62,70,0.30)';
|
||||
ctx.lineWidth = active ? 5 : 3;
|
||||
ctx.beginPath();
|
||||
sm.forEach((p, i) => (i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])));
|
||||
ctx.stroke();
|
||||
if (active) {
|
||||
// Richtungspfeile
|
||||
ctx.fillStyle = 'rgba(232,110,20,0.95)';
|
||||
for (let i = 20; i < sm.length; i += 40) {
|
||||
const a = Math.atan2(sm[i][1] - sm[i - 1][1], sm[i][0] - sm[i - 1][0]);
|
||||
ctx.save();
|
||||
ctx.translate(sm[i][0], sm[i][1]);
|
||||
ctx.rotate(a);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(8, 0); ctx.lineTo(-4, -6); ctx.lineTo(-4, 6);
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
// Kontrollpunkte
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
ctx.fillStyle = i === 0 ? '#2e8b2e' : i === pts.length - 1 ? '#c0392b' : '#fff';
|
||||
ctx.strokeStyle = '#2f3e46';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(pts[i][0], pts[i][1], 9, 0, 7);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
loadMap(mapId);
|
||||
requestAnimationFrame(draw);
|
||||
Reference in New Issue
Block a user