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:
@@ -60,8 +60,8 @@ export const BUILDINGS = {
|
||||
},
|
||||
bikepark: {
|
||||
name: 'Bikepark', emoji: '🚵', cost: 10000, upkeep: 80,
|
||||
zone: 'mid', staff: 2, summerCap: 160, trailLen: 5,
|
||||
desc: 'Flowtrails am Hang: bis 160 Sommergäste. Braucht eine Bahn in der Nähe (max. 4 Felder), die die Bikes hinaufbringt.',
|
||||
zone: 'mid', staff: 2, summerCap: 160, trailLen: 5, size: 2,
|
||||
desc: 'Großes Basis-Areal (2×2 Felder) mit Flowtrails: bis 160 Sommergäste. Braucht eine Bahn in der Nähe (max. 4 Felder), die die Bikes hinaufbringt.',
|
||||
},
|
||||
ropes: {
|
||||
name: 'Hochseilgarten', emoji: '🧗', cost: 6500, upkeep: 45,
|
||||
|
||||
@@ -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 15–35 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'
|
||||
|
||||
@@ -11,7 +11,7 @@ import { player } from './player.js';
|
||||
|
||||
window.__ttLoaded = true; // Signal an den file://-Fallback in index.html
|
||||
|
||||
const MS_PER_DAY = 2000; // 1 Tag = 2 s bei Geschwindigkeit 1× – ruhiges Tempo
|
||||
const MS_PER_DAY = 4000; // 1 Tag = 4 s bei 1× – ruhiges Tempo (halbiert 2026-07-11)
|
||||
|
||||
// Ereignis-Pacing: Schonfrist zum Ankommen + Mindestabstand zwischen
|
||||
// Ereignis-/Entscheidungskarten, damit am Anfang nicht mehrere Karten im
|
||||
|
||||
@@ -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.' };
|
||||
|
||||
Reference in New Issue
Block a user