// 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 }; }