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);