Tourismusregion: Komplett-Integration (zweite neue Sim, beta)

Wellenbasierte Routen-Sim (DefensegeograSimMac) nach dem Tourismustal-
Playbook integriert: Sim nach App/sims/tourismusregion (game.html,
Sprites 47MB->9.5MB, ohne Eigen-Backend/Editor/Dashboard/Keys),
Plattform-Hooks (GGS_LIVE_STATE mit Welle/Budget/Umwelt/Sterne/Lenkung,
Submit bei Partie-Ende mit fertigem 0-100-Score, GGS_TUTORIAL, Home-
Button, Musik leise). Wrapper + Modul-Detailseite. SQL: module_info
(beta, Karte), Glossar-Kernbegriff Besucherlenkung (+Bild, Leichte
Sprache) + 12 Zuordnungen, Lehrplan-Mapping auf tourismus-
raumentwicklung (primaer) + verkehrserziehung/systemisches-denken.
LEHRPLAN.md mit Schwerpunkt Besucherlenkung & Mobilitaet. Lehrer-
Backend: Benchmark (Sim-Score direkt), Live-Cockpit-Spalten,
needsHelp, Highlights.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 01:14:12 +02:00
parent 778634fab6
commit 8047778a5b
86 changed files with 5237 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
// Wegkarten-Geometrie: Routen als Polylinien in Bild-Pixeln, feste
// Bauplatz-Slots mit Zonen, Liftumweg auf den Berg. Ersetzt das frühere
// Iso-Kachelgitter komplett (exklusiver Diorama-Look, Karten in maps.js).
// DOM-frei läuft auch im Headless-Test.
import { BUILDINGS } from './data.js';
import { MAPS, TILE2PX } from './maps.js';
export { MAPS, TILE2PX };
// deterministischer Zufall: gleiche Wellen für alle
export function mulberry32(seed) {
let a = seed >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Karte für eine Partie instanzieren: Slots als Objekte, Routen aufgelöst
// (reverseOf-Varianten erzeugen die Gegenrichtung auf demselben Weg).
export function prepMap(mapId) {
const def = MAPS[mapId] || MAPS.talschleife;
const routes = {};
for (const [id, r] of Object.entries(def.routes)) {
if (r.reverseOf) {
routes[id] = { ...r, points: [...def.routes[r.reverseOf].points].reverse() };
} else {
routes[id] = r;
}
}
return {
...def,
routes,
slots: def.slots.map((s, i) => ({ ...s, idx: i, building: null })),
};
}
// kumulierte Längen einer Punktliste (für Bewegung per Bogenlänge)
export function cumLengths(pts) {
const cum = [0];
for (let i = 1; i < pts.length; i++) {
cum.push(cum[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));
}
return cum;
}
// ---------- Routenwahl für eine Besuchergruppe ----------
// Liefert {pts, cum, exit}. Mit gebautem Lift wird für berg-affine Gruppen
// der Seilbahn-Umweg eingefügt (rauf, kleine Runde oben, runter).
export function makeRoute(state, group, rnd, routePick = null) {
const map = state.map;
const ids = map.entryRoutes[group.entry] || Object.keys(map.routes);
let total = 0;
const weighted = ids.map(id => {
const w = map.routes[id].lake ? 0.4 + group.lakeAffinity * 1.6 : 1;
total += w;
return [id, w];
});
let pick = (routePick ?? rnd()) * total; // Konvoi: ganze Welle, gleiche Route
let routeId = weighted[0][0];
for (const [id, w] of weighted) { pick -= w; if (pick <= 0) { routeId = id; break; } }
const route = map.routes[routeId];
let pts = route.points.map(p => ({ x: p[0], y: p[1] }));
const lift = state.buildings.find(b => b.type === 'skilift');
if (lift && map.lift) {
const winter = state.seasonKey === 'winter';
const p = Math.min(0.95, group.bergAffinity * (winter ? 1.1 : 0.55));
if (rnd() < p) {
// Route an der liftnächsten Stelle unterbrechen und Umweg einfügen
let best = 0, bestD = Infinity;
for (let i = 0; i < pts.length; i++) {
const d = Math.hypot(pts[i].x - lift.x, pts[i].y - lift.y);
if (d < bestD) { bestD = d; best = i; }
}
pts = [
...pts.slice(0, best + 1),
...bergDetour(lift, map),
...pts.slice(best + 1),
];
}
}
return { pts, cum: cumLengths(pts), exit: route.exit };
}
// Seilbahn-Umweg: Zustieg → Fahrt hinauf (lift) → Runde oben → Fahrt hinab
export function bergDetour(lift, map) {
const top = { x: map.lift.topX, y: map.lift.topY };
const loop = (map.lift.loop || []).map(p => ({ x: p[0], y: p[1] }));
return [
{ x: lift.x, y: lift.y },
{ ...top, lift: true },
...loop,
{ ...top },
{ x: lift.x, y: lift.y, lift: true },
];
}
// ---------- Platzierung auf festen Slots ----------
export function canPlace(state, type, slotIdx) {
const def = BUILDINGS[type];
const slot = state.slots[slotIdx];
if (!def || !slot) return { ok: false, reason: 'Kein Bauplatz.' };
if (slot.building) return { ok: false, reason: 'Dieser Bauplatz ist schon belegt.' };
if (slot.blocked && type !== 'naturschutz') {
return { ok: false, reason: 'Naturschutzgebiet hier bleibt die Natur unberührt.' };
}
const p = def.place || {};
const liftBuilt = state.buildings.some(b => b.type === 'skilift');
if (p.zone === 'hang') {
if (slot.zone !== 'hang') return { ok: false, reason: 'Der Skilift braucht den Hang-Bauplatz unten am Berg.' };
if (p.unique && liftBuilt) return { ok: false, reason: 'Es gibt schon einen Lift baue ihn stattdessen aus.' };
} else if (p.zone === 'berg') {
if (slot.zone !== 'berg') return { ok: false, reason: 'Die Almhütte gehört auf den Berg (Plätze oben am Gipfelweg).' };
if (p.needsLift && !liftBuilt) return { ok: false, reason: 'Ohne Lift kommt hier niemand herauf baue zuerst den Skilift.' };
} else if (p.near === 'lake') {
if (slot.zone !== 'lake') return { ok: false, reason: 'Ein Badesteg braucht einen Platz direkt am Seeufer.' };
} else if (p.near === 'road') {
if (slot.zone !== 'road') return { ok: false, reason: 'Muss an die Zufahrtsstraße dort kommen Autos und Busse an.' };
} else {
// normale Gebäude: Standardplätze; Aussichtspunkt darf auch auf den Berg
if (slot.zone === 'lake') return { ok: false, reason: 'Direkt am Wasser passt nur ein Badesteg.' };
if (slot.zone === 'road') return { ok: false, reason: 'Die Plätze an der Zufahrt sind für Parkplatz und Bushaltestelle reserviert.' };
if (slot.zone === 'hang') return { ok: false, reason: 'Dieser Platz am Hang ist für den Skilift reserviert.' };
if (slot.zone === 'berg') {
if (type !== 'aussicht' && type !== 'almhuette') {
return { ok: false, reason: 'Hier oben passen nur Almhütte oder Aussichtspunkt.' };
}
if (!liftBuilt) return { ok: false, reason: 'Ohne Lift kommt hier oben niemand vorbei.' };
}
}
if (state.budget < def.cost[0]) {
return { ok: false, reason: `Zu teuer ${def.name} kostet ${def.cost[0]} €.` };
}
return { ok: true };
}