From 250532f9a8de3e29930860f2db0dd09de670f057 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 11 Jul 2026 23:43:32 +0200 Subject: [PATCH] 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 (@L, 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) --- App/sims/tourismustal/editor.html | 94 +++++++----- App/sims/tourismustal/js/editor-tune.js | 187 +++++++++++++++++------- App/sims/tourismustal/js/iso.js | 25 +++- 3 files changed, 207 insertions(+), 99 deletions(-) diff --git a/App/sims/tourismustal/editor.html b/App/sims/tourismustal/editor.html index f2ce346..b929cfb 100644 --- a/App/sims/tourismustal/editor.html +++ b/App/sims/tourismustal/editor.html @@ -8,37 +8,51 @@ +
-

🛠️ Sprite-Editor

+

🛠️ Sprite-Editor

- +
@@ -47,35 +61,39 @@
- - + +
- -
+ +
+ + + +
+
🏠: horizontal ziehen = Breite, vertikal = Boden-Anker. + ☀️: Panel frei ziehen. Feinjustage: ±-Tasten.
- -
- - -
- - -
- - -
+
+
Breite w
–
+
+
Anker bottom
–
+
+
Solar dx
–
+
+
Solar dy
–
+
+
Solar Größe s
–
+
+
- +
- + -
Werte wirken sofort – auch im echten Spiel auf DIESEM Gerät - (localStorage). Endgültig werden sie erst, wenn Atlas den Export in den Code übernimmt. - Karte: ziehen/zoomen wie im Spiel.
diff --git a/App/sims/tourismustal/js/editor-tune.js b/App/sims/tourismustal/js/editor-tune.js index 2d67cf1..601a965 100644 --- a/App/sims/tourismustal/js/editor-tune.js +++ b/App/sims/tourismustal/js/editor-tune.js @@ -1,7 +1,9 @@ -// Sprite-Editor: Gebäude in allen Ausbaustufen (1×1 und 2×2/Mega) auf echten -// Tiles justieren – Sprite-Breite, Boden-Anker und Solarpanel-Position. -// Werte landen live in localStorage (tt_sprite_tuning) und wirken sofort im -// Spiel auf diesem Gerät; der Export wird von Atlas fest in iso.js übernommen. +// 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'; @@ -9,7 +11,7 @@ import { BUILDINGS } from './data.js'; const $ = id => document.getElementById(id); -// ---------- Test-Welt: flach geräumte Bühne ---------- +// ---------- Test-Welt ---------- const state = createState('boom'); state.stageRow = 0; state.season = 'Sommer'; @@ -18,21 +20,21 @@ 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.buildingRef; delete t.mega; delete t.eco; delete t.bikeTrail; } } state.buildings = []; state.pistes = []; state.trails = []; -// Bühne: ebene 2×2-Fläche nahe der Kartenmitte suchen -let SPOT = { c: 18, r: 24 }; +let SPOT = { c: 18, r: 22 }; outer: -for (let r = 20; r < 34; r++) { +for (let r = 18; r < 30; r++) { for (let c = 10; c < 28; c++) { - const ok = [[0, 0], [1, 0], [0, 1], [1, 1]].every(([dc, dr]) => { - const t = state.map[r + dr]?.[c + dc]; - return t && !t.road && t.terrain !== 'water' && t.terrain !== 'village' - && t.terrain !== 'rock' && t.elev >= 1 && t.elev <= 3; - }); + 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; } } } @@ -50,13 +52,14 @@ function saveTuning() { // ---------- Auswahl ---------- const sel = $('edType'); for (const [type, def] of Object.entries(BUILDINGS)) { - if (def.action) continue; // Aufforsten etc. + 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; @@ -64,23 +67,37 @@ function spriteKey() { if (type === 'hotel' && curLevel >= 2) return 'hotel2'; return type; } +function solarKey() { return `${spriteKey()}@L${curLevel}`; } -// ---------- Bühne neu bestücken ---------- +// 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.buildingRef; delete t.mega; delete t.eco; delete t.bikeTrail; } } state.buildings = []; state.pistes = []; state.trails = []; const type = sel.value; - const def = BUILDINGS[type]; - const mega = $('edMega').checked && (def.size || 1) === 1; + const mega = $('edMega').checked && (BUILDINGS[type].size || 1) === 1; const { c, r } = SPOT; if (mega) { - // 4 gleiche im Quadrat → verschmilzt automatisch (falls merge-fähig), - // sonst von Hand zum 2×2 zusammensetzen. 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); @@ -104,44 +121,80 @@ function rebuild() { state.map[b.r][b.c].level = curLevel; if ($('edEco').checked) { b.eco = true; state.map[b.r][b.c].eco = true; } } - syncSliders(); + syncVals(); } -// ---------- Slider ↔ Werte ---------- -function syncSliders() { - const key = spriteKey(); - const sd = SPRITE_DEFS[key] || { w: 60, bottom: 8 }; - const sp = SOLAR_POS[key] - || ($('edMega').checked || (BUILDINGS[sel.value].size || 1) > 1 - ? SOLAR_POS.defaultBig : SOLAR_POS.default); - $('edW').value = sd.w; $('edWv').textContent = sd.w; - $('edB').value = sd.bottom; $('edBv').textContent = sd.bottom; - $('edSX').value = sp.dx ?? 0; $('edSXv').textContent = sp.dx ?? 0; - $('edSY').value = sp.dy ?? -27; $('edSYv').textContent = sp.dy ?? -27; - $('edSS').value = sp.s ?? 1; $('edSSv').textContent = (sp.s ?? 1).toFixed(2); +// ---------- 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 applySprite() { - const key = spriteKey(); - const w = +$('edW').value, bottom = +$('edB').value; - SPRITE_DEFS[key] = Object.assign(SPRITE_DEFS[key] || {}, { w, bottom }); - tuned.sprites[key] = { w, bottom }; - $('edWv').textContent = w; $('edBv').textContent = bottom; - saveTuning(); -} -function applySolar() { - const key = spriteKey(); - const v = { dx: +$('edSX').value, dy: +$('edSY').value, s: +$('edSS').value }; - SOLAR_POS[key] = v; - tuned.solar[key] = v; - $('edSXv').textContent = v.dx; $('edSYv').textContent = v.dy; - $('edSSv').textContent = v.s.toFixed(2); +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(); } -['edW', 'edB'].forEach(id => $(id).addEventListener('input', applySprite)); -['edSX', 'edSY', 'edSS'].forEach(id => $(id).addEventListener('input', applySolar)); +// ---------- ±-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); @@ -155,11 +208,10 @@ document.querySelectorAll('#edLevels button').forEach(btn => { }); $('edReset').addEventListener('click', () => { - const key = spriteKey(); - delete tuned.sprites[key]; - delete tuned.solar[key]; + delete tuned.sprites[spriteKey()]; + delete tuned.solar[solarKey()]; saveTuning(); - location.reload(); // Original-Werte aus dem Code laden + location.reload(); }); $('edCopy').addEventListener('click', async () => { @@ -181,6 +233,29 @@ 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); diff --git a/App/sims/tourismustal/js/iso.js b/App/sims/tourismustal/js/iso.js index 56c8ae5..a667f26 100644 --- a/App/sims/tourismustal/js/iso.js +++ b/App/sims/tourismustal/js/iso.js @@ -1995,7 +1995,7 @@ export class IsoRenderer { ctx.fill(); } } - if (tile.eco) this._drawSolar(spriteName, x, y, !!tile.mega || (BUILDINGS[type]?.size || 1) > 1); + if (tile.eco) this._drawSolar(spriteName, x, y, !!tile.mega || (BUILDINGS[type]?.size || 1) > 1, level); return; } switch (type) { @@ -2199,14 +2199,16 @@ export class IsoRenderer { break; } } - if (tile.eco) this._drawSolar(type, x, y, !!tile.mega); + if (tile.eco) this._drawSolar(type, x, y, !!tile.mega, level); } // Solardach: Zeichen des Öko-Umbaus – dunkles Panel mit weißen Stegen - // und kleinem Glanzpunkt. Position pro Sprite-Key justierbar (SOLAR_POS). - _drawSolar(key, x, y, big = false) { + // und kleinem Glanzpunkt. Position pro Sprite-Key UND Ausbaustufe + // justierbar: erst "@L", dann "", dann Default. + _drawSolar(key, x, y, big = false, level = 1) { const ctx = this.ctx; - const pos = SOLAR_POS[key] || (big ? SOLAR_POS.defaultBig : SOLAR_POS.default); + const pos = SOLAR_POS[`${key}@L${level}`] || SOLAR_POS[key] + || (big ? SOLAR_POS.defaultBig : SOLAR_POS.default); const s = pos.s ?? 1; ctx.save(); ctx.translate(x + (pos.dx ?? 0), y + (pos.dy ?? 0)); @@ -2371,6 +2373,19 @@ export class IsoRenderer { this.ctx.fillStyle = fill; this.ctx.fill(); } + } else if (def?.trailLen) { + // Bikepark: die ganze reservierte Strecke talwärts zeigen – + // pro Kachel grün/rot, je nachdem ob sie frei ist + for (let i = 1; i <= def.trailLen; i++) { + const tt = this.state.map[t.r + i]?.[t.c]; + if (!tt) break; + const free = !tt.building && !tt.bikeTrail && !tt.road + && !['water', 'rock', 'village'].includes(tt.terrain); + const p = this.tileToWorld(t.c, t.r + i, tt.elev); + this._diamond(p.x, p.y); + this.ctx.fillStyle = free ? fill : 'rgba(200,60,50,0.4)'; + this.ctx.fill(); + } } this._diamond(x, y);