Tourismustal: iPad-Feedback Runde 2 (5 Punkte)
- Bau-Ablehnung erklaert jetzt immer auch die Bauregeln des Gebaeudes (Toast: Grund + Beschreibung, 7 s). - Lehrer-Schalter fuer den Vorlese-Modus: class_modules.audio_info (Migration, Default AN) + modules-API set_audio_info + Toggle auf der Modul-Karte im Cockpit (Vorlesen an/aus) + Wrapper injiziert TT_FEATURES.audioInfo + Sim blendet den Umschalter aus und erzwingt Lese-Modus, wenn gesperrt. - Bus zeigt bei Rueckfahrt sein Heck: Heckscheibe + rote Ruecklichter, Tuer/Spiegel richtungsabhaengig. - Bikepark-Flowtrails: 3 Stufen hangAUFwaerts (gleiche Richtung wie Lift-Pisten). - Info-Button an jedem Bauwerk im Baumenue: erklaert Wirkung, Kosten, Lage-Regeln und Lehrplan-Wissen VOR dem Bau. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -459,3 +459,11 @@ button.secondary {
|
||||
#infoBanner .ib-icon { font-size: 20px; }
|
||||
#infoBanner .ib-hint { color: var(--muted); font-size: 12px; margin-left: 4px; white-space: nowrap; }
|
||||
@keyframes ibSlide { from { transform: translate(-50%, -14px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }
|
||||
|
||||
/* ℹ-Button im Baumenü */
|
||||
.build-item { position: relative; }
|
||||
.build-item .bi-menu-info {
|
||||
background: none; border: none; cursor: pointer; font-size: 14px;
|
||||
padding: 4px 6px; opacity: .65; flex-shrink: 0;
|
||||
}
|
||||
.build-item .bi-menu-info:hover { opacity: 1; }
|
||||
|
||||
@@ -60,7 +60,7 @@ export const BUILDINGS = {
|
||||
},
|
||||
bikepark: {
|
||||
name: 'Bikepark', emoji: '🚵', cost: 10000, upkeep: 80,
|
||||
zone: 'mid', staff: 2, summerCap: 160, trailLen: 5, size: 2,
|
||||
zone: 'mid', staff: 2, summerCap: 160, trailLen: 3, 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: {
|
||||
|
||||
@@ -1392,9 +1392,11 @@ export class IsoRenderer {
|
||||
const s = this.state;
|
||||
if (!s.buildings.some(b => b.type === 'access')) return;
|
||||
const ctx = this.ctx;
|
||||
let f = (this.simTime / 26000) % 1;
|
||||
const phase = (this.simTime / 26000) % 1;
|
||||
let f = phase;
|
||||
if (f > 0.5) f = 1 - f;
|
||||
f *= 2;
|
||||
const dir = phase <= 0.5 ? 1 : -1; // 1 = fährt nach rechts unten, −1 = zurück
|
||||
const c = CORE.c0 + 0.5 + f * (CORE.c1 - CORE.c0 - 1);
|
||||
const r = ROAD_ROW;
|
||||
const tile = s.map[r][Math.min(MAP_COLS - 1, Math.round(c))];
|
||||
@@ -1432,26 +1434,39 @@ export class IsoRenderer {
|
||||
ctx.lineTo(...P(b, W, winT)); ctx.lineTo(...P(a, W, winT));
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
// Türlinie
|
||||
// Türlinie (Tür sitzt Richtung Front)
|
||||
ctx.strokeStyle = this._shade(body, -34); ctx.lineWidth = 0.8;
|
||||
ctx.beginPath(); ctx.moveTo(...P(L * 0.06, W)); ctx.lineTo(...P(L * 0.06, W, winB)); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(...P(L * 0.06 * dir, W)); ctx.lineTo(...P(L * 0.06 * dir, W, winB)); ctx.stroke();
|
||||
|
||||
// Front (Fahrtrichtung, +L)
|
||||
// Sichtbares Ende (+L): je nach Fahrtrichtung FRONT oder HECK
|
||||
ctx.fillStyle = body;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...P(L, W)); ctx.lineTo(...P(L, -W));
|
||||
ctx.lineTo(...P(L, -W, H)); ctx.lineTo(...P(L, W, H));
|
||||
ctx.closePath(); ctx.fill();
|
||||
// Windschutzscheibe
|
||||
ctx.fillStyle = 'rgba(223,232,236,0.92)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...P(L, W * 0.7, winB)); ctx.lineTo(...P(L, -W * 0.7, winB));
|
||||
ctx.lineTo(...P(L, -W * 0.7, winT)); ctx.lineTo(...P(L, W * 0.7, winT));
|
||||
ctx.closePath(); ctx.fill();
|
||||
// Scheinwerfer
|
||||
ctx.fillStyle = '#ffe6a0';
|
||||
const hl1 = P(L, W * 0.6, H * 0.3), hl2 = P(L, -W * 0.6, H * 0.3);
|
||||
ctx.beginPath(); ctx.arc(hl1[0], hl1[1], 1.1, 0, 7); ctx.arc(hl2[0], hl2[1], 1.1, 0, 7); ctx.fill();
|
||||
if (dir === 1) {
|
||||
// Windschutzscheibe
|
||||
ctx.fillStyle = 'rgba(223,232,236,0.92)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...P(L, W * 0.7, winB)); ctx.lineTo(...P(L, -W * 0.7, winB));
|
||||
ctx.lineTo(...P(L, -W * 0.7, winT)); ctx.lineTo(...P(L, W * 0.7, winT));
|
||||
ctx.closePath(); ctx.fill();
|
||||
// Scheinwerfer
|
||||
ctx.fillStyle = '#ffe6a0';
|
||||
const hl1 = P(L, W * 0.6, H * 0.3), hl2 = P(L, -W * 0.6, H * 0.3);
|
||||
ctx.beginPath(); ctx.arc(hl1[0], hl1[1], 1.1, 0, 7); ctx.arc(hl2[0], hl2[1], 1.1, 0, 7); ctx.fill();
|
||||
} else {
|
||||
// Heckscheibe (kleiner, höher gesetzt)
|
||||
ctx.fillStyle = 'rgba(223,232,236,0.85)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...P(L, W * 0.55, winB + 1.5)); ctx.lineTo(...P(L, -W * 0.55, winB + 1.5));
|
||||
ctx.lineTo(...P(L, -W * 0.55, winT)); ctx.lineTo(...P(L, W * 0.55, winT));
|
||||
ctx.closePath(); ctx.fill();
|
||||
// rote Rücklichter
|
||||
ctx.fillStyle = '#c0392b';
|
||||
const tl1 = P(L, W * 0.68, H * 0.3), tl2 = P(L, -W * 0.68, H * 0.3);
|
||||
ctx.beginPath(); ctx.arc(tl1[0], tl1[1], 1.2, 0, 7); ctx.arc(tl2[0], tl2[1], 1.2, 0, 7); ctx.fill();
|
||||
}
|
||||
// Stoßstange
|
||||
ctx.strokeStyle = '#2f3e46'; ctx.lineWidth = 1.4;
|
||||
ctx.beginPath(); ctx.moveTo(...P(L, W, H * 0.16)); ctx.lineTo(...P(L, -W, H * 0.16)); ctx.stroke();
|
||||
@@ -1478,18 +1493,19 @@ export class IsoRenderer {
|
||||
ctx.quadraticCurveTo(wp[0], wp[1] - 4.2, wp[0] + 3.7, wp[1] + 1);
|
||||
ctx.stroke();
|
||||
}
|
||||
// Türgriff
|
||||
// Türgriff (Richtung Front)
|
||||
ctx.strokeStyle = this._shade(body, -42); ctx.lineWidth = 1;
|
||||
const gh = P(L * 0.16, W, winB * 0.7);
|
||||
const gh = P(L * 0.16 * dir, W, winB * 0.7);
|
||||
ctx.beginPath(); ctx.moveTo(gh[0] - 2, gh[1]); ctx.lineTo(gh[0] + 2, gh[1]); ctx.stroke();
|
||||
// Außenspiegel vorne
|
||||
ctx.fillStyle = '#2f3e46';
|
||||
const mir = P(L, W * 1.4, H * 0.62);
|
||||
ctx.beginPath(); ctx.arc(mir[0], mir[1], 1.3, 0, 7); ctx.fill();
|
||||
// Rücklicht hinten
|
||||
ctx.fillStyle = '#c0392b';
|
||||
const rl = P(-L, W, H * 0.3);
|
||||
ctx.beginPath(); ctx.arc(rl[0], rl[1], 1.2, 0, 7); ctx.fill();
|
||||
if (dir === 1) {
|
||||
// Außenspiegel vorne + Rücklicht am hinteren (verdeckten) Ende
|
||||
ctx.fillStyle = '#2f3e46';
|
||||
const mir = P(L, W * 1.4, H * 0.62);
|
||||
ctx.beginPath(); ctx.arc(mir[0], mir[1], 1.3, 0, 7); ctx.fill();
|
||||
ctx.fillStyle = '#c0392b';
|
||||
const rl = P(-L, W, H * 0.3);
|
||||
ctx.beginPath(); ctx.arc(rl[0], rl[1], 1.2, 0, 7); ctx.fill();
|
||||
}
|
||||
ctx.lineWidth = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,8 @@ function onTileTap(c, r) {
|
||||
|
||||
const check = canPlace(state, type, c, r);
|
||||
if (!check.ok) {
|
||||
ui.toast(`❌ ${check.reason}`);
|
||||
// Ablehnung erklärt IMMER auch die Bauregeln des Gebäudes
|
||||
ui.toast(`❌ ${check.reason} 💡 ${BUILDINGS[type].name}: ${BUILDINGS[type].desc}`, 7000);
|
||||
audio.error();
|
||||
telemetry.log('build_rejected', { type, c, r, reason: check.reason });
|
||||
return;
|
||||
|
||||
@@ -347,12 +347,13 @@ export function place(state, type, c, r) {
|
||||
}
|
||||
}
|
||||
if (def.trailLen) {
|
||||
// Bike-Trails schlängeln sich talwärts unter der Basisstation (Bikepark).
|
||||
// Sichtbar groß, aber nur 1 Feld Gebäude – und sie kosten extra Natur.
|
||||
// Bike-Trails ziehen sich hangAUFwärts über der Basisstation – gleiche
|
||||
// Richtung wie die Lift-Pisten (die Bahn bringt die Bikes hinauf, die
|
||||
// Trails führen zurück herunter). Sie kosten extra Natur.
|
||||
let cc = c;
|
||||
for (let i = 1; i <= def.trailLen; i++) {
|
||||
cc += (i % 2 ? 1 : -1);
|
||||
const t = state.map[r + i]?.[Math.max(1, Math.min(MAP_COLS - 2, cc))];
|
||||
const t = state.map[r - i]?.[Math.max(1, Math.min(MAP_COLS - 2, cc))];
|
||||
if (t && t.terrain !== 'water' && !t.building) {
|
||||
state.trails.push({ c: t.c, r: t.r, owner: { c, r } });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Auto-Pause), Meldungs-Log, Graphen, Skipass-Regler, Ziele, Toasts,
|
||||
// Onboarding, Szenenwahl und Topbar-Buttons.
|
||||
|
||||
import { BUILDINGS } from './data.js';
|
||||
import { BUILDINGS, BUILD_INFO, KNOWLEDGE } from './data.js';
|
||||
import { seasonOfDay, DAYS_PER_YEAR } from './model.js';
|
||||
import { telemetry } from './telemetry.js';
|
||||
import { audio } from './audio.js';
|
||||
@@ -18,7 +18,11 @@ export class UI {
|
||||
this.cardOpen = false;
|
||||
this.speedBeforeCard = 1;
|
||||
this.collectedCards = [];
|
||||
this.infoMode = localStorage.getItem('tt_infomode') || 'read';
|
||||
// Vorlese-Modus: kann von der Lehrperson pro Klasse gesperrt werden
|
||||
this.audioAllowed = window.TT_FEATURES?.audioInfo !== false;
|
||||
this.infoMode = this.audioAllowed
|
||||
? (localStorage.getItem('tt_infomode') || 'read')
|
||||
: 'read';
|
||||
this.activeBuild = null;
|
||||
this.toastTimer = null;
|
||||
|
||||
@@ -77,11 +81,37 @@ export class UI {
|
||||
el.innerHTML = `
|
||||
<span class="emoji">${emoji}</span>
|
||||
<span class="info"><b>${name}</b><small>${desc}</small></span>
|
||||
<span class="cost">${cost}</span>`;
|
||||
<span class="cost">${cost}</span>
|
||||
<button class="bi-menu-info" title="Alles über: ${name}">ℹ️</button>`;
|
||||
el.addEventListener('click', () => this._toggleBuild(type, el));
|
||||
el.querySelector('.bi-menu-info').addEventListener('click', e => {
|
||||
e.stopPropagation(); // nicht in den Baumodus wechseln
|
||||
audio.click();
|
||||
this.showBuildInfo(type);
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
// ℹ im Baumenü: erklärt ein Bauwerk VOR dem Bau – Wirkung, Kosten,
|
||||
// Lage-Regeln und das Lehrplan-Wissen dazu.
|
||||
showBuildInfo(type) {
|
||||
const def = BUILDINGS[type];
|
||||
const info = BUILD_INFO[type];
|
||||
const know = (KNOWLEDGE[type] || [])
|
||||
.map(k => `📚 ${k.term}: ${k.text}`).join('\n\n');
|
||||
const staffLine = def.staff ? ` + ${def.staff} Angestellte` : '';
|
||||
this.showCard({
|
||||
id: `menuinfo-${type}`, transient: true, forceModal: true,
|
||||
icon: def.emoji, sound: 'none',
|
||||
title: def.name,
|
||||
text: `${def.desc}\n\nBaukosten: ${def.cost.toLocaleString('de-AT')} € · `
|
||||
+ `laufend: ${def.upkeep} €/Tag${staffLine}.`
|
||||
+ (info ? `\n\n${info.text}` : '')
|
||||
+ (know ? `\n\n${know}` : ''),
|
||||
});
|
||||
telemetry.log('menu_info', { type });
|
||||
}
|
||||
|
||||
_toggleBuild(type, el) {
|
||||
audio.click();
|
||||
document.querySelectorAll('.build-item').forEach(i => i.classList.remove('active'));
|
||||
@@ -141,6 +171,7 @@ export class UI {
|
||||
$('playerPanel').classList.toggle('hidden');
|
||||
telemetry.log('player_panel', { open: !$('playerPanel').classList.contains('hidden') });
|
||||
});
|
||||
if (!this.audioAllowed) $('btnInfoMode').classList.add('hidden');
|
||||
$('btnInfoMode').addEventListener('click', e => {
|
||||
audio.click();
|
||||
this.infoMode = this.infoMode === 'audio' ? 'read' : 'audio';
|
||||
|
||||
Reference in New Issue
Block a user