Tourismustal: iPad-Test-Feedback Runde 1

- Bikepark: echtes 2x2-Footprint (canPlace/place/demolish/upgrade/Report
  ankerbasiert, buildingRef auf Teilflaechen), Sprite ohne Schatten neu
  generiert, zentriert gross gezeichnet. Baukriterium gelockert: Bahn-Naehe
  ersetzt die Zonen-Regel (wie Hotel im Bahn-Umkreis).
- Zeit halbiert: 1 Tag = 4 s bei 1x (Absturz-Rate entsprechend angepasst).
- Problem-Sprechblasen alle ~3 min am Gebaeude (Personal gesucht / kein
  Schnee), erster Hinweis nach 15-35 s.
- Heli: sichtbares Abheben/Landen am Pad (aus vorigem Commit) bleibt.
- Test-Harnesse (model-test, strategien) wandern in App/sims/tourismustal/tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:20:06 +02:00
parent 2b4a92e614
commit 16c562ed1d
7 changed files with 276 additions and 22 deletions
+52 -16
View File
@@ -248,20 +248,31 @@ export function canPlace(state, type, c, r) {
const def = BUILDINGS[type];
const tile = state.map[r]?.[c];
if (!def || !tile) return { ok: false, reason: 'Außerhalb der Karte.' };
if (!inCore(c, r)) return { ok: false, reason: 'Hier ist unwegsames Hochgebirge im Tal bauen.' };
if (!state.unlocked.has(type)) return { ok: false, reason: 'Noch nicht freigeschaltet.' };
if (tile.building) return { ok: false, reason: 'Hier steht schon ein Gebäude.' };
if (tile.terrain === 'rock') return { ok: false, reason: 'Auf Fels kann nicht gebaut werden.' };
if (tile.terrain === 'water') return { ok: false, reason: 'Im Fluss kann nicht gebaut werden.' };
if (tile.terrain === 'village') return { ok: false, reason: 'Hier steht das alte Dorf.' };
if (tile.road) return { ok: false, reason: 'Auf der Straße kann nicht gebaut werden.' };
if (r < state.stageRow) return { ok: false, reason: 'Die Hochlagen sind noch nicht erschlossen.' };
if (state.money < def.cost) return { ok: false, reason: 'Nicht genug Geld.' };
// Footprint prüfen: 1×1 Standard, große Gebäude (size 2 = 2×2) brauchen
// jede Teilfläche frei. Anker ist die Kachel links oben (c, r).
const size = def.size || 1;
for (let dr = 0; dr < size; dr++) {
for (let dc = 0; dc < size; dc++) {
const t = state.map[r + dr]?.[c + dc];
const platz = size > 1 ? ` (${def.name} braucht ${size}×${size} freie Felder)` : '';
if (!t) return { ok: false, reason: 'Außerhalb der Karte.' + platz };
if (!inCore(c + dc, r + dr)) return { ok: false, reason: 'Hier ist unwegsames Hochgebirge im Tal bauen.' + platz };
if (t.building) return { ok: false, reason: 'Hier steht schon ein Gebäude.' + platz };
if (t.terrain === 'rock') return { ok: false, reason: 'Auf Fels kann nicht gebaut werden.' + platz };
if (t.terrain === 'water') return { ok: false, reason: 'Im Fluss kann nicht gebaut werden.' + platz };
if (t.terrain === 'village') return { ok: false, reason: 'Hier steht das alte Dorf.' + platz };
if (t.road) return { ok: false, reason: 'Auf der Straße kann nicht gebaut werden.' + platz };
if (r + dr < state.stageRow) return { ok: false, reason: 'Die Hochlagen sind noch nicht erschlossen.' };
}
}
if (def.zone !== 'any') {
const [lo, hi] = ZONE_RANGE[def.zone];
// Lift/Gondel wirken wie eine eigene Basis: in ihrem Umkreis (4 Felder)
// darf auch am Gebirge alles gebaut werden (außer Bus & Bad).
const nearLift = ['lift', 'gondola', 'restaurant', 'hotel', 'staffhouse'].includes(type)
// darf auch am Gebirge alles gebaut werden (außer Bus & Bad). Der
// Bikepark hängt ohnehin an der Bahn Bahn-Nähe ersetzt die Zonen-Regel.
const nearLift = ['lift', 'gondola', 'restaurant', 'hotel', 'staffhouse', 'bikepark'].includes(type)
&& state.buildings.some(b =>
(b.type === 'lift' || b.type === 'gondola') && dist(b, { c, r }) <= 4);
if (!nearLift && (tile.elev < lo || tile.elev > hi)) {
@@ -296,11 +307,21 @@ export function place(state, type, c, r) {
const def = BUILDINGS[type];
const tile = state.map[r][c];
const wasForest = tile.terrain === 'forest';
tile.building = type;
state.buildings.push({ type, c, r, alt: tile.elev });
const size = def.size || 1;
state.buildings.push({ type, c, r, alt: tile.elev, size });
state.money -= def.cost;
// Bauen kostet Naturwert Rodung von Wald deutlich mehr
state.nature = Math.max(15, state.nature - (wasForest ? 5 : 2));
// Alle Teilflächen belegen; Nicht-Anker-Kacheln zeigen auf den Anker
// (buildingRef), damit Klick/Abriss überall auf dem Areal funktioniert.
for (let dr = 0; dr < size; dr++) {
for (let dc = 0; dc < size; dc++) {
const t = state.map[r + dr]?.[c + dc];
if (!t) continue;
t.building = type;
if (dr || dc) t.buildingRef = { c, r };
// Bauen kostet Naturwert Rodung von Wald deutlich mehr
state.nature = Math.max(15, state.nature - (t.terrain === 'forest' ? 5 : 2));
}
}
if (def.pisteLen) {
// Piste entsteht hangaufwärts über der Talstation (Lift & Gondel)
@@ -328,9 +349,12 @@ export function place(state, type, c, r) {
}
export function demolish(state, c, r) {
const tile = state.map[r][c];
let tile = state.map[r][c];
if (!tile.building) return { ok: false, reason: 'Hier steht kein Gebäude.' };
// 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);
state.money += refund;
state.nature = Math.min(100, state.nature + 1);
@@ -338,8 +362,15 @@ export function demolish(state, c, r) {
state.pistes = state.pistes.filter(p => !(p.owner.c === c && p.owner.r === r));
state.trails = state.trails.filter(t => !(t.owner.c === c && t.owner.r === r));
const name = def.name;
tile.building = null;
tile.level = null;
for (let dr = 0; dr < size; dr++) {
for (let dc = 0; dc < size; dc++) {
const t = state.map[r + dr]?.[c + dc];
if (!t) continue;
t.building = null;
t.level = null;
delete t.buildingRef;
}
}
return { ok: true, refund, name };
}
@@ -380,6 +411,9 @@ export function seasonFactor(type, season) {
// Voller Bericht zu EINEM Gebäude: Werte, Auslastung, Tageskosten, Nutzen.
// Grundlage der Inspektions-/Ausbaukarte (Lernprogramm: alles sichtbar).
export function buildingReport(state, c, r) {
// Teilfläche eines großen Gebäudes → Anker
const ref = state.map[r]?.[c]?.buildingRef;
if (ref) ({ c, r } = ref);
const b = state.buildings.find(x => x.c === c && x.r === r);
if (!b) return null;
const def = BUILDINGS[b.type];
@@ -430,6 +464,8 @@ export function upgradeCost(type, level) {
}
export function upgrade(state, c, r) {
const ref = state.map[r]?.[c]?.buildingRef;
if (ref) ({ c, r } = ref);
const tile = state.map[r]?.[c];
const b = state.buildings.find(x => x.c === c && x.r === r);
if (!tile?.building || !b) return { ok: false, reason: 'Hier steht kein Gebäude.' };