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
+38 -3
View File
@@ -35,7 +35,7 @@ const SPRITE_DEFS = {
access: { w: 52, bottom: 8 },
rescue: { w: 60, bottom: 9 },
snow: { w: 44, bottom: 7 },
bikepark: { w: 58, bottom: 8 },
bikepark: { w: 108, bottom: 14 }, // 2×2-Areal, zentriert gezeichnet
ropes: { w: 58, bottom: 8 },
bikerental: { w: 50, bottom: 8 },
platform: { w: 62, bottom: 9 },
@@ -629,7 +629,8 @@ export class IsoRenderer {
const incident = this.heli || this.guests.some(g => g.mode === 'crashed');
if (!incident) {
for (const g of this.guests) {
if (g.mode === 'ski' && Math.random() < 0.02 * dt) {
// (0.01: Tage sind seit der Zeit-Halbierung doppelt so lang)
if (g.mode === 'ski' && Math.random() < 0.01 * dt) {
g.mode = 'crashed'; g.crashAt = this.simTime;
g.route = []; g.nextLift = null; g.moving = false;
this._say(g, '\u{1F198}', 9e8); // 🆘 bleibt sichtbar
@@ -783,6 +784,32 @@ export class IsoRenderer {
ctx.lineWidth = 1;
}
// Problem-Sprechblasen: Gebäude mit akutem Problem melden sich alle ~3
// Minuten (Echtzeit) mit einer aufsteigenden Blase z. B. „Personal
// gesucht!" damit man Ursachen auch ohne Klick ins Gebäude sieht.
_problemBubbles() {
const s = this.state;
const now = this.time;
if (!this._probNext) this._probNext = {};
const staffGap = s.staffNeeded > s.staffAvailable;
for (const b of s.buildings) {
const def = BUILDINGS[b.type];
let msg = null;
if (staffGap && (def.staff || 0) > 0) msg = '👷 Personal gesucht!';
else if ((b.type === 'lift' || b.type === 'gondola')
&& s.season === 'Winter' && !liftSkiable(s, b.alt ?? 3)) msg = '❄️ Kein Schnee!';
const k = `${b.c},${b.r}`;
if (!msg) { delete this._probNext[k]; continue; }
if (!this._probNext[k]) {
// erster Hinweis nach 1535 s, danach im 3-Minuten-Takt
this._probNext[k] = now + 15000 + Math.random() * 20000;
} else if (now >= this._probNext[k]) {
this._probNext[k] = now + 180000;
this.addFloat(msg, '#c0392b', b.c + 0.5, b.r - 0.4, true);
}
}
}
// Sichtbarer Umbau: ~10 s Bauzeit (Spielzeit), danach wird die neue Stufe
// scharf geschaltet. Läuft für ALLE Baustellen (auch außerhalb des Bildes).
CONSTRUCT_MS = 10000;
@@ -894,6 +921,7 @@ export class IsoRenderer {
this._drawBus();
this._drawFestivals(winter);
this._drawStaffAlerts();
this._problemBubbles();
this._drawConstruction();
this._drawRiders();
this._drawStaff();
@@ -1084,7 +1112,8 @@ export class IsoRenderer {
this._drawTree(x, y, snowy, tile.tint);
}
if (tile.deco) this._drawDeco(tile.deco, x, y);
if (tile.building) this._drawBuilding(tile.building, x, y, tile);
// Große Gebäude (2×2) zeichnet nur die Anker-Kachel zentriert übers Areal
if (tile.building && !tile.buildingRef) this._drawBuilding(tile.building, x, y, tile);
// Noch nicht erschlossene Hochlagen liegen unter Nebel
if (tile.r < this.state.stageRow) {
@@ -1635,6 +1664,12 @@ 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);
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'