/** * Flussmanagement — Engine (Single Source of Truth) * ------------------------------------------------------------------ * Headless Spiellogik: kein DOM, keine Audio-Aufrufe, kein Zeichnen. * Der Konsument (game.html) ruft die Engine-Methoden auf und zeichnet * den zurueckgegebenen Zustand. Events werden in einem internen Puffer * `game._pendingEvents` gesammelt und bei der naechsten mutierenden * Aktion zurueckgegeben. * * Portiert aus App/fluss.html (V1). Logik bleibt weitgehend erhalten, * wird aber in eine serialisierbare State-Struktur gehoben, damit * Autosave und Resume sauber funktionieren. * * Exportiert als globales `window.FlussEngine` (kein ES-Module). */ (function () { 'use strict'; /* ============================================================ KONSTANTEN — Grid, Werkzeuge, Wetter, Schwierigkeit ============================================================ */ const COLS = 20; const ROWS = 16; const RIVER_SEGMENTS = ROWS; // ein Fluss-Punkt pro Zeile const MAX_YEARS_DEFAULT = 100; const TOOLS = [ { id: 'inspect', name: 'Ansehen', icon: '🔍', cost: 0, desc: 'Klicken, um ein Feld zu untersuchen.' }, { id: 'deich', name: 'Deich bauen', icon: '🛡️', cost: 8, desc: 'Schützt angrenzende Felder vor Hochwasser.' }, { id: 'feld', name: 'Feld anlegen', icon: '🌾', cost: 4, desc: 'Mehr Nahrung, aber weniger Biodiversität.' }, { id: 'siedlung', name: 'Siedlung bauen', icon: '🏘️', cost: 12, desc: 'Mehr Bevölkerung und Wirtschaft.' }, { id: 'wald', name: 'Wald pflanzen', icon: '🌳', cost: 6, desc: 'Stärkt Biodiversität und Retention.' }, { id: 'abriss', name: 'Abriss', icon: '🧨', cost: 2, desc: 'Entfernt ein gebautes Feld.' }, { id: 'begradigen', name: 'Fluss begradigen', icon: '📏', cost: 15, desc: 'Gewinnt Bauland, schadet Biodiversität.' }, ]; const WEATHER = [ { key: 'drought', name: 'Dürre', icon: '☀️', factor: 0.2, prob: 0.12 }, { key: 'dry', name: 'Trocken', icon: '🌤️', factor: 0.4, prob: 0.25 }, { key: 'normal', name: 'Normal', icon: '⛅', factor: 0.8, prob: 0.33 }, { key: 'rain', name: 'Regen', icon: '🌧️', factor: 1.5, prob: 0.20 }, { key: 'heavy_rain', name: 'Starkregen', icon: '⛈️', factor: 2.8, prob: 0.10 }, ]; // Achievements — werden in tick/applyTool gesetzt und bei Level-Ende // ausgewertet. // Tierarten als Biodiversitaets-Belohnung. Je hoeher der Biodiv-Wert, // desto seltenere Arten siedeln sich an. Hysterese: Arten verschwinden // erst, wenn Biodiv 5 Punkte unter die Schwelle faellt — verhindert // Flapping an der Grenze. const SPECIES = [ { key: 'hase', icon: '🐇', name: 'Feldhase', threshold: 40 }, { key: 'frosch', icon: '🐸', name: 'Laubfrosch', threshold: 50 }, { key: 'ente', icon: '🦆', name: 'Stockente', threshold: 58 }, { key: 'biene', icon: '🐝', name: 'Wildbiene', threshold: 62 }, { key: 'falter', icon: '🦋', name: 'Schmetterling', threshold: 68 }, { key: 'reh', icon: '🦌', name: 'Reh', threshold: 74 }, { key: 'eichhorn', icon: '🐿️', name: 'Eichhörnchen', threshold: 80 }, { key: 'eule', icon: '🦉', name: 'Waldkauz', threshold: 86 }, { key: 'fuchs', icon: '🦊', name: 'Fuchs', threshold: 91 }, { key: 'storch', icon: '🕊️', name: 'Weißstorch', threshold: 96 }, ]; const SPECIES_HYSTERESIS = 5; const ACHIEVEMENTS = [ { key: 'first_deich', icon: '🛡️', title: 'Sicher ist sicher', desc: 'Ersten Deich gebaut.' }, { key: 'first_wald', icon: '🌳', title: 'Grüner Daumen', desc: 'Ersten Wald gepflanzt.' }, { key: 'decade_safe', icon: '🏆', title: 'Zehn Jahre stabil', desc: '10 Jahre ohne zerstörte Siedlung.' }, { key: 'balance_found', icon: '⚖️', title: 'Ausgleich gefunden', desc: 'Biodiv. > 60 UND Wirtschaft > 50 über 50 Jahre.' }, { key: 'renaturierung', icon: '🦋', title: 'Renaturiert', desc: 'Begradigung wieder aufgelöst.' }, { key: 'survivor', icon: '🎖️', title: 'Century-Hüter', desc: '100 Jahre am Fluss überlebt.' }, ]; // Hilfs-PRNG — wir verlassen uns auf Math.random(), aber der Seed-Block // dient als Platzhalter, falls spaeter deterministische Replays noetig werden. function rollWeather() { let r = Math.random(), cum = 0; for (const w of WEATHER) { cum += w.prob; if (r < cum) return w; } return WEATHER[2]; } /* ============================================================ GAME STATE FABRIK ============================================================ */ function createGame(opts) { opts = opts || {}; const level = opts.level || 1; const cfg = opts.levelConfig || {}; const maxYears = cfg.timeLimit || MAX_YEARS_DEFAULT; const startBudget = cfg.startBudget != null ? cfg.startBudget : 60; const eventFreq = cfg.eventFrequency != null ? cfg.eventFrequency : 0.3; const toolsAvailable = cfg.toolsAvailable || TOOLS.map(t => t.id); const game = { level: level, levelConfig: cfg, maxYears: maxYears, eventFreq: eventFreq, toolsAvailable: toolsAvailable, // Fluss: 16 Spaltenpositionen + Engineering-Stufe riverCol: [8,9,10,11,10,9,8,7,8,9,10,11,10,9,8,7], riverColSmooth: [8,9,10,11,10,9,8,7,8,9,10,11,10,9,8,7], riverEng: new Array(RIVER_SEGMENTS).fill(0), riverRadius: [], // Land-Grid (ROWS x COLS) land: [], tilePop: [], tileYield: [], tileFishYield: [], // Fischfang pro Siedlung, fuer Rendering floodIntensity: [], tileFloodYears: [], // Wetter & Zeit weather: WEATHER[2], wxTimer: 0, year: 0, budget: startBudget, gameOver: false, // Verlaufsgeschichte fuer Graphen (pro Jahr ein Eintrag) history: [], // Didaktik-Tracking achievements: {}, // key -> true species: {}, // key -> true (vorhandene Tierarten) stats: { settlementsLost: 0, fieldsLost: 0, yearsWithoutLoss: 0, balancedYears: 0, straightenCount: 0, denaturalizeCount: 0, builtTools: {}, // tool -> count }, _pendingEvents: [], _nextEventId: 1, }; // Fluss-Radien initialisieren (variieren) for (let i = 0; i < RIVER_SEGMENTS; i++) { game.riverRadius[i] = 0.8 + Math.sin(i * 0.9) * 0.3 + Math.random() * 0.2; } // Grid initialisieren for (let r = 0; r < ROWS; r++) { game.land[r] = []; game.tilePop[r] = []; game.tileYield[r] = []; game.tileFishYield[r] = []; game.floodIntensity[r] = []; game.tileFloodYears[r] = []; for (let c = 0; c < COLS; c++) { const dist = Math.abs(c - game.riverCol[r]); if (dist <= 1) game.land[r][c] = 'meadow'; else if (dist > 4 && Math.random() < 0.5) game.land[r][c] = 'forest'; else game.land[r][c] = 'meadow'; game.tilePop[r][c] = 0; game.tileYield[r][c] = 0; game.tileFishYield[r][c] = 0; game.floodIntensity[r][c] = 0; game.tileFloodYears[r][c] = 0; } } // Startgebaeude (eine Siedlung + zwei Felder) game.land[4][13] = 'settlement'; game.tilePop[4][13] = 3; game.land[5][13] = 'field'; game.land[5][14] = 'field'; // Startzustand hat biodiv ~100: alle Arten, deren Schwelle erfuellt ist, // sind bereits da (ohne Toast-Flut). const startBiodiv = estimateStartBiodiv(game); SPECIES.forEach(sp => { if (startBiodiv >= sp.threshold) game.species[sp.key] = true; }); pushEvent(game, '🏞️', 'Willkommen am Fluss! Er wandert — plane voraus.', 'neutral'); return game; } function estimateStartBiodiv(game) { let forests = 0, settlements = 0, fields = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const t = game.land[r][c]; if (t === 'forest') forests++; else if (t === 'settlement') settlements++; else if (t === 'field') fields++; } // Selbe Formel wie im Tick-Snapshot. return clamp100(forests * 2 - (settlements + fields) * 2.5); } /* ============================================================ FLUSS-GEOMETRIE ============================================================ */ function distToRiver(game, r, c) { let minD = 999; for (let i = 0; i < ROWS; i++) { const rc = game.riverColSmooth[i]; const d = Math.sqrt((r - i) * (r - i) + (c - rc) * (c - rc)); if (d < minD) minD = d; if (i < ROWS - 1) { const rc2 = game.riverColSmooth[i + 1]; const d2 = Math.sqrt((r - (i + 1)) * (r - (i + 1)) + (c - rc2) * (c - rc2)); if (d2 < minD) minD = d2; } } return minD; } function isRiverTile(game, r, c) { return distToRiver(game, r, c) < 1.3; } function hasLeveeNear(game, r, c) { for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && game.land[nr][nc] === 'levee') return true; } return false; } /* ============================================================ TICK — ein Jahr weiter ============================================================ */ function tick(game) { if (game.gameOver) return []; game.year++; // 1. Einkommen / Nahrung / Bevoelkerungsdynamik // Globale Einnahmen-Daempfung: Werkzeuge sollen sich nicht zu schnell // refinanzieren, sonst hat die Lernende keinen echten Trade-off. const INCOME_FACTOR = 0.5; let income = 0, totalPop = 0, totalFood = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const dist = distToRiver(game, r, c); const moisture = Math.max(0, 1 - Math.abs(dist - 2.5) / 3); const fi = game.floodIntensity[r][c]; const tile = game.land[r][c]; if (tile === 'field') { let dryPenalty = 0; for (let i = Math.max(0, r-1); i <= Math.min(ROWS-1, r+1); i++) { if (game.riverEng[i] > 0) dryPenalty = Math.max(dryPenalty, game.riverEng[i] * 0.25); } const effMoisture = Math.max(0, moisture - dryPenalty); let workerBonus = 0; for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) { const nr = r+dr, nc = c+dc; if (nr>=0&&nr=0&&nc totalPop * 0.4; if (cur < 8 && hasFood && fi < 0.15 && Math.random() < 0.35) { game.tilePop[r][c] = Math.min(8, cur + 1); } if (fi > 0.2 && cur > 0 && Math.random() < fi * 0.2) { game.tilePop[r][c] = Math.max(0, cur - 1); } totalPop += game.tilePop[r][c]; income += game.tilePop[r][c] * 0.8; // Fischfang: je naeher am Fluss, umso mehr Fische. Hochwasser // truebt das Wasser und halbiert den Fang. if (dist <= 3 && game.tilePop[r][c] > 0) { const floodFactor = fi > 0.3 ? 0.5 : 1; const fishCaught = Math.round((3 - dist) * game.tilePop[r][c] * 0.75 * floodFactor); totalFood += fishCaught; game.tileFishYield[r][c] = fishCaught; } else if (game.tileFishYield && game.tileFishYield[r]) { game.tileFishYield[r][c] = 0; } } } game.budget += Math.round(income * INCOME_FACTOR); // 2. Wetter wechselt alle 3 Jahre game.wxTimer++; if (game.wxTimer >= 3) { game.wxTimer = 0; game.weather = rollWeather(); } // 3. Fluss wandert — bis zu ZWEI Punkte pro Jahr, wenn nicht befestigt // (vorher nur einer; jetzt deutlich lebendiger). const moves = 2; for (let m = 0; m < moves; m++) { const idx = Math.floor(Math.random() * ROWS); if (game.riverEng[idx] !== 0) continue; if (Math.random() >= 0.95 + game.eventFreq * 0.05) continue; // fast immer const dir = Math.random() < 0.5 ? -1 : 1; const newCol = game.riverCol[idx] + dir; const prevOk = idx === 0 || Math.abs(newCol - game.riverCol[idx - 1]) <= 3; const nextOk = idx === ROWS - 1 || Math.abs(newCol - game.riverCol[idx + 1]) <= 3; if (!(newCol >= 1 && newCol < COLS - 1 && prevOk && nextOk)) continue; { game.riverCol[idx] = newCol; const prevTile = game.land[idx][newCol]; if (prevTile === 'settlement') { game.land[idx][newCol] = 'ruin'; game.stats.settlementsLost++; pushEvent(game, '🌊', 'Fluss verschlingt Siedlung — die Bewohner fliehen.', 'bad', 'flood-settlement'); } else if (prevTile === 'field') { game.land[idx][newCol] = 'meadow'; game.stats.fieldsLost++; pushEvent(game, '🌊', 'Fluss überflutet ein Feld.', 'bad', 'flood-field'); } else if (prevTile === 'levee') { // Deich haelt — Fluss wird zurueckgedraengt game.riverCol[idx] -= dir; pushEvent(game, '🛡️', 'Ein Deich hält dem Wasser stand!', 'good', 'levee-hold'); } } } // 4. Hochwasser / Feuchtigkeit smooth updaten for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const dist = distToRiver(game, r, c); let target = 0; if (game.weather.factor >= 1.0 && dist < 4) { const base = Math.max(0, 1 - dist / 4); const tile = game.land[r][c]; const leveeProt = tile === 'levee' ? 0.9 : (hasLeveeNear(game, r, c) ? 0.5 : 0); target = base * game.weather.factor * 0.4 * (1 - leveeProt); } game.floodIntensity[r][c] += (target - game.floodIntensity[r][c]) * 0.2; if (game.floodIntensity[r][c] > 0.35) { game.tileFloodYears[r][c] = (game.tileFloodYears[r][c] || 0) + 1; } else { game.tileFloodYears[r][c] = 0; } const tile = game.land[r][c]; // Schadens-Schwellen: deutlich haerter als V1, damit die Lernende // Hochwasser ernst nimmt. if (tile === 'field' && game.tileFloodYears[r][c] >= 1) { game.land[r][c] = 'meadow'; game.stats.fieldsLost++; pushEvent(game, '🌾', 'Ein Feld ist ertrunken — zu lange unter Wasser.', 'bad', 'flood-field'); } if (tile === 'settlement' && game.floodIntensity[r][c] > 0.4 && Math.random() < 0.12) { game.land[r][c] = 'ruin'; game.stats.settlementsLost++; pushEvent(game, '🏚️', 'Eine Siedlung ist durch das Hochwasser zerstört worden!', 'bad', 'flood-settlement'); } if (tile === 'forest' && game.tileFloodYears[r][c] >= 2) { game.land[r][c] = 'dead_tree'; pushEvent(game, '🌳', 'Ein Wald ist im Dauerhochwasser abgestorben.', 'bad', 'tree-flood-die'); } if (tile === 'levee' && game.floodIntensity[r][c] > 0.35 && Math.random() < 0.15) { game.land[r][c] = 'meadow'; pushEvent(game, '🧱', 'Ein Deich wurde vom Hochwasser weggespült!', 'bad', 'levee-break'); } } // 4b. Begradigung trocknet Umgebung aus → Baeume sterben for (let i = 0; i < ROWS; i++) { if (game.riverEng[i] > 0) { const rc = game.riverCol[i]; for (let dc = -3; dc <= 3; dc++) { const cc = rc + dc; if (cc < 0 || cc >= COLS) continue; if (Math.abs(dc) <= 1) continue; if (game.land[i][cc] === 'forest' && Math.random() < 0.03 * game.riverEng[i]) { game.land[i][cc] = 'dead_tree'; pushEvent(game, '🌳', 'Ein Baum vertrocknet neben der Begradigung.', 'bad', 'tree-straighten-die'); } } } } // 5. Flussbreite passt sich an (Hochwasser → breiter) for (let i = 0; i < ROWS; i++) { const target = game.weather.factor > 1.2 ? 1.8 : 1.2; game.riverRadius[i] += (target - game.riverRadius[i]) * 0.1; } // 6. Ruinen und tote Baeume verfallen for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { if (game.land[r][c] === 'ruin' && Math.random() < 0.03) game.land[r][c] = 'meadow'; if (game.land[r][c] === 'dead_tree' && Math.random() < 0.03) game.land[r][c] = 'meadow'; } // 7. Smooth-Fluss-Animation (auch im Headless: wir brauchen die Werte // fuer Distanz-Berechnungen im naechsten Tick) for (let i = 0; i < ROWS; i++) { game.riverColSmooth[i] += (game.riverCol[i] - game.riverColSmooth[i]) * 0.3; } // 8. History eintragen const counts = countTiles(game); const avgEng = game.riverEng.reduce((s,v) => s+v, 0) / ROWS; const floodTotal = sum2d(game.floodIntensity) / (ROWS * COLS); const snapshot = { year: game.year, pop: clamp100(totalPop * 5 - counts.ruin * 8), // Nahrungs-Skala: 100 wird erst erreicht, wenn ungefaehr die Haelfte // des Talbodens unter Weizen steht (totalFood ~333). Vorher 1.5 // war zu locker — Anzeige war fast immer voll. food: clamp100(totalFood * 0.3), // Biodiv: Wald-Gewicht halbiert (4 -> 2), Bau-Strafe erhoeht (1.5 -> 2.5), // damit der Startwert nahe 100 liegt und jeder Bau sofort spuerbar ist. biodiv: clamp100(counts.forest * 2 - avgEng * 30 - (counts.settlement + counts.field) * 2.5), flood: clamp100(15 + counts.levee * 6 + avgEng * 8 - floodTotal * 80), economy: clamp100(totalPop * 3 + counts.field * 2 - counts.ruin * 5), budget: game.budget, }; game.history.push(snapshot); // Didaktik-Tracking if (game.stats.settlementsLost === 0) game.stats.yearsWithoutLoss++; if (snapshot.biodiv > 60 && snapshot.economy > 50) game.stats.balancedYears++; else game.stats.balancedYears = 0; // Arten-Check: Schwelle nach oben → neue Art; Schwelle + Hysterese // nach unten → Art verschwindet. Die Events landen im Stapel und als // Toast, damit der Biodiv-Wandel spuerbar wird. SPECIES.forEach(sp => { const has = !!game.species[sp.key]; if (!has && snapshot.biodiv >= sp.threshold) { game.species[sp.key] = true; pushEvent(game, sp.icon, 'Neue Art im Gebiet: ' + sp.name, 'good', 'species-arrived'); } else if (has && snapshot.biodiv < sp.threshold - SPECIES_HYSTERESIS) { delete game.species[sp.key]; pushEvent(game, sp.icon, sp.name + ' verschwindet aus dem Gebiet.', 'bad', 'species-left'); } }); checkAchievements(game, snapshot); if (game.year >= game.maxYears) { game.gameOver = true; unlockAchievement(game, 'survivor'); } return drainEvents(game); } /* ============================================================ TOOLS — Interaktion mit dem Grid ============================================================ */ function applyTool(game, toolId, r, c) { if (game.gameOver) return { ok: false, reason: 'game_over', events: drainEvents(game) }; if (!game.toolsAvailable.includes(toolId)) return { ok: false, reason: 'locked', events: [] }; if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return { ok: false, reason: 'out_of_bounds', events: [] }; const tool = TOOLS.find(t => t.id === toolId); if (!tool) return { ok: false, reason: 'unknown', events: [] }; const onRiver = isRiverTile(game, r, c); const cur = game.land[r][c]; if (toolId === 'inspect') { const msg = inspectTile(game, r, c); pushEvent(game, msg.icon, msg.text, msg.type); return { ok: true, events: drainEvents(game) }; } if (toolId === 'begradigen') { if (!onRiver) return fail(game, 'Nur auf dem Fluss anwendbar.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.riverEng[r] = Math.min(3, game.riverEng[r] + 1); game.riverCol[r] = Math.floor(COLS / 2); game.budget -= tool.cost; game.stats.straightenCount++; pushEvent(game, '📏', 'Fluss begradigt — Bauland, aber Ufer trocknet aus.', 'bad', 'begradigen'); track(game, toolId); return { ok: true, events: drainEvents(game) }; } if (toolId === 'deich') { if (onRiver) return fail(game, 'Deich nicht direkt im Fluss möglich.'); const d = distToRiver(game, r, c); if (d > 4) return fail(game, 'Zu weit vom Fluss — Deich muss am Ufer stehen.'); if (cur !== 'meadow' && cur !== 'forest') return fail(game, 'Nur auf Wiese oder Wald möglich.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.land[r][c] = 'levee'; game.budget -= tool.cost; unlockAchievement(game, 'first_deich'); track(game, toolId); pushEvent(game, '🛡️', 'Deich gebaut — Schutz für die Nachbarschaft.', 'good'); return { ok: true, events: drainEvents(game) }; } if (toolId === 'feld') { if (onRiver) return fail(game, 'Feld nicht im Fluss möglich.'); if (cur !== 'meadow') return fail(game, 'Nur auf freier Wiese möglich.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.land[r][c] = 'field'; game.budget -= tool.cost; track(game, toolId); const d = distToRiver(game, r, c); if (d < 2.5) pushEvent(game, '🌾', 'Feld nah am Fluss — guter Ertrag, aber Hochwassergefahr.', 'good'); else if (d < 4) pushEvent(game, '🌾', 'Feld in guter Lage.', 'good'); else pushEvent(game, '🌾', 'Feld weit vom Fluss — wird trocken, geringer Ertrag.', 'bad'); return { ok: true, events: drainEvents(game) }; } if (toolId === 'siedlung') { if (onRiver) return fail(game, 'Siedlung nicht im Fluss möglich.'); if (cur !== 'meadow' && cur !== 'field') return fail(game, 'Nur auf Wiese oder Feld möglich.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.land[r][c] = 'settlement'; game.tilePop[r][c] = 0; game.budget -= tool.cost; track(game, toolId); const d = distToRiver(game, r, c); if (d < 2) pushEvent(game, '🏘️', 'Siedlung sehr nah am Fluss — Deich dringend empfohlen!', 'bad'); else if (d < 4) pushEvent(game, '🏘️', 'Siedlung gebaut — gute Lage.', 'good'); else pushEvent(game, '🏘️', 'Siedlung sicher, aber weit weg vom Wasser.', 'neutral'); return { ok: true, events: drainEvents(game) }; } if (toolId === 'wald') { if (onRiver) return fail(game, 'Wald nicht im Fluss möglich.'); if (cur !== 'meadow') return fail(game, 'Nur auf Wiese möglich.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.land[r][c] = 'forest'; game.budget -= tool.cost; unlockAchievement(game, 'first_wald'); track(game, toolId); pushEvent(game, '🌳', 'Wald gepflanzt — stärkt Biodiversität und Retention.', 'good'); return { ok: true, events: drainEvents(game) }; } if (toolId === 'abriss') { if (onRiver && game.riverEng[r] > 0) { if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.riverEng[r] = 0; game.budget -= tool.cost; game.stats.denaturalizeCount++; unlockAchievement(game, 'renaturierung'); pushEvent(game, '🦋', 'Begradigung aufgelöst — der Fluss darf wieder wandern.', 'good', 'renaturierung'); return { ok: true, events: drainEvents(game) }; } if (!['field','settlement','levee','ruin','dead_tree','forest'].includes(cur)) return fail(game, 'Hier gibt es nichts zum Abreißen.'); if (game.budget < tool.cost) return fail(game, 'Nicht genug Budget.'); game.land[r][c] = 'meadow'; game.budget -= tool.cost; track(game, toolId); pushEvent(game, '🧨', 'Gebäude abgerissen.', 'neutral'); return { ok: true, events: drainEvents(game) }; } return { ok: false, reason: 'no_match', events: [] }; } function inspectTile(game, r, c) { const dist = distToRiver(game, r, c); const fi = game.floodIntensity[r]?.[c] || 0; const fy = game.tileFloodYears[r]?.[c] || 0; const onRiver = isRiverTile(game, r, c); const cur = game.land[r][c]; if (onRiver) { const eng = game.riverEng[r]; const engNames = ['natürlich','Holzverbau','Bruchstein','Beton']; let t = 'Fluss — Befestigung: ' + (engNames[eng] || 'natürlich') + '. '; t += eng > 0 ? 'Fluss ist hier fixiert. Umgebung trocknet aus.' : 'Fließt frei — kann jedes Jahr wandern!'; return { icon: '🌊', text: t, type: eng > 0 ? 'bad' : 'neutral' }; } if (cur === 'field') { const yi = game.tileYield[r]?.[c] || 0; let t = 'Feld — Ertrag: ' + yi + '/8. '; if (fi > 0.35) t += 'Abgesoffen! Noch ' + Math.max(0, 2 - fy) + ' Jahr(e) bis es stirbt.'; else if (fi > 0.15) t += 'Zu feucht — Ertrag reduziert.'; else if (dist > 5) t += 'Sehr trocken — weit vom Fluss.'; else if (dist > 3.5) t += 'Etwas trocken.'; else t += 'Gute Feuchtigkeit!'; return { icon: '🌾', text: t, type: yi > 4 ? 'good' : yi > 1 ? 'neutral' : 'bad' }; } if (cur === 'settlement') { const pop = game.tilePop[r]?.[c] || 0; let t = 'Siedlung — ' + pop + '/8 Bewohner. '; if (fi > 0.3) t += 'Feucht! Bewohner werden krank und fliehen.'; else if (fi > 0.1) t += 'Etwas feucht — manche Bewohner sind unwohl.'; else t += 'Trocken und sicher.'; return { icon: '🏘️', text: t, type: fi > 0.2 ? 'bad' : 'good' }; } if (cur === 'levee') { let t = 'Deich. '; t += fi > 0.3 ? 'Unter Druck! Kann bei Dauerflut brechen.' : 'Intakt. Schützt die Nachbarschaft.'; return { icon: '🛡️', text: t, type: fi > 0.3 ? 'bad' : 'good' }; } if (cur === 'forest') { let t = 'Wald — schützt vor Erosion, speichert Grundwasser. '; let nearStraight = false; for (let i = Math.max(0,r-1); i <= Math.min(ROWS-1,r+1); i++) if (game.riverEng[i] > 0) nearStraight = true; if (nearStraight) t += 'Neben Begradigung — Baum in Gefahr!'; else if (fi > 0.2) t += 'Feucht. Bei Dauerflut stirbt der Baum.'; return { icon: '🌳', text: t, type: nearStraight ? 'bad' : 'good' }; } if (cur === 'ruin') return { icon: '🏚️', text: 'Ruine. Kann abgerissen werden. Verfällt auch von selbst.', type: 'bad' }; if (cur === 'dead_tree') return { icon: '🪵', text: 'Toter Baum — vertrocknet oder ertrunken.', type: 'bad' }; // meadow let t = 'Wiese — freie Fläche. '; if (dist < 2.5) t += 'Nah am Fluss: fruchtbar, aber hochwassergefährdet.'; else if (dist < 4) t += 'Gute Lage für Feld oder Siedlung.'; else t += 'Weit vom Fluss. Trocken.'; return { icon: '🌱', text: t, type: 'neutral' }; } /* ============================================================ SUMMARY / SERIALIZE ============================================================ */ function getStatus(game) { const counts = countTiles(game); const avgEng = game.riverEng.reduce((s,v) => s+v, 0) / ROWS; const floodTotal = sum2d(game.floodIntensity) / (ROWS * COLS); let pop = 0, food = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { if (game.land[r][c] === 'settlement') pop += game.tilePop[r][c]; if (game.land[r][c] === 'field') food += game.tileYield[r][c]; } return { year: game.year, maxYears: game.maxYears, budget: game.budget, weather: game.weather, gameOver: game.gameOver, params: { population: pop, populationMax: 200, // Identische Formeln wie im Tick-Snapshot oben — Single Source of Truth // sollte langfristig einer extrahierten Formel-Funktion folgen. food: clamp100(food * 0.3), biodiv: clamp100(counts.forest * 2 - avgEng * 30 - (counts.settlement + counts.field) * 2.5), flood: clamp100(15 + counts.levee * 6 + avgEng * 8 - floodTotal * 80), economy: clamp100(pop * 3 + counts.field * 2 - counts.ruin * 5), }, counts: counts, achievements: Object.keys(game.achievements), }; } function getSummary(game) { const st = getStatus(game); let score = Math.round((st.params.biodiv + st.params.flood + st.params.economy) / 3); score = Math.max(0, Math.min(100, score - game.stats.settlementsLost * 5)); const stars = score >= 85 ? 5 : score >= 70 ? 4 : score >= 55 ? 3 : score >= 35 ? 2 : 1; return { simId: 'fluss', level: game.level, stars: stars, score: score, completed: game.year >= game.maxYears, params: st.params, stats: game.stats, badges_earned: Object.keys(game.achievements), durationYears: game.year, }; } function serialize(game) { return JSON.stringify({ v: 1, level: game.level, levelConfig: game.levelConfig, maxYears: game.maxYears, eventFreq: game.eventFreq, toolsAvailable: game.toolsAvailable, riverCol: game.riverCol, riverColSmooth: game.riverColSmooth, riverEng: game.riverEng, riverRadius: game.riverRadius, land: game.land, tilePop: game.tilePop, tileYield: game.tileYield, tileFishYield: game.tileFishYield, floodIntensity: game.floodIntensity, tileFloodYears: game.tileFloodYears, weather: game.weather, wxTimer: game.wxTimer, year: game.year, budget: game.budget, gameOver: game.gameOver, history: game.history, achievements: game.achievements, species: game.species, stats: game.stats, }); } function deserialize(json) { let s; try { s = JSON.parse(json); } catch { return null; } if (!s || s.v !== 1) return null; const game = createGame({ level: s.level, levelConfig: s.levelConfig }); Object.assign(game, { maxYears: s.maxYears, eventFreq: s.eventFreq, toolsAvailable: s.toolsAvailable, riverCol: s.riverCol, riverColSmooth: s.riverColSmooth, riverEng: s.riverEng, riverRadius: s.riverRadius, land: s.land, tilePop: s.tilePop, tileYield: s.tileYield, tileFishYield: s.tileFishYield || [], floodIntensity: s.floodIntensity, tileFloodYears: s.tileFloodYears, weather: s.weather, wxTimer: s.wxTimer, year: s.year, budget: s.budget, gameOver: s.gameOver, history: s.history, achievements: s.achievements, species: s.species || {}, stats: s.stats, _pendingEvents: [], }); return game; } /* ============================================================ HELPERS ============================================================ */ function countTiles(game) { const out = { meadow:0, forest:0, field:0, settlement:0, levee:0, ruin:0, dead_tree:0 }; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const t = game.land[r][c]; if (out[t] !== undefined) out[t]++; } return out; } function sum2d(arr) { let s = 0; for (let r = 0; r < arr.length; r++) for (let c = 0; c < arr[r].length; c++) s += arr[r][c]; return s; } function clamp100(v) { return Math.max(0, Math.min(100, Math.round(v))); } function pushEvent(game, icon, text, type, topic) { game._pendingEvents.push({ id: game._nextEventId++, icon: icon, text: text, type: type || 'neutral', year: game.year, topic: topic || null, }); } function drainEvents(game) { const out = game._pendingEvents; game._pendingEvents = []; return out; } function fail(game, msg) { pushEvent(game, '⚠️', msg, 'bad'); return { ok: false, reason: 'constraint', events: drainEvents(game) }; } function track(game, toolId) { game.stats.builtTools[toolId] = (game.stats.builtTools[toolId] || 0) + 1; } function unlockAchievement(game, key) { if (game.achievements[key]) return false; const def = ACHIEVEMENTS.find(a => a.key === key); if (!def) return false; game.achievements[key] = true; pushEvent(game, def.icon, 'Erfolg freigeschaltet: ' + def.title, 'good'); return true; } function checkAchievements(game, snap) { if (game.stats.yearsWithoutLoss >= 10) unlockAchievement(game, 'decade_safe'); if (game.stats.balancedYears >= 50) unlockAchievement(game, 'balance_found'); } /* ============================================================ EXPORT ============================================================ */ window.FlussEngine = { TOOLS: TOOLS, WEATHER: WEATHER, ACHIEVEMENTS: ACHIEVEMENTS, SPECIES: SPECIES, COLS: COLS, ROWS: ROWS, MAX_YEARS_DEFAULT: MAX_YEARS_DEFAULT, createGame: createGame, tick: tick, applyTool: applyTool, distToRiver: distToRiver, isRiverTile: isRiverTile, inspectTile: inspectTile, countTiles: countTiles, getStatus: getStatus, getSummary: getSummary, serialize: serialize, deserialize: deserialize, }; })();