diff --git a/App/sims/tourismustal/js/iso.js b/App/sims/tourismustal/js/iso.js index d47b214..4ddfd1c 100644 --- a/App/sims/tourismustal/js/iso.js +++ b/App/sims/tourismustal/js/iso.js @@ -448,15 +448,23 @@ export class IsoRenderer { if (!ct || ct.terrain === 'water' || !liftSkiable(s, ct.elev)) { g.mode = 'walk'; g.route = []; g.nextLift = null; g.longDwell = false; g.tx = g.x + (Math.random() - 0.5) * 2; g.ty = g.y + 1.5; + this._say(g, '#*!', 4000); // schimpft: Schnee zu Ende, Ski getragen! } } if (g.mode === 'crashed') { g.moving = false; continue; } // liegt, wartet auf Bergung if (g.mode === 'dwell') { // verweilen (essen, schauen) - g.wait -= dt; - g.moving = false; - if (g.wait <= 0) this._nextActivity(g); - continue; + // Niemand steht im Wasser herum: sofort ans Ufer weitergehen + const wt = s.map[Math.round(g.y)]?.[Math.round(g.x)]; + if (wt?.terrain === 'water') { + const d = this._randomDestination(); + g.mode = 'walk'; g.longDwell = false; g.tx = d.c; g.ty = d.r; + } else { + g.wait -= dt; + g.moving = false; + if (g.wait <= 0) this._nextActivity(g); + continue; + } } if (g.mode === 'ride') { // sitzt sichtbar im Sessel @@ -1664,17 +1672,18 @@ export class IsoRenderer { _drawBuilding(type, x, y, tile) { const ctx = this.ctx; const level = tile.level || 1; - // 2×2-Gebäude: zentriert über dem Areal zeichnen (Anker = links oben) - if ((BUILDINGS[type]?.size || 1) > 1) { - const half = (BUILDINGS[type].size - 1) / 2; - const ctr = this.tileToWorld(tile.c + half, tile.r + half, tile.elev); + // 2×2-Gebäude (Bikepark oder Megabauwerk): zentriert über dem Areal + const isBig = (BUILDINGS[type]?.size || 1) > 1 || tile.mega; + if (isBig) { + const ctr = this.tileToWorld(tile.c + 0.5, tile.r + 0.5, tile.elev); x = ctr.x; y = ctr.y; } // KI-Sprites für Gebäude (Bahnen & Rodelbahn bleiben Vektor: dynamisch) const spriteName = type === 'hotel' && level >= 2 ? 'hotel2' : type; - const spriteScale = type === 'hotel' + let spriteScale = type === 'hotel' ? (level >= 3 ? 1.15 : 1) : [1, 1.15, 1.3][Math.min(2, level - 1)]; + if (tile.mega) spriteScale *= 1.75; // Megabauwerk füllt sein 2×2-Areal if (type !== 'lift' && type !== 'gondola' && type !== 'sled' && this._sprite(spriteName, x, y, spriteScale)) { this._badge(x + 18, y - 34, level); diff --git a/App/sims/tourismustal/js/main.js b/App/sims/tourismustal/js/main.js index dd23881..867f1b6 100644 --- a/App/sims/tourismustal/js/main.js +++ b/App/sims/tourismustal/js/main.js @@ -128,12 +128,29 @@ function onTileTap(c, r) { telemetry.log('build_rejected', { type, c, r, reason: check.reason }); return; } - place(state, type, c, r); + const res = place(state, type, c, r); ui.toast(`✅ ${BUILDINGS[type].name} gebaut.`); ui.clearBuildSelection(); // Baumodus verlassen → sicher navigieren audio.build(); ui.updateKPIs(); telemetry.log('build_placed', { type, c, r, moneyLeft: state.money }); + // 4 gleiche im Quadrat → Megabauwerk (Skaleneffekt-Lernmoment) + if (res.merged) { + audio.milestone?.(); + renderer.addFloat('🏛️ Megabauwerk!', '#e8892b', res.merged.c + 1, res.merged.r - 0.5, true); + const first = !state.firedEvents.has('mega-info'); + state.firedEvents.add('mega-info'); + ui.showCard({ + id: `mega-${res.merged.c}-${res.merged.r}`, icon: '🏛️', sound: 'milestone', + title: `Megabauwerk: ${res.merged.name}!`, + text: `Vier gleiche Gebäude im Quadrat sind zu EINEM großen ${res.merged.name} verschmolzen.` + + '\n\nDas ist ein Skaleneffekt: Das Megabauwerk leistet so viel wie ' + + 'alle vier zusammen – braucht aber nur die Betriebskosten von DREI. ' + + 'Große Betriebe arbeiten pro Gast günstiger als kleine.' + + (first ? '\n\nTipp: Das klappt mit vielen Gebäudetypen – probiere es aus!' : ''), + }); + telemetry.log('mega_merge', { type, c: res.merged.c, r: res.merged.r }); + } // Lernprogramm: beim ERSTEN Bau jedes Typs erklärt eine Karte die Wirkung const infoId = `bi-${type}`; if (BUILD_INFO[type] && !state.firedEvents.has(infoId)) { diff --git a/App/sims/tourismustal/js/model.js b/App/sims/tourismustal/js/model.js index f18b1bb..9d57648 100644 --- a/App/sims/tourismustal/js/model.js +++ b/App/sims/tourismustal/js/model.js @@ -345,7 +345,9 @@ export function place(state, type, c, r) { } state.nature = Math.max(15, state.nature - 2); } - return { wasForest }; + // 4 gleiche im Quadrat? → Megabauwerk (Skaleneffekt) + const merged = tryMerge(state, c, r); + return { wasForest, merged }; } export function demolish(state, c, r) { @@ -354,8 +356,10 @@ export function demolish(state, c, r) { // Klick auf eine Teilfläche eines großen Gebäudes → auf den Anker umlenken if (tile.buildingRef) { ({ c, r } = tile.buildingRef); tile = state.map[r][c]; } const def = BUILDINGS[tile.building]; - const size = def.size || 1; - const refund = Math.round(def.cost * 0.3); + const entry = state.buildings.find(b => b.c === c && b.r === r); + const size = entry?.size || def.size || 1; + // Mega = 4 verschmolzene Gebäude → 4-facher Bauwert als Abriss-Basis + const refund = Math.round(def.cost * (entry?.mega ? 4 : 1) * 0.3); state.money += refund; state.nature = Math.min(100, state.nature + 1); state.buildings = state.buildings.filter(b => !(b.c === c && b.r === r)); @@ -369,6 +373,7 @@ export function demolish(state, c, r) { t.building = null; t.level = null; delete t.buildingRef; + delete t.mega; } } return { ok: true, refund, name }; @@ -380,7 +385,44 @@ export const UPGRADABLE = Object.keys(BUILDINGS); function effVal(b, key) { const v = BUILDINGS[b.type][key] || 0; - return Math.round(v * (1 + 0.5 * ((b.level || 1) - 1))); + let val = v * (1 + 0.5 * ((b.level || 1) - 1)); + // Megabauwerk (4 verschmolzene Gebäude): 4-fache Leistung, aber nur + // 3-fache Betriebskosten – Skaleneffekt („economies of scale"). + if (b.mega) val *= (key === 'upkeep' || key === 'staff') ? 3 : 4; + return Math.round(val); +} + +// Verschmelzen: liegen 4 gleiche 1×1-Gebäude im 2×2-Quadrat, werden sie zu +// EINEM Megabauwerk (size 2, mega). Bahnen, Bus & Bikepark ausgenommen. +const MERGEABLE = ['restaurant', 'hotel', 'staffhouse', 'snow', + 'badesee', 'sled', 'ropes', 'playground', 'bikerental', 'trail']; +function tryMerge(state, c, r) { + const type = state.map[r]?.[c]?.building; + if (!type || !MERGEABLE.includes(type)) return null; + const at = (cc, rr) => state.buildings.find(b => + b.c === cc && b.r === rr && b.type === type + && (b.size || 1) === 1 && !b.mega && !b.underConstruction); + // alle 4 möglichen Quadrate, die (c,r) enthalten + for (const [ac, ar] of [[c, r], [c - 1, r], [c, r - 1], [c - 1, r - 1]]) { + const parts = [at(ac, ar), at(ac + 1, ar), at(ac, ar + 1), at(ac + 1, ar + 1)]; + if (parts.some(p => !p)) continue; + const level = Math.max(...parts.map(p => p.level || 1)); + // drei Teile entfernen, Anker wird Mega + const anchor = parts[0]; + state.buildings = state.buildings.filter(b => b === anchor || !parts.includes(b)); + anchor.size = 2; anchor.mega = true; anchor.level = level; + for (let dr = 0; dr < 2; dr++) { + for (let dc = 0; dc < 2; dc++) { + const t = state.map[ar + dr][ac + dc]; + t.building = type; + t.level = level; + if (dr || dc) t.buildingRef = { c: ac, r: ar }; + else { delete t.buildingRef; t.mega = true; } + } + } + return { type, name: BUILDINGS[type].name, c: ac, r: ar, level }; + } + return null; } function dist(a, b) { @@ -431,6 +473,7 @@ export function buildingReport(state, c, r) { if (def.snowPower) lines.push(`Beschneiung: +${effVal(b, 'snowPower').toFixed(1)} cm/Tag`); if (def.boost) lines.push(`Verstärker: Bikepark & Panoramaweg im Umkreis +${Math.round(def.boost * 100)} % Gäste`); if (def.trailLen) lines.push(`Flowtrails: ${def.trailLen} Abschnitte talwärts`); + if (b.mega) lines.push('🏛️ Skaleneffekt: 4-fache Leistung, aber nur 3-fache Betriebskosten'); // Aktuelle Probleme dieses Gebäudes (erscheinen im Klick-Dialog) const problems = []; @@ -445,7 +488,7 @@ export function buildingReport(state, c, r) { problems.push('💤 Nebensaison: ruht gerade – die laufenden Kosten laufen (reduziert) weiter.'); return { - type: b.type, name: def.name, emoji: def.emoji, level, + type: b.type, name: b.mega ? `Mega-${def.name}` : def.name, emoji: def.emoji, level, alt: b.alt ?? 3, dailyUpkeep, dailyWage, dailyCost: dailyUpkeep + dailyWage,