cfdfa4bb28
- Justier-Editor (editor.html): jedes Gebaeude in allen Stufen, 1x1 und Mega-2x2, Oeko an/aus - Sprite-Breite, Boden-Anker und Solarpanel- Position (dx/dy/s pro Sprite-Key) live auf echten Tiles tunen. Werte wirken sofort via localStorage-Override, Export-JSON zum Einbacken. - Bikepark umgebaut aufs Rodelbahn-Prinzip: 1x1-Station, Flowtrail- Strecke 5 Felder talwaerts mit Schwuengen, Platz wird SCHON BEIM BAU geprueft und reserviert (tile.bikeTrail blockiert Bebauung), fahrende Raeder mit Kurvenneigung (mehr Biker je Ausbaustufe), Ziel-Bogen. - Animationen: Spielplatz (Schaukel + Rutscher), Hochseilgarten (Zipline-Gleiter), Aussichtsplattform (Foto-Blitz), Fahrradverleih (drehendes Werkstatt-Rad), Gasthaus (Kamin-Dampf). - Ambiente: Wolkenschatten ziehen uebers Tal; Schafe auf Almen und Kuehe auf Talwiesen (grasen, deterministische Herdenplaetze, nicht im Winter). - Baumenue-Karten: oben Titel+Info links / Preis rechts, darunter zweispaltig Icon und Beschreibung. - Solar-Position pro Gebaeudetyp justierbar (SOLAR_POS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
6.3 KiB
JavaScript
187 lines
6.3 KiB
JavaScript
// 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.
|
||
|
||
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: flach geräumte Bühne ----------
|
||
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;
|
||
}
|
||
}
|
||
state.buildings = []; state.pistes = []; state.trails = [];
|
||
|
||
// Bühne: ebene 2×2-Fläche nahe der Kartenmitte suchen
|
||
let SPOT = { c: 18, r: 24 };
|
||
outer:
|
||
for (let r = 20; r < 34; 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;
|
||
});
|
||
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; // Aufforsten etc.
|
||
const o = document.createElement('option');
|
||
o.value = type;
|
||
o.textContent = `${def.emoji} ${def.name}`;
|
||
sel.appendChild(o);
|
||
}
|
||
let curLevel = 1;
|
||
|
||
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;
|
||
}
|
||
|
||
// ---------- Bühne neu bestücken ----------
|
||
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;
|
||
}
|
||
}
|
||
state.buildings = []; state.pistes = []; state.trails = [];
|
||
const type = sel.value;
|
||
const def = BUILDINGS[type];
|
||
const mega = $('edMega').checked && (def.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);
|
||
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; }
|
||
}
|
||
syncSliders();
|
||
}
|
||
|
||
// ---------- 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);
|
||
}
|
||
|
||
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);
|
||
saveTuning();
|
||
}
|
||
|
||
['edW', 'edB'].forEach(id => $(id).addEventListener('input', applySprite));
|
||
['edSX', 'edSY', 'edSS'].forEach(id => $(id).addEventListener('input', applySolar));
|
||
|
||
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', () => {
|
||
const key = spriteKey();
|
||
delete tuned.sprites[key];
|
||
delete tuned.solar[key];
|
||
saveTuning();
|
||
location.reload(); // Original-Werte aus dem Code laden
|
||
});
|
||
|
||
$('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);
|
||
requestAnimationFrame(frame);
|
||
}
|
||
requestAnimationFrame(frame);
|