Files
geograsim/App/sims/tourismustal/js/editor-tune.js
T
Adminator 250532f9a8 Tourismustal: Editor v2 (Ziehen statt Slider, Solar pro Stufe) + Bikepark-Vorschau
- Editor: Zieh-Modi direkt auf der Karte (Gebaeude: horizontal=Breite,
  vertikal=Boden-Anker; Solar: frei ziehen) + Pixel-Feinjustage per
  Plus/Minus-Tasten, grosse Live-Wertanzeige, Footprint-Hilfslinien.
- Solar-Position wird PRO AUSBAUSTUFE gespeichert (<key>@L<stufe>,
  Vererbung Stufe -> Sprite -> Default) - Renderer-Lookup erweitert.
- Bikepark zeigt beim Bauen die komplette Strecke talwaerts (5 Felder,
  pro Kachel gruen/rot je nach Belegbarkeit) - wie die Lift-Piste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:43:32 +02:00

262 lines
8.5 KiB
JavaScript

// Sprite-Editor v2: Gebäude & Solarpanel direkt auf der Karte ZIEHEN.
// - 🏠-Modus: horizontal ziehen = Sprite-Breite, vertikal = Boden-Anker
// - ☀️-Modus: Solarpanel frei ziehen (Position wird PRO AUSBAUSTUFE gespeichert)
// - ±-Tasten für pixelgenaue Feinjustage
// Werte wirken live (localStorage tt_sprite_tuning); der Export wird von
// Atlas fest in iso.js übernommen.
import { createState, place } from './model.js';
import { IsoRenderer, SPRITE_DEFS, SOLAR_POS } from './iso.js';
import { BUILDINGS } from './data.js';
const $ = id => document.getElementById(id);
// ---------- Test-Welt ----------
const state = createState('boom');
state.stageRow = 0;
state.season = 'Sommer';
state.money = 9e9;
state.guestsToday = 0;
for (const row of state.map) {
for (const t of row) {
t.building = null; t.level = null;
delete t.buildingRef; delete t.mega; delete t.eco; delete t.bikeTrail;
}
}
state.buildings = []; state.pistes = []; state.trails = [];
let SPOT = { c: 18, r: 22 };
outer:
for (let r = 18; r < 30; r++) {
for (let c = 10; c < 28; c++) {
const ok = [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [0, 3], [0, 4], [0, 5]]
.every(([dc, dr]) => {
const t = state.map[r + dr]?.[c + dc];
return t && !t.road && !['water', 'village', 'rock'].includes(t.terrain)
&& t.elev >= 1 && t.elev <= 4;
});
if (ok) { SPOT = { c, r }; break outer; }
}
}
const renderer = new IsoRenderer(document.getElementById('map'), state, { onTileTap: () => {} });
// ---------- Tuning-Store ----------
let tuned = { sprites: {}, solar: {} };
try { tuned = Object.assign(tuned, JSON.parse(localStorage.getItem('tt_sprite_tuning') || '{}')); } catch {}
function saveTuning() {
localStorage.setItem('tt_sprite_tuning', JSON.stringify(tuned));
$('edOut').value = JSON.stringify(tuned, null, 2);
}
// ---------- Auswahl ----------
const sel = $('edType');
for (const [type, def] of Object.entries(BUILDINGS)) {
if (def.action) continue;
const o = document.createElement('option');
o.value = type;
o.textContent = `${def.emoji} ${def.name}`;
sel.appendChild(o);
}
let curLevel = 1;
let dragMode = 'pan';
function spriteKey() {
const type = sel.value;
if ($('edMega').checked && SPRITE_DEFS[`mega_${type}`]) return `mega_${type}`;
if (type === 'hotel' && curLevel >= 2) return 'hotel2';
return type;
}
function solarKey() { return `${spriteKey()}@L${curLevel}`; }
// Aktuelle Solar-Position (mit Vererbung Stufe → Sprite → Default)
function solarPos() {
const k = solarKey();
if (!SOLAR_POS[k]) {
const base = SOLAR_POS[spriteKey()]
|| (($('edMega').checked) ? SOLAR_POS.defaultBig : SOLAR_POS.default);
SOLAR_POS[k] = { dx: base.dx ?? 0, dy: base.dy ?? -27, s: base.s ?? 1 };
}
return SOLAR_POS[k];
}
function spriteDef() {
const k = spriteKey();
if (!SPRITE_DEFS[k]) SPRITE_DEFS[k] = { w: 60, bottom: 8 };
return SPRITE_DEFS[k];
}
// ---------- Bühne ----------
function rebuild() {
for (const row of state.map) {
for (const t of row) {
t.building = null; t.level = null;
delete t.buildingRef; delete t.mega; delete t.eco; delete t.bikeTrail;
}
}
state.buildings = []; state.pistes = []; state.trails = [];
const type = sel.value;
const mega = $('edMega').checked && (BUILDINGS[type].size || 1) === 1;
const { c, r } = SPOT;
if (mega) {
place(state, type, c, r); place(state, type, c + 1, r);
place(state, type, c, r + 1); place(state, type, c + 1, r + 1);
let b = state.buildings.find(x => x.mega);
if (!b && state.buildings.length) {
b = state.buildings[0];
state.buildings = [b];
b.size = 2; b.mega = true;
for (let dr = 0; dr < 2; dr++) for (let dc = 0; dc < 2; dc++) {
const t = state.map[r + dr][c + dc];
t.building = type;
if (dr || dc) t.buildingRef = { c, r };
else { delete t.buildingRef; t.mega = true; }
}
}
} else {
place(state, type, c, r);
}
const b = state.buildings[0];
if (b) {
b.level = curLevel;
state.map[b.r][b.c].level = curLevel;
if ($('edEco').checked) { b.eco = true; state.map[b.r][b.c].eco = true; }
}
syncVals();
}
// ---------- Werte-Anzeige ----------
function syncVals() {
const sd = spriteDef();
const sp = solarPos();
$('vW').textContent = Math.round(sd.w);
$('vB').textContent = Math.round(sd.bottom);
$('vSX').textContent = Math.round(sp.dx);
$('vSY').textContent = Math.round(sp.dy);
$('vSS').textContent = (sp.s ?? 1).toFixed(2);
$('edKey').textContent = `${spriteKey()} · Solar ${solarKey()}`;
}
function persist() {
const sk = spriteKey();
tuned.sprites[sk] = { w: Math.round(SPRITE_DEFS[sk].w), bottom: Math.round(SPRITE_DEFS[sk].bottom) };
const lk = solarKey();
const sp = SOLAR_POS[lk];
tuned.solar[lk] = { dx: Math.round(sp.dx), dy: Math.round(sp.dy), s: +(sp.s ?? 1).toFixed(2) };
saveTuning();
syncVals();
}
// ---------- ±-Feinjustage ----------
document.querySelectorAll('.ed-val .pm button').forEach(btn => {
btn.addEventListener('click', () => {
const d = +btn.dataset.d;
const sd = spriteDef(); const sp = solarPos();
switch (btn.dataset.t) {
case 'w': sd.w += d; break;
case 'b': sd.bottom += d; break;
case 'sx': sp.dx += d; break;
case 'sy': sp.dy += d; break;
case 'ss': sp.s = Math.max(0.3, +(sp.s + d * 0.05).toFixed(2)); break;
}
persist();
});
});
// ---------- Zieh-Modi ----------
const dragLayer = $('dragLayer');
document.querySelectorAll('#edModes button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#edModes button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
dragMode = btn.dataset.mode;
dragLayer.classList.toggle('on', dragMode !== 'pan');
});
});
let dragging = false, lastX = 0, lastY = 0;
dragLayer.addEventListener('pointerdown', e => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
dragLayer.setPointerCapture(e.pointerId);
});
dragLayer.addEventListener('pointermove', e => {
if (!dragging) return;
const dx = (e.clientX - lastX) / renderer.cam.scale;
const dy = (e.clientY - lastY) / renderer.cam.scale;
lastX = e.clientX; lastY = e.clientY;
if (dragMode === 'sprite') {
const sd = spriteDef();
sd.w = Math.max(20, sd.w + dx * 1.5); // horizontal = Breite
sd.bottom = sd.bottom + dy; // vertikal = Boden-Anker
} else if (dragMode === 'solar') {
const sp = solarPos();
sp.dx += dx; sp.dy += dy;
}
persist();
});
dragLayer.addEventListener('pointerup', () => { dragging = false; });
dragLayer.addEventListener('pointercancel', () => { dragging = false; });
// ---------- Auswahl-Events ----------
sel.addEventListener('change', rebuild);
$('edMega').addEventListener('change', rebuild);
$('edEco').addEventListener('change', rebuild);
document.querySelectorAll('#edLevels button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#edLevels button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
curLevel = +btn.dataset.lvl;
rebuild();
});
});
$('edReset').addEventListener('click', () => {
delete tuned.sprites[spriteKey()];
delete tuned.solar[solarKey()];
saveTuning();
location.reload();
});
$('edCopy').addEventListener('click', async () => {
$('edOut').value = JSON.stringify(tuned, null, 2);
try {
await navigator.clipboard.writeText($('edOut').value);
$('edCopy').textContent = '✅ kopiert!';
} catch {
$('edOut').select();
$('edCopy').textContent = '⬇︎ unten markiert';
}
setTimeout(() => { $('edCopy').textContent = '📋 Export kopieren'; }, 1800);
});
// ---------- Loop ----------
rebuild();
$('edOut').value = JSON.stringify(tuned, null, 2);
let last = performance.now();
function frame(now) {
const dt = now - last; last = now;
renderer.draw(now, dt, 1);
// Anker-Hilfe: Footprint-Raute(n) der Bühne nachziehen
const ctx = renderer.ctx;
ctx.save();
ctx.translate(renderer.cam.x, renderer.cam.y);
ctx.scale(renderer.cam.scale, renderer.cam.scale);
const b = state.buildings[0];
if (b) {
const size = b.size || 1;
ctx.strokeStyle = 'rgba(232,137,43,0.9)';
ctx.lineWidth = 1.6;
ctx.setLineDash([5, 4]);
for (let dr = 0; dr < size; dr++) {
for (let dc = 0; dc < size; dc++) {
const t = state.map[b.r + dr][b.c + dc];
const p = renderer.tileToWorld(b.c + dc, b.r + dr, t.elev);
renderer._diamond(p.x, p.y);
ctx.stroke();
}
}
ctx.setLineDash([]);
ctx.lineWidth = 1;
}
ctx.restore();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);