Tourismusregion Quick-Fixes: Startflow, Auto-Save, Lift-Talstation, iPad-Responsive

- Startbildschirm: Name/Klasscode-Felder entfernt (Kinder sind über die
  Plattform-Session identifiziert). Klick auf eine Wegkarte startet die
  Partie sofort — separater „Partie starten"-Button entfernt.
- Abschluss: „Ergebnis speichern"-Button entfernt; Speichern passiert schon
  automatisch am Partie-Ende (submitAssessment). Hinweis „automatisch
  gespeichert" ergänzt. btnSave-Listener null-sicher gemacht.
- Seilbahn: Talstation (Baukörper + Satteldach + Umlenkrad) ergänzt — der
  Lift-Anfang wirkte vorher „transparent".
- Responsive/iPad: Panels bekommen Höhen-Deckel + internes Scrollen (decken
  nie den ganzen Schirm zu). Neue Media-Query für iPad-Landscape (901–1200px
  / niedrige Höhe): kompaktere Panels, Graphen nach rechts-unten aus dem
  Info-Panel-Bereich. Sehr flache Screens blenden die Graphen aus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:15:41 +02:00
parent 9937fbf62a
commit db4b75c5e6
5 changed files with 67 additions and 21 deletions
+35
View File
@@ -326,6 +326,19 @@ button.secondary {
} }
.end-feedback p { font-size: 13.5px; line-height: 1.45; } .end-feedback p { font-size: 13.5px; line-height: 1.45; }
/* Panels dürfen NIE den ganzen Bildschirm zudecken (iPad-Fix):
Höhe deckeln + intern scrollen. Gilt für alle Größen. */
#kpis, #buildmenu {
max-height: calc(100dvh - 96px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
#infoPanel {
max-height: 46dvh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Kleinere Screens (iPad Hochformat) */ /* Kleinere Screens (iPad Hochformat) */
@media (max-width: 900px) { @media (max-width: 900px) {
#topbar h1 .subtitle { display: none; } #topbar h1 .subtitle { display: none; }
@@ -337,6 +350,28 @@ button.secondary {
#infoPanel { width: 240px; } #infoPanel { width: 240px; }
} }
/* iPad Landscape & kleine Laptops (9011200px oder niedrige Höhe):
fiel früher durch → Desktop-Panels drängelten auf 820px Höhe und
deckten die Karte zu. Jetzt kompaktere Panels; die zwei Graphen
wandern nach rechts-unten (neben das Info-Panel, nicht darüber). */
@media (min-width: 901px) and (max-width: 1200px), (max-height: 860px) {
#kpis { width: 200px; top: 76px; }
#buildmenu { width: 224px; top: 76px; }
#infoPanel { width: 236px; max-height: 42dvh; }
.graphbox { width: 190px; height: 96px; }
/* Graphen rechts unten, links neben dem Baumenü statt mittig */
#graphSat { left: auto; right: 268px; }
#graphMoney { left: auto; right: 466px; }
#musicPanel { right: 244px; width: 200px; }
}
/* Sehr flach (Landscape-Handy / geteilter iPad-Screen): Graphen ausblenden,
Karte hat Vorrang. */
@media (max-height: 560px) {
.graphbox { display: none; }
#infoPanel { max-height: 60dvh; }
}
/* Musik-Player (rechts oben, links neben dem Baumenü) */ /* Musik-Player (rechts oben, links neben dem Baumenü) */
#musicPanel { #musicPanel {
top: 84px; top: 84px;
+2 -7
View File
@@ -124,13 +124,8 @@
<p class="intro">Besuchergruppen kommen in <b>8 Wellen</b> durch dein Tal. <p class="intro">Besuchergruppen kommen in <b>8 Wellen</b> durch dein Tal.
Baue in den Baupausen die passende Infrastruktur, mach jede Gruppe Baue in den Baupausen die passende Infrastruktur, mach jede Gruppe
glücklich und halte Wirtschaft, Umwelt und Budget im Gleichgewicht.</p> glücklich und halte Wirtschaft, Umwelt und Budget im Gleichgewicht.</p>
<input id="playerName" type="text" maxlength="40" <h3 class="map-title">Wegkarte wählen — tippe eine Karte, dann geht's los</h3>
placeholder="Dein Name (für die Auswertung, optional)" autocomplete="off">
<input id="classCode" type="text" maxlength="20"
placeholder="Klassencode (optional)" autocomplete="off">
<h3 class="map-title">Wegkarte wählen</h3>
<div id="mapList"></div> <div id="mapList"></div>
<button id="btnStart" class="primary">▶ Partie starten (8 Wellen)</button>
<p class="start-hint">Tipp: Gebäude wirken nur auf Gäste in ihrer Reichweite <p class="start-hint">Tipp: Gebäude wirken nur auf Gäste in ihrer Reichweite
baue nah an den Wegen!</p> baue nah an den Wegen!</p>
</div> </div>
@@ -147,8 +142,8 @@
<p id="endFacts" class="end-facts"></p> <p id="endFacts" class="end-facts"></p>
<div id="endFeedback" class="end-feedback"></div> <div id="endFeedback" class="end-feedback"></div>
<div id="endReflect" class="end-reflect"></div> <div id="endReflect" class="end-reflect"></div>
<p class="end-saved">💾 Dein Ergebnis wurde automatisch gespeichert.</p>
<div class="card-buttons"> <div class="card-buttons">
<button id="btnSave" class="primary">💾 Ergebnis speichern</button>
<button id="btnRestart" class="secondary">🔄 Nochmal spielen</button> <button id="btnRestart" class="secondary">🔄 Nochmal spielen</button>
</div> </div>
</div> </div>
+13 -13
View File
@@ -27,16 +27,23 @@ let endShown = false;
// ---------- Start: Kartenwahl + Partie ---------- // ---------- Start: Kartenwahl + Partie ----------
let chosenMap = 'talschleife'; let chosenMap = 'talschleife';
let _starting = false; // Doppel-Start verhindern (rasche Doppelklicks)
const mapList = document.getElementById('mapList'); const mapList = document.getElementById('mapList');
for (const id of MAP_ORDER) { for (const id of MAP_ORDER) {
const mdef = MAPS[id]; const mdef = MAPS[id];
const el = document.createElement('button'); const el = document.createElement('button');
el.className = 'map-btn' + (id === chosenMap ? ' active' : ''); el.className = 'map-btn';
el.dataset.map = id; el.dataset.map = id;
el.innerHTML = `<img src="${mdef.image}" alt=""><span><b>${mdef.icon} ${mdef.name}</b><small>${mdef.desc}</small></span>`; el.innerHTML = `<img src="${mdef.image}" alt=""><span><b>${mdef.icon} ${mdef.name}</b><small>${mdef.desc}</small></span>`;
el.addEventListener('click', () => { // Klick auf eine Wegkarte startet direkt (kein separater „Partie starten"-Button)
el.addEventListener('click', async () => {
if (_starting) return;
_starting = true;
chosenMap = id; chosenMap = id;
document.querySelectorAll('.map-btn').forEach(b => b.classList.toggle('active', b === el)); el.classList.add('active');
await overridesReady;
document.getElementById('startOverlay').classList.add('hidden');
startGame();
}); });
mapList.appendChild(el); mapList.appendChild(el);
} }
@@ -47,21 +54,14 @@ const overridesReady = fetch('js/maps-custom.json', { cache: 'no-store' })
.then(o => applyMapOverrides(o)) .then(o => applyMapOverrides(o))
.catch(() => {}); .catch(() => {});
document.getElementById('btnStart').addEventListener('click', async () => {
await overridesReady;
document.getElementById('startOverlay').classList.add('hidden');
startGame();
});
function startGame() { function startGame() {
const name = document.getElementById('playerName').value.trim().slice(0, 40); // Kinder sind über die Plattform-Session bereits identifiziert — kein Name/Code-Feld nötig.
const code = document.getElementById('classCode').value.trim().slice(0, 20); state = createState({ playerName: '', classCode: '', seed: 20260703, mapId: chosenMap });
state = createState({ playerName: name, classCode: code, seed: 20260703, mapId: chosenMap });
window.__ttState = state; // für Playwright-Tests lesbar window.__ttState = state; // für Playwright-Tests lesbar
endShown = false; endShown = false;
telemetry.init(() => Math.round(state?.time ?? 0)); telemetry.init(() => Math.round(state?.time ?? 0));
telemetry.log('session_start', { player: name, classCode: code }); telemetry.log('session_start', { player: '', classCode: '' });
ui = new UI(state, { ui = new UI(state, {
setSpeed: v => { speed = v; }, setSpeed: v => { speed = v; },
+13
View File
@@ -375,6 +375,19 @@ export class SceneRenderer {
ctx.fillStyle = '#5a5f63'; ctx.fillStyle = '#5a5f63';
ctx.fillRect(b.x - 16, b.y - 6, 32, 18); ctx.fillRect(b.x - 16, b.y - 6, 32, 18);
ctx.lineWidth = 1; ctx.lineWidth = 1;
// Talstation (unten) — sonst wirkt der Lift-Anfang „transparent".
// Solider Baukörper + Satteldach + angedeutetes Umlenkrad am Seil.
ctx.fillStyle = '#7a5c3e'; // Wand (Holz)
ctx.strokeStyle = '#3a2c1e'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.roundRect(a.x - 19, a.y - 2, 38, 24, 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#8a6a48'; // Tor
ctx.fillRect(a.x - 9, a.y + 8, 18, 14);
ctx.fillStyle = '#5a4632'; // Satteldach
ctx.beginPath(); ctx.moveTo(a.x - 24, a.y - 2); ctx.lineTo(a.x, a.y - 15); ctx.lineTo(a.x + 24, a.y - 2); ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#3a3f43'; ctx.lineWidth = 2; // Umlenkrad
ctx.beginPath(); ctx.arc(a.x, a.y + 2, 5, 0, 7); ctx.stroke();
ctx.lineWidth = 1;
// leere Gondeln pendeln // leere Gondeln pendeln
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
let f = ((this.simTime / 26000) + i / 4) % 1; let f = ((this.simTime / 26000) + i / 4) % 1;
+4 -1
View File
@@ -585,7 +585,10 @@ export class UI {
$('infoClose').addEventListener('click', () => { audio.click(); this.hideInfoPanel(); }); $('infoClose').addEventListener('click', () => { audio.click(); this.hideInfoPanel(); });
$('btnUpgrade').addEventListener('click', () => this.api.upgradeSelected()); $('btnUpgrade').addEventListener('click', () => this.api.upgradeSelected());
$('btnDemolish').addEventListener('click', () => this.api.demolishSelected()); $('btnDemolish').addEventListener('click', () => this.api.demolishSelected());
$('btnSave').addEventListener('click', () => this.api.save()); // „Ergebnis speichern"-Button entfernt — Speichern passiert automatisch am
// Partie-Ende (submitAssessment in main.js). Listener nur setzen, falls der
// Button (Alt-Standalone) doch existiert.
{ const _bs = $('btnSave'); if (_bs) _bs.addEventListener('click', () => this.api.save()); }
$('btnRestart').addEventListener('click', () => this.api.restart()); $('btnRestart').addEventListener('click', () => this.api.restart());
$('btnStats').addEventListener('click', () => { audio.click(); this.showStats(); }); $('btnStats').addEventListener('click', () => { audio.click(); this.showStats(); });
$('statsClose').addEventListener('click', () => { audio.click(); $('statsOverlay').classList.add('hidden'); }); $('statsClose').addEventListener('click', () => { audio.click(); $('statsOverlay').classList.add('hidden'); });