8047778a5b
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>
142 lines
5.7 KiB
JavaScript
142 lines
5.7 KiB
JavaScript
// 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 };
|
||
}
|