Files
Adminator 8047778a5b 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>
2026-07-12 01:14:12 +02:00

142 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };
}