Tourismustal: Megabauwerke + Figuren-Feinschliff

- Merge-Mechanik: 4 gleiche 1x1-Gebaeude im Quadrat verschmelzen zum
  2x2-Megabauwerk. Skaleneffekt als Lernmoment: 4-fache Leistung, nur
  3-fache Betriebskosten (Erklaerkarte beim ersten Mal). Level = Maximum
  der Teile, Abriss-Erloes auf 4-fachem Bauwert, Bahnen/Bus/Bikepark
  ausgenommen. Rendering zentriert und 1.75x skaliert.
- Gaeste stehen nicht mehr im Wasser (dwell auf Wasser -> sofort ans Ufer).
- Skifahrer schimpft "#*!", wenn ihm der Schnee unterm Ski ausgeht.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 10:43:06 +02:00
parent 727e82c8fe
commit 2591f37191
3 changed files with 84 additions and 15 deletions
+18 -9
View File
@@ -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);
+18 -1
View File
@@ -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)) {
+48 -5
View File
@@ -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,