Files
geograsim/App/fluss.html
T
Adminator f22c5ebbfe Stand 2026-04-13: PHP/MySQL Infrastruktur, Flussmanagement, Stadt-Prototyp
- PHP/MySQL Backend (XAMPP + Produktionsserver)
- Front-Controller, API-Endpunkte, Session-Management
- Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss)
- Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem)
- Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst
- persistence.ts: Dualer Speicher (localStorage + Server-API)
- 6 Unit-Test-Dateien fuer bestehende Simulationen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:43:42 +02:00

860 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Flussmanagement</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#f7f6f3;--white:#fff;--text:#1a1a1a;--text2:#4a4a4a;--text3:#8a8a8a;--fjord:#4a7c8a;--fjord-l:#dae8ec;--fjord-d:#3a6470;--moss:#5a8a5e;--moss-l:#dceadd;--sand:#c4a35a;--coral:#c07a6b}
html,body{height:100%;overflow:hidden}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
.nav{padding:.4rem .8rem;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(0,0,0,.05);background:var(--white);flex-shrink:0}
.nav-brand{display:flex;align-items:center;gap:.4rem;font-weight:800;font-size:.9rem;color:var(--fjord);text-decoration:none}
.nav-brand img{width:24px;height:24px;border-radius:5px}
.nav-btn{padding:.25rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:var(--white);color:var(--text2);font-size:.68rem;font-weight:600;cursor:pointer;text-decoration:none}
.nav-btn:hover{background:var(--fjord-l);color:var(--fjord)}
.main{flex:1;display:grid;grid-template-columns:190px 1fr 200px;gap:.4rem;padding:.4rem;min-height:0;overflow:hidden}
.panel{background:var(--white);border-radius:9px;border:1px solid rgba(0,0,0,.04);padding:.5rem;box-shadow:0 1px 4px rgba(0,0,0,.02);overflow-y:auto}
.panel-h{font-size:.58rem;font-weight:700;color:var(--fjord);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.3rem}
.col-left,.col-right{display:flex;flex-direction:column;gap:.4rem;overflow-y:auto}
.col-center{display:flex;flex-direction:column;gap:.3rem;min-height:0}
.topbar{flex-shrink:0;display:flex;align-items:center;gap:.4rem;background:var(--white);border-radius:8px;padding:.3rem .6rem;border:1px solid rgba(0,0,0,.04);flex-wrap:wrap}
.topbar .stat{font-size:.68rem;font-weight:700;display:flex;align-items:center;gap:.2rem}
.topbar .stat .v{font-size:.85rem}
.topbar .gold .v{color:var(--sand)}
.topbar .yr .v{color:var(--fjord)}
.topbar .inc{font-size:.58rem;color:var(--moss);font-weight:600}
.topbar .wx{font-size:.78rem;padding:.1rem .35rem;border-radius:4px;background:var(--bg);font-weight:600}
.spd{display:flex;gap:.1rem;margin-left:auto}
.spd button{width:26px;height:22px;border:1px solid rgba(0,0,0,.07);border-radius:4px;background:var(--white);font-size:.62rem;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center}
.spd button.on{background:var(--fjord);color:#fff;border-color:var(--fjord)}
.canvas-wrap{flex:1;background:#dde6d4;border-radius:9px;overflow:hidden;position:relative;min-height:200px;cursor:crosshair}
.canvas-wrap canvas{width:100%;height:100%;display:block}
.param{display:flex;align-items:center;gap:.3rem;padding:.18rem 0;border-bottom:1px solid rgba(0,0,0,.02)}.param:last-child{border:none}
.param-i{font-size:.75rem;width:17px;text-align:center}.param-inf{flex:1}
.param-n{font-size:.55rem;color:var(--text3)}.param-bar{height:3.5px;background:#e8e5dc;border-radius:2px;margin-top:1px;overflow:hidden}
.param-bar-f{height:100%;border-radius:2px;transition:width .8s}.param-v{font-size:.62rem;font-weight:700;min-width:17px;text-align:right}
.tool{display:flex;align-items:center;gap:.3rem;padding:.3rem .4rem;border:1.5px solid rgba(0,0,0,.05);border-radius:6px;background:var(--white);cursor:pointer;width:100%;text-align:left;margin-bottom:.18rem;transition:all .12s}
.tool:hover{border-color:var(--fjord);background:var(--fjord-l)}.tool.on{border-color:var(--fjord);background:var(--fjord);color:#fff}
.tool.off{opacity:.3;pointer-events:none;filter:grayscale(100%);background:var(--bg)!important;border-color:rgba(0,0,0,.05)!important;color:var(--text3)!important}
.tool-e{font-size:.85rem}.tool-inf{flex:1}.tool-nm{font-size:.62rem;font-weight:600}.tool-ds{font-size:.5rem;color:var(--text3)}.tool-c{font-size:.52rem;font-weight:600;color:var(--sand)}
.log{font-size:.55rem;padding:.12rem 0;border-bottom:1px solid rgba(0,0,0,.02);color:var(--text2)}.log.bad{color:var(--coral)}.log.good{color:var(--moss)}
.ov{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:200;backdrop-filter:blur(3px)}
.ov-c{background:var(--white);border-radius:14px;padding:1.4rem;max-width:400px;width:90%;text-align:center}
.ov-c h2{font-size:1rem;font-weight:800;margin-bottom:.2rem}.ov-c p{font-size:.75rem;color:var(--text2);margin-bottom:.5rem;line-height:1.4}
.ov-btn{padding:.4rem 1rem;border:none;border-radius:6px;background:var(--fjord);color:#fff;font-size:.75rem;font-weight:700;cursor:pointer}
.hidden{display:none!important}
@media(max-width:800px){.main{grid-template-columns:1fr}}
</style>
</head>
<body>
<nav class="nav">
<a href="index.html" class="nav-brand"><img src="assets/img/logo.png" alt="">Flussmanagement</a>
<div style="display:flex;gap:.2rem"><button id="btn-reset" class="nav-btn">🔄 Neu</button><a href="index.html" class="nav-btn">← Zurück</a></div>
</nav>
<div id="game" class="main">
<div class="col-left">
<div class="panel"><div class="panel-h">Zustand</div><div id="params"></div></div>
<div class="panel"><div class="panel-h">Verlauf</div><canvas id="graph" style="width:100%;height:90px;display:block;border-radius:6px;background:#f4f2ee"></canvas></div>
<div class="panel"><div class="panel-h">Ereignisse</div><div id="log" style="max-height:100px;overflow-y:auto"></div></div>
</div>
<div class="col-center">
<div class="topbar">
<div class="stat gold">💰 <span class="v" id="h-bud">30</span></div>
<span class="inc" id="h-inc">+0/J</span>
<span class="wx" id="h-wx"></span>
<div class="stat yr">📅 <span class="v" id="h-yr">0</span></div>
<div class="spd"><button data-s="0"></button><button data-s="1" class="on"></button><button data-s="2">▶▶</button><button data-s="4">▶▶▶</button></div>
</div>
<div class="canvas-wrap"><canvas id="cv"></canvas></div>
</div>
<div class="col-right">
<div class="panel"><div class="panel-h">Werkzeug</div><div id="tools"></div></div>
<div class="panel"><div class="panel-h">Info</div><div id="info" style="font-size:.6rem;color:var(--text2)">Maus über die Karte bewegen.</div></div>
</div>
</div>
<div id="ov-intro" class="ov">
<div class="ov-c">
<div style="font-size:1.8rem;margin-bottom:.15rem">🏞️</div>
<h2>Flussmanagement</h2>
<p>Der Fluss lebt — er ändert jedes Jahr seinen Lauf! Baue Siedlungen und Felder, aber schütze sie mit Deichen. Begradige den Fluss wenn nötig, aber die Natur rächt sich flussabwärts.</p>
<button class="ov-btn" id="btn-go">Los geht's!</button>
</div>
</div>
<div id="ov-end" class="ov hidden">
<div class="ov-c"><h2 id="end-t"></h2><p id="end-p"></p><button class="ov-btn" id="btn-again">Nochmal</button></div>
</div>
<script>
// =====================================================================
// FLUSSMANAGEMENT v3 — Punkt-basierter Fluss mit Kreisen
// Fluss = 16 Punkte (Spaltenposition), verbunden durch Kreise
// Pro Jahr wandert EIN Punkt 1 Feld links/rechts
// =====================================================================
const COLS = 20, ROWS = 16
const MAX_YEARS = 100, MS_PER_TICK = 2500, WX_INTERVAL = 3
// Fluss: Array von 16 Spaltenpositionen (0..COLS-1)
let riverCol = [] // Ganzzahlige Zielpositionen (Spalte)
let riverColSmooth = [] // Smooth-interpolierte Anzeigepositionen (fliessend)
let riverEng = [] // Befestigung pro Segment (0=natur, 1=holz, 2=stein, 3=beton)
let riverRadius = [] // Radius pro Segment (variiert)
// Land-Grid
let land = [] // ROWS x COLS
let tilePop = [], tileCap = [], tileYield = []
let floodIntensity = [] // 0..1 smooth per tile
let tileFloodYears = [] // wie viele Jahre am Stueck ueberschwemmt
// Game
let budget = 30, year = 0, speed = 1, gameOver = false
let tickAccum = 0, wxTimer = 0, selectedTool = null
let eventLog = [], floats = [] // floating emojis
let history = [] // [{year, pop, food, bio, safety, economy}]
const WX = [
{n:'Dürre',e:'☀️',f:0.2,p:.12},{n:'Trocken',e:'🌤️',f:0.4,p:.25},
{n:'Normal',e:'⛅',f:0.8,p:.33},{n:'Regen',e:'🌧️',f:1.5,p:.20},{n:'Starkregen',e:'⛈️',f:2.8,p:.10}
]
let weather = WX[2]
function rollWx(){let r=Math.random(),c=0;for(const w of WX){c+=w.p;if(r<c)return w}return WX[2]}
const TOOLS = [
{id:'inspect',nm:'Experte fragen',e:'🧑‍🏫',ds:'Klicke irgendwo — der Experte erklärt die Lage.',cost:0},
{id:'levee',nm:'Deich',e:'🧱',ds:'Hält Fluss auf. Kann bei Dauerflut brechen.',cost:6},
{id:'field',nm:'Feld',e:'🌾',ds:'Ertrag je nach Feuchtigkeit. Stirbt nach 2J Flut.',cost:4},
{id:'settlement',nm:'Siedlung',e:'🏘️',ds:'Bewohner zahlen Steuern. Fliehen bei Flut.',cost:10},
{id:'forest',nm:'Wald',e:'🌳',ds:'Bremst Erosion, schützt Grundwasser. Trocknet neben Begradigung aus.',cost:5},
{id:'demolish',nm:'Abreißen',e:'🗑️',ds:'Entfernt alles — auch Begradigung!',cost:2},
{id:'straighten',nm:'Begradigen',e:'📏',ds:'Zieht Fluss zur Mitte. Fixiert ihn dort. Trocknet Umgebung aus!',cost:60},
]
function initGame() {
// Fluss: startet als Maeander
riverCol = [8,9,10,11,10,9,8,7,8,9,10,11,10,9,8,7]
riverColSmooth = riverCol.map(c => c) // Kopie fuer Animation
riverEng = new Array(ROWS).fill(0)
// Variierende Radien — Fluss ist nicht ueberall gleich breit
riverRadius = riverCol.map((_, i) => 0.8 + Math.sin(i * 0.9) * 0.3 + Math.random() * 0.2)
land = []; floodIntensity = []; tilePop = []; tileCap = []; tileYield = []; tileFloodYears = []
for (let r = 0; r < ROWS; r++) {
land[r] = []; floodIntensity[r] = []; tilePop[r] = []; tileCap[r] = []; tileYield[r] = []; tileFloodYears[r] = []
for (let c = 0; c < COLS; c++) {
floodIntensity[r][c] = 0; tilePop[r][c] = 0; tileCap[r][c] = 0; tileYield[r][c] = 0; tileFloodYears[r][c] = 0
// Wald weit vom Fluss, Wiese nah dran
const dist = Math.abs(c - riverCol[r])
if (dist <= 1) land[r][c] = 'meadow'
else if (dist > 4 && Math.random() < 0.5) land[r][c] = 'forest'
else land[r][c] = 'meadow'
}
}
// Startgebaeude
land[4][13] = 'settlement'; land[5][13] = 'field'; land[5][14] = 'field'
tilePop[4][13] = 3; tileCap[4][13] = 8
budget = 30; year = 0; speed = 1; gameOver = false
weather = WX[2]; wxTimer = 0; tickAccum = 0
eventLog = [{t:'🏞️ Willkommen! Der Fluss wandert — plane voraus!',c:'good'}]; history = []
selectedTool = null; floats = []
}
// === FLUSS-GEOMETRIE ===
// Abstand eines Tiles (r,c) zum Fluss (kontinuierlich, nicht nur ganzzahlig)
function distToRiver(r, c) {
// Naechster Punkt auf der Flusslinie
let minD = 999
for (let i = 0; i < ROWS; i++) {
const rc = riverColSmooth[i]
// Segment von Zeile i zu i+1
if (i < ROWS - 1) {
const rc2 = riverColSmooth[i + 1]
// Punkt-zu-Segment Abstand (vereinfacht: nehme naechsten Endpunkt)
const d1 = Math.sqrt((r - i) ** 2 + (c - rc) ** 2)
const d2 = Math.sqrt((r - (i + 1)) ** 2 + (c - rc2) ** 2)
minD = Math.min(minD, d1, d2)
} else {
minD = Math.min(minD, Math.sqrt((r - i) ** 2 + (c - rc) ** 2))
}
}
return minD
}
function isRiverTile(r, c) {
return distToRiver(r, c) < 1.3
}
// === TICK ===
function tick() {
if (gameOver) return
year++
// 1. Einkommen
let income = 0, totalPop = 0, totalFood = 0
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const dist = distToRiver(r, c)
const moisture = Math.max(0, 1 - Math.abs(dist - 2.5) / 3) // optimal bei ~2.5 Felder Abstand
const fi = floodIntensity[r][c]
if (land[r][c] === 'field') {
// Begradigung in der Naehe trocknet Feld aus
let dryPenalty = 0
for (let i = Math.max(0, r-1); i <= Math.min(ROWS-1, r+1); i++) {
if (riverEng[i] > 0) dryPenalty = Math.max(dryPenalty, riverEng[i] * 0.25)
}
const effMoisture = Math.max(0, moisture - dryPenalty)
// Ertrag: Basis-Feuchtigkeit + braucht Arbeiter in der Naehe
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<ROWS&&nc>=0&&nc<COLS&&land[nr][nc]==='settlement')
workerBonus += tilePop[nr][nc] * 0.15
}
workerBonus = Math.min(1, workerBonus) // max 100% Arbeiterbonus
const baseYield = (1 + effMoisture * 5) * (1 - fi * 0.9)
const y = Math.round(Math.max(0, Math.min(8, baseYield * (0.3 + workerBonus * 0.7))))
tileYield[r][c] = y; income += y * 0.5; totalFood += y
}
if (land[r][c] === 'settlement') {
tileCap[r][c] = 8
const cur = tilePop[r][c]
// Zuzug: braucht Nahrung UND trockenes Haus
const hasFood = totalFood > totalPop * 0.4
if (cur < 8 && hasFood && fi < 0.15) {
tilePop[r][c] = Math.min(8, cur + (Math.random() < 0.35 ? 1 : 0))
}
// Feuchtigkeit → Bewohner werden krank, fliehen langsam
if (fi > 0.2 && cur > 0 && Math.random() < fi * 0.2) {
tilePop[r][c] = Math.max(0, cur - 1)
}
totalPop += tilePop[r][c]
income += tilePop[r][c] * 0.8
}
}
budget += Math.round(income)
// 2. Wetter
wxTimer++; if (wxTimer >= WX_INTERVAL) { wxTimer = 0; weather = rollWx() }
// 3. FLUSS WANDERT — EIN Punkt pro Jahr
const idx = Math.floor(Math.random() * ROWS)
if (riverEng[idx] === 0) { // nur wenn nicht befestigt
const dir = Math.random() < 0.5 ? -1 : 1
const newCol = riverCol[idx] + dir
// Max 3 Felder Abstand zum Nachbarn
const prevOk = idx === 0 || Math.abs(newCol - riverCol[idx - 1]) <= 3
const nextOk = idx === ROWS - 1 || Math.abs(newCol - riverCol[idx + 1]) <= 3
if (newCol >= 1 && newCol < COLS - 1 && prevOk && nextOk) {
riverCol[idx] = newCol
// Was auf dem neuen Feld stand → zerstoeren
if (land[idx][newCol] === 'settlement') {
land[idx][newCol] = 'ruin'
eventLog.push({t: `🌊 Fluss verschlingt Siedlung! (Zeile ${idx+1})`, c: 'bad'})
} else if (land[idx][newCol] === 'field') {
land[idx][newCol] = 'meadow'
eventLog.push({t: `🌊 Fluss überspült Feld (Zeile ${idx+1})`, c: 'bad'})
} else if (land[idx][newCol] === 'levee') {
// Deich haelt — Fluss wird zurueckgedraengt
riverCol[idx] -= dir
eventLog.push({t: `🧱 Deich hält! (Zeile ${idx+1})`, c: 'good'})
}
}
}
// 4. Hochwasser + Trockenheit durch Begradigung
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const dist = distToRiver(r, c)
let target = 0
if (weather.f >= 1.0 && dist < 4) {
const base = Math.max(0, 1 - dist / 4)
const leveeProt = land[r][c] === 'levee' ? 0.9 : (hasLeveeNear(r, c) ? 0.5 : 0)
target = base * weather.f * 0.4 * (1 - leveeProt)
}
floodIntensity[r][c] += (target - floodIntensity[r][c]) * 0.2
// Flood-Jahre tracken
if (floodIntensity[r][c] > 0.35) {
tileFloodYears[r][c] = (tileFloodYears[r][c] || 0) + 1
} else {
tileFloodYears[r][c] = 0
}
// Feld stirbt nach 2 Jahren Dauerflut
if (land[r][c] === 'field' && tileFloodYears[r][c] >= 2) {
land[r][c] = 'meadow'
eventLog.push({t: `🌾💀 Feld ertrunken! (${c+1},${r+1})`, c: 'bad'})
}
// Siedlung: Bewohner fliehen bei Flut, Gebaeude wird Ruine nach 3J
if (land[r][c] === 'settlement' && floodIntensity[r][c] > 0.5 && Math.random() < 0.05) {
land[r][c] = 'ruin'; eventLog.push({t:`🏚️ Siedlung zerstört!`,c:'bad'})
}
// Wald stirbt bei Dauerflut
if (land[r][c] === 'forest' && tileFloodYears[r][c] >= 3) {
land[r][c] = 'dead_tree'
}
// Deich-Erosion bei Dauerflut
if (land[r][c] === 'levee' && floodIntensity[r][c] > 0.4 && Math.random() < 0.08) {
land[r][c] = 'meadow'; eventLog.push({t:'🧱💨 Deich weggespült!',c:'bad'})
}
}
// 4b. Begradigung trocknet Umgebung aus
for (let i = 0; i < ROWS; i++) {
if (riverEng[i] > 0) {
// 2 Felder links und rechts vom begradigten Fluss werden trocken
const rc = 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 // direkt am Fluss ist eh Wasser
// Baeume sterben langsam neben Begradigung
if (land[i][cc] === 'forest' && Math.random() < 0.03 * riverEng[i]) {
land[i][cc] = 'dead_tree'
eventLog.push({t:'🌳💀 Baum vertrocknet neben Begradigung',c:'bad'})
}
// Felder neben Begradigung produzieren weniger (wird in Yield-Berechnung beruecksichtigt)
}
}
}
// 5. Flussbreite bei Hochwasser
for (let i = 0; i < ROWS; i++) {
riverRadius[i] += ((weather.f > 1.2 ? 1.8 : 1.2) - riverRadius[i]) * 0.1
}
// 6. Verfall
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
if (land[r][c] === 'ruin' && Math.random() < 0.03) land[r][c] = 'meadow'
if (land[r][c] === 'dead_tree' && Math.random() < 0.03) land[r][c] = 'meadow'
}
// History fuer Graph speichern
const avgEng_=riverEng.reduce((s,v)=>s+v,0)/ROWS
const floodTot=floodIntensity.flat().reduce((s,v)=>s+v,0)/(ROWS*COLS)
const clp=v=>Math.min(100,Math.max(0,Math.round(v)))
let h_sett=0,h_fld=0,h_for=0,h_lev=0,h_ruin=0;for(let rr=0;rr<ROWS;rr++)for(let cc=0;cc<COLS;cc++){const tt=land[rr][cc];if(tt==='settlement')h_sett++;if(tt==='field')h_fld++;if(tt==='forest')h_for++;if(tt==='levee')h_lev++;if(tt==='ruin')h_ruin++}
history.push({
year,
pop:clp(totalPop*5-h_ruin*8),
food:clp(totalFood*1.5),
bio:clp(h_for*4-avgEng_*30-(h_sett+h_fld)*1.5),
safety:clp(15+h_lev*6+avgEng_*8-floodTot*80),
economy:clp(totalPop*3+h_fld*2-h_ruin*5)
})
if (year >= MAX_YEARS) { gameOver = true; showEnd() }
// Wetter-Emojis
if (weather.f >= 1.5) floats.push({x:Math.random(),y:-0.05,e:weather.f>=2.5?'⛈️':'🌧️',t:performance.now()})
if (weather.f <= 0.3) floats.push({x:Math.random()*0.5+0.25,y:-0.05,e:'☀️',t:performance.now()})
renderUI()
}
function hasLeveeNear(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&&land[nr][nc]==='levee') return true
}
return false
}
// === APPLY TOOL ===
function applyAt(gr, gc) {
if (gameOver || !selectedTool || gr<0 || gr>=ROWS || gc<0 || gc>=COLS) return
const t = selectedTool, cur = land[gr][gc]
const onRiver = isRiverTile(gr, gc)
const nearRiver = distToRiver(gr, gc) < 2.5
if (t === 'inspect') {
const d = distToRiver(gr, gc)
const fi = floodIntensity[gr]?.[gc] || 0
const fy = tileFloodYears[gr]?.[gc] || 0
if (onRiver) {
const eng = riverEng[gr]
const engNames = ['natürlich','Holzverbau','Bruchstein','Beton']
let msg = `🌊 Fluss — Befestigung: ${engNames[eng] || 'natürlich'}.`
if (eng > 0) msg += ` Fluss ist hier fixiert. Umgebung trocknet aus.`
else msg += ` Fließt frei — kann jedes Jahr wandern!`
eventLog.push({t: msg, c: ''})
} else if (cur === 'field') {
const yi = tileYield[gr]?.[gc] || 0
let msg = `🌾 Feld — Ertrag: ${yi}/8.`
if (fi > 0.35) msg += ` ⚠️ Abgesoffen! Kaum Ertrag. Noch ${2-fy} Jahr(e) bis es stirbt.`
else if (fi > 0.15) msg += ` 💧 Zu feucht — Ertrag reduziert.`
else if (d > 5) msg += ` 🥵 Sehr trocken — weit vom Fluss. Wenig Ertrag.`
else if (d > 3.5) msg += ` Etwas trocken. Näherer Standort wäre besser.`
else msg += ` 👍 Gute Feuchtigkeit!`
let hasWorkers = false
for(let dr=-2;dr<=2;dr++)for(let dc=-2;dc<=2;dc++){const nr=gr+dr,nc=gc+dc;if(nr>=0&&nr<ROWS&&nc>=0&&nc<COLS&&land[nr][nc]==='settlement'&&tilePop[nr][nc]>0)hasWorkers=true}
if (!hasWorkers) msg += ` ⚠️ Keine Arbeiter! Siedlung in der Nähe nötig.`
eventLog.push({t: msg, c: yi > 4 ? 'good' : yi > 1 ? '' : 'bad'})
} else if (cur === 'settlement') {
const pop = tilePop[gr]?.[gc] || 0
let msg = `🏘️ Siedlung — ${pop}/8 Bewohner.`
if (fi > 0.3) msg += ` 🤒 Feucht! Bewohner werden krank und fliehen.`
else if (fi > 0.1) msg += ` 😰 Etwas feucht. Manche Bewohner sind unwohl.`
else msg += ` Trocken und sicher.`
if (pop < 8) msg += ` Neue Bewohner kommen, wenn genug Nahrung da ist.`
eventLog.push({t: msg, c: fi > 0.2 ? 'bad' : 'good'})
} else if (cur === 'levee') {
let msg = `🧱 Deich.`
if (fi > 0.3) msg += ` ⚠️ Unter Druck! Kann bei Dauerflut brechen.`
else msg += ` Intakt. Schützt dahinterliegende Flächen.`
eventLog.push({t: msg, c: fi > 0.3 ? 'bad' : 'good'})
} else if (cur === 'forest') {
let msg = `🌳 Wald — schützt vor Erosion, speichert Grundwasser.`
let nearStraight = false
for(let i=Math.max(0,gr-1);i<=Math.min(ROWS-1,gr+1);i++){if(riverEng[i]>0)nearStraight=true}
if (nearStraight) msg += ` ⚠️ Neben Begradigung — trocknet aus, Baum in Gefahr!`
if (fi > 0.2) msg += ` 💧 Feucht. Bei Dauerflut stirbt der Baum.`
eventLog.push({t: msg, c: nearStraight ? 'bad' : 'good'})
} else if (cur === 'ruin') {
eventLog.push({t: `🏚️ Ruine — ehemaliges Gebäude. Kann abgerissen werden (💰2). Verfällt mit der Zeit.`, c: 'bad'})
} else if (cur === 'dead_tree') {
eventLog.push({t: `🪵 Toter Baum — vertrocknet oder ertrunken. Verrottet langsam.`, c: 'bad'})
} else {
let msg = `🌱 Wiese — freie Fläche.`
if (d < 2.5) msg += ` Nah am Fluss: fruchtbar, aber hochwassergefährdet.`
else if (d < 4) msg += ` Gute Lage für Feld oder Siedlung.`
else msg += ` Weit vom Fluss. Trocken.`
eventLog.push({t: msg, c: ''})
}
renderUI(); return
}
if (t === 'straighten') {
if (!onRiver) return
if (budget < 60) return
// Begradigung: Fluss wird zur Bildmitte (Spalte 10) gezogen
riverEng[gr] = Math.min(3, riverEng[gr] + 1)
riverCol[gr] = COLS / 2 // Mitte
budget -= 60
eventLog.push({t:`📏 Fluss begradigt zur Mitte (Zeile ${gr+1}) — 💰60`,c:''})
} else if (t === 'levee') {
if (onRiver) return
const dRiv = distToRiver(gr, gc)
if (dRiv > 4) { eventLog.push({t:'🧱 Zu weit vom Fluss! Deich muss in Flussnähe stehen.',c:'bad'}); renderUI(); return }
if (cur !== 'meadow' && cur !== 'forest') return
if (budget < 6) return
land[gr][gc] = 'levee'; budget -= 6
} else if (t === 'field') {
if (onRiver) return
if (cur !== 'meadow') return
if (budget < 4) return
land[gr][gc] = 'field'; budget -= 4
// Kontextuelle Rueckmeldung
const d = distToRiver(gr, gc)
if (d < 2.5) eventLog.push({t:'🌾 Feld angelegt — schön nah am Wasser! Guter Ertrag, aber bei Hochwasser gefährdet.',c:'good'})
else if (d < 4) eventLog.push({t:'🌾 Feld angelegt — gute Lage. Genug Feuchtigkeit, sicher vor dem Fluss.',c:'good'})
else eventLog.push({t:'🌾 Feld angelegt — weit vom Fluss! Hier wird es trocken. Ertrag wird gering sein.',c:'bad'})
// Arbeiter-Hinweis
let hasWorkers = false
for(let dr=-2;dr<=2;dr++)for(let dc=-2;dc<=2;dc++){const nr=gr+dr,nc=gc+dc;if(nr>=0&&nr<ROWS&&nc>=0&&nc<COLS&&land[nr][nc]==='settlement'&&tilePop[nr][nc]>0)hasWorkers=true}
if(!hasWorkers) eventLog.push({t:'⚠️ Keine Siedlung in der Nähe! Ohne Arbeiter nur 30% Ertrag.',c:'bad'})
} else if (t === 'settlement') {
if (onRiver) return
if (cur !== 'meadow' && cur !== 'field') return
if (budget < 10) return
land[gr][gc] = 'settlement'; tilePop[gr][gc] = 0; tileCap[gr][gc] = 8; budget -= 10
const d = distToRiver(gr, gc)
if (d < 2) eventLog.push({t:'🏘️ Siedlung gebaut — sehr nah am Fluss! Hochwassergefahr! Deich empfohlen.',c:'bad'})
else if (d < 4) eventLog.push({t:'🏘️ Siedlung gebaut — gute Lage. Bewohner ziehen bald ein, wenn Nahrung da ist.',c:'good'})
else eventLog.push({t:'🏘️ Siedlung gebaut — sicher, aber weit weg. Felder in der Nähe anlegen!',c:''})
} else if (t === 'forest') {
if (onRiver) return
if (cur !== 'meadow') return
if (budget < 5) return
land[gr][gc] = 'forest'; budget -= 5
eventLog.push({t:'🌳 Wald gepflanzt — schützt vor Erosion und speichert Grundwasser.',c:'good'})
} else if (t === 'demolish') {
// Begradigung abreissen: auf Fluss klicken
if (onRiver && riverEng[gr] > 0) {
if (budget < 2) return
riverEng[gr] = 0 // Befestigung entfernt → Fluss wandert dort wieder frei
budget -= 2
eventLog.push({t:'🗑️ Begradigung entfernt (Zeile '+( gr+1)+')',c:''})
renderUI(); return
}
if (!['field','settlement','levee','ruin','dead_tree','forest'].includes(cur)) return
if (budget < 2) return
land[gr][gc] = 'meadow'; budget -= 2
}
renderUI()
}
// === CANVAS ===
const $cv = document.getElementById('cv'), ctx = $cv.getContext('2d')
let W=0, H=0, tw=0, th=0, animT=0
function resize() {
const r=$cv.parentElement.getBoundingClientRect(), dpr=devicePixelRatio||1
$cv.width=r.width*dpr;$cv.height=r.height*dpr;ctx.setTransform(dpr,0,0,dpr,0,0)
W=r.width;H=r.height;tw=W/COLS;th=H/ROWS
}
function draw() {
animT += 0.012
const s = Math.min(tw, th) * 0.3
// Background
ctx.fillStyle='#c8d8b0'; ctx.fillRect(0,0,W,H)
// === TILES ===
for (let r=0;r<ROWS;r++) for (let c=0;c<COLS;c++) {
const x=c*tw, y=r*th, tile=land[r][c]
const dist=distToRiver(r,c)
const fi=floodIntensity[r]?.[c]||0
const moisture=Math.max(0,1-dist/5) * 0.2 // permanente Feuchtigkeit nahe Fluss
// Grundfarbe
let bR=155,bG=185,bB=120 // meadow
if(tile==='forest'){bR=55;bG=115;bB=45}
if(tile==='field'){bR=185;bG=170;bB=75}
if(tile==='settlement'){bR=175;bG=150;bB=115}
if(tile==='levee'){bR=135;bG=110;bB=80}
if(tile==='ruin'){bR=125;bG=110;bB=95}
if(tile==='dead_tree'){bR=105;bG=125;bB=75}
// Feuchtigkeit + Hochwasser → Blau-Shift
const blue = Math.min(1, moisture + fi * 0.7)
const fR=bR*(1-blue)+65*blue|0
const fG=bG*(1-blue*0.4)+135*blue*0.4|0
const fB=bB*(1-blue)+185*blue|0
ctx.fillStyle=`rgb(${fR},${fG},${fB})`
ctx.fillRect(x,y,tw+0.5,th+0.5)
// Tile-Objekte
const cx=x+tw/2, cy=y+th/2
if(tile==='forest'){
ctx.fillStyle='#4a3020';ctx.fillRect(cx-s*0.1,cy-s*0.2,s*0.2,s*0.5)
ctx.fillStyle=r<ROWS*0.3?'#1a5a2a':'#2a7a2a'
ctx.beginPath();ctx.arc(cx,cy-s*0.4,s*0.45,0,Math.PI*2);ctx.fill()
}
if(tile==='settlement'){
// 3x3 Raster: Personen aussen, Haus-Emoji in der Mitte
// Personen-Emojis je nach Gesundheit (Feuchtigkeit des Hauses)
const pop=tilePop[r]?.[c]||0
const efs=Math.max(6,Math.min(12,tw*0.28))
ctx.font=`${efs}px serif`;ctx.textAlign='center';ctx.textBaseline='middle'
const gx=tw*0.32, gy=th*0.32
const healthy=['👩','👨','👧','👦','👩‍🦱','👨‍🦰','👱‍♀️','🧑']
const sick=['😰','🤒','🤢','😷','🤒','😰','🤢','😷']
for(let gr_=0;gr_<3;gr_++)for(let gc_=0;gc_<3;gc_++){
const idx=gr_*3+gc_
if(idx===4){ctx.fillText('🏘️',cx,cy)}
else{
const slotIdx=idx>4?idx-1:idx
if(slotIdx<pop){
// Gesund, leicht krank, schwer krank je nach Feuchtigkeit
let emoji
if(fi>0.35) emoji=sick[slotIdx%8] // schwer krank
else if(fi>0.15) emoji=(slotIdx%2===0)?sick[slotIdx%8]:healthy[slotIdx%8] // manche krank
else emoji=healthy[slotIdx%8] // alle gesund
ctx.fillText(emoji,cx+(gc_-1)*gx,cy+(gr_-1)*gy)
}
}
}
ctx.textBaseline='alphabetic'
}
if(tile==='field'){
// 3x3 Emoji-Raster: Weizen aussen, Status-Emoji in der Mitte
const yi=tileYield[r]?.[c]||0
const dist_=distToRiver(r,c)
let dryPen=0; for(let ii=Math.max(0,r-1);ii<=Math.min(ROWS-1,r+1);ii++){if(riverEng[ii]>0)dryPen=Math.max(dryPen,riverEng[ii]*0.25)}
const moist=Math.max(0,(1-Math.abs(dist_-2.5)/3)-dryPen)
// Status: 💧=nass, 👍=gut, 🥵=trocken
let status; if(fi>0.25) status='💧'; else if(moist>0.4) status='👍'; else status='🥵'
// 8 Positionen um die Mitte: Weizen oder Wasser
// Bei Ueberschwemmung ersetzen Wasser-Emojis den Weizen von unten
const waterSlots = fi > 0.15 ? Math.min(8, Math.round(fi * 12)) : 0
const wheatSlots = Math.min(8, yi) - waterSlots
const grid3 = [] // 9 Positionen: 0-8, Mitte=4
for(let p=0;p<9;p++){
if(p===4){grid3[p]=status; continue}
const slotIdx=p>4?p-1:p // 0-7 fuer die 8 Aussenpositionen
if(slotIdx<Math.max(0,wheatSlots)) grid3[p]='🌾'
else if(slotIdx<Math.max(0,wheatSlots)+waterSlots) grid3[p]='💧'
else grid3[p]=''
}
const efs=Math.max(6,Math.min(12,tw*0.28))
ctx.font=`${efs}px serif`;ctx.textAlign='center';ctx.textBaseline='middle'
const gx=tw*0.32, gy=th*0.32
for(let gr_=0;gr_<3;gr_++)for(let gc_=0;gc_<3;gc_++){
const idx=gr_*3+gc_
if(grid3[idx]){ctx.fillText(grid3[idx],cx+(gc_-1)*gx,cy+(gr_-1)*gy)}
}
ctx.textBaseline='alphabetic'
}
if(tile==='levee'){
// Deich — wird schmaler bei Erosion (hohe flood intensity)
const w=s*0.8*(1-fi*0.5)
ctx.fillStyle=fi>0.3?'rgba(140,115,85,0.5)':'rgba(140,115,85,0.9)'
ctx.fillRect(cx-w,cy-s*0.1,w*2,s*0.25)
}
if(tile==='ruin'){
ctx.fillStyle='rgba(100,85,70,0.5)';ctx.fillRect(cx-s*0.3,cy-s*0.15,s*0.6,s*0.3)
}
if(tile==='dead_tree'){
ctx.strokeStyle='#2a2a2a';ctx.lineWidth=Math.max(0.5,s*0.1)
ctx.beginPath();ctx.moveTo(cx,cy+s*0.2);ctx.lineTo(cx+s*0.05,cy-s*0.3)
ctx.moveTo(cx+s*0.03,cy-s*0.15);ctx.lineTo(cx-s*0.2,cy-s*0.4)
ctx.stroke()
}
}
// Gitter (dezent)
ctx.strokeStyle='rgba(0,0,0,0.04)';ctx.lineWidth=0.3
for(let r=0;r<=ROWS;r++){ctx.beginPath();ctx.moveTo(0,r*th);ctx.lineTo(W,r*th);ctx.stroke()}
for(let c=0;c<=COLS;c++){ctx.beginPath();ctx.moveTo(c*tw,0);ctx.lineTo(c*tw,H);ctx.stroke()}
// === FLUSS — fliessender Verlauf aus interpolierten Kreisen ===
// Smooth-Animation: riverColSmooth wandert langsam zu riverCol
for (let i = 0; i < ROWS; i++) {
riverColSmooth[i] += (riverCol[i] - riverColSmooth[i]) * 0.06
}
// Virtuelle Punkte oben und unten (damit der Fluss nicht im Nichts beginnt)
const pts = [] // {x (pixel), y (pixel), rad (pixel), eng}
// Virtueller Punkt oberhalb
pts.push({
x: riverColSmooth[0] * tw + tw/2,
y: -th,
rad: riverRadius[0] * tw * 0.45,
eng: riverEng[0]
})
// Alle echten Punkte
for (let i = 0; i < ROWS; i++) {
pts.push({
x: riverColSmooth[i] * tw + tw/2,
y: i * th + th/2,
rad: riverRadius[i] * tw * 0.45,
eng: riverEng[i]
})
}
// Virtueller Punkt unterhalb
pts.push({
x: riverColSmooth[ROWS-1] * tw + tw/2,
y: H + th,
rad: riverRadius[ROWS-1] * tw * 0.45,
eng: riverEng[ROWS-1]
})
// Fluss zeichnen: Kreise entlang der Verbindungen, Radius interpoliert
const totalPts = pts.length
for (let i = 0; i < totalPts - 1; i++) {
const p0 = pts[i], p1 = pts[i+1]
const steps = 12
for (let s = 0; s <= steps; s++) {
const t = s / steps
const px = p0.x + (p1.x - p0.x) * t
const py = p0.y + (p1.y - p0.y) * t
const pr = p0.rad + (p1.rad - p0.rad) * t // Radius interpoliert
const rr = pr + Math.sin(animT * 1.8 + py * 0.01) * pr * 0.06 // leichtes Pulsieren
// Wasser
ctx.fillStyle = 'rgba(50,118,172,0.88)'
ctx.beginPath(); ctx.arc(px, py, rr, 0, Math.PI * 2); ctx.fill()
}
}
// Schimmer/Stroemung ueber den ganzen Fluss
ctx.strokeStyle = `rgba(255,255,255,${0.06 + Math.sin(animT * 2) * 0.03})`; ctx.lineWidth = 0.8
for (let k = 0; k < 8; k++) {
const t = (k / 8 + animT * 0.02) % 1
const idx = t * (totalPts - 1)
const i = Math.floor(idx), frac = idx - i
if (i >= totalPts - 1) continue
const px = pts[i].x + (pts[i+1].x - pts[i].x) * frac
const py = pts[i].y + (pts[i+1].y - pts[i].y) * frac
const pr = pts[i].rad + (pts[i+1].rad - pts[i].rad) * frac
ctx.beginPath(); ctx.moveTo(px - pr * 0.4, py); ctx.lineTo(px + pr * 0.4, py + 1); ctx.stroke()
}
// Uferbefestigung bei begradigten Segmenten
for (let i = 1; i < totalPts - 1; i++) {
const p = pts[i]
const eng = p.eng
if (eng > 0) {
const engColors = ['','rgba(130,100,50,0.5)','rgba(110,110,110,0.6)','rgba(160,160,160,0.7)']
ctx.strokeStyle = engColors[Math.min(3, eng)]
ctx.lineWidth = 1.5 + eng * 1.2
const segH = th * 0.5
ctx.beginPath(); ctx.moveTo(p.x - p.rad - 2, p.y - segH); ctx.lineTo(p.x - p.rad - 2, p.y + segH); ctx.stroke()
ctx.beginPath(); ctx.moveTo(p.x + p.rad + 2, p.y - segH); ctx.lineTo(p.x + p.rad + 2, p.y + segH); ctx.stroke()
}
}
// === WOLKEN ===
for(let i=0;i<3;i++){
const cx=((animT*(10+i*4)+i*250)%(W+120))-60
const cy=10+i*18+Math.sin(animT*0.3+i)*4
const isRain=weather.f>=1.5&&i<2
ctx.fillStyle=isRain?'rgba(90,90,110,0.3)':'rgba(255,255,255,0.35)'
ctx.beginPath();ctx.ellipse(cx,cy,22,8,0,0,Math.PI*2);ctx.fill()
ctx.beginPath();ctx.ellipse(cx-10,cy+2,16,7,0,0,Math.PI*2);ctx.fill()
ctx.beginPath();ctx.ellipse(cx+12,cy+1,18,7,0,0,Math.PI*2);ctx.fill()
if(isRain){ctx.strokeStyle='rgba(80,130,180,0.2)';ctx.lineWidth=0.5
for(let j=0;j<4;j++){const dx=cx-8+j*5;ctx.beginPath();ctx.moveTo(dx,cy+8);ctx.lineTo(dx-1,cy+14+Math.random()*3);ctx.stroke()}}
}
// Floating emojis
const now=performance.now()
for(let i=floats.length-1;i>=0;i--){
const f=floats[i],age=(now-f.t)/1000
if(age>3){floats.splice(i,1);continue}
f.y-=0.0004
ctx.globalAlpha=Math.max(0,1-age/3)
ctx.font=`${12+age*3}px serif`;ctx.textAlign='center'
ctx.fillText(f.e,f.x*W,f.y*H)
ctx.globalAlpha=1
}
requestAnimationFrame(draw)
}
// === RENDER UI ===
function renderUI(){
let inc=0,pop=0,food=0,sett=0,fld=0,for_=0,lev=0,ruin=0
for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
const t=land[r][c];if(t==='settlement'){sett++;pop+=tilePop[r][c]}
if(t==='field'){fld++;food+=tileYield[r][c];inc+=tileYield[r][c]*0.5}
if(t==='forest')for_++;if(t==='levee')lev++;if(t==='ruin')ruin++
if(t==='settlement')inc+=tilePop[r][c]*0.8
}
document.getElementById('h-bud').textContent=budget
document.getElementById('h-inc').textContent=`+${Math.round(inc)}/J`
document.getElementById('h-yr').textContent=`${year}/${MAX_YEARS}`
document.getElementById('h-wx').textContent=`${weather.e} ${weather.n}`
const avgEng=riverEng.reduce((s,v)=>s+v,0)/ROWS
const floodTotal=floodIntensity.flat().reduce((s,v)=>s+v,0)/(ROWS*COLS)
const clamp=v=>Math.min(100,Math.max(0,Math.round(v)))
const ps=[
{n:`Bevölkerung (${pop})`,i:'👥',v:pop,abs:true}, // absolut, nicht 0-100
{n:'Nahrung',i:'🌾',v:clamp(food*1.5-(weather.f<0.3?20:0))},
{n:'Biodiversität',i:'🦎',v:clamp(for_*4-avgEng*30-(sett+fld)*1.5)},
{n:'Hochwasserschutz',i:'🛡️',v:clamp(15+lev*6+avgEng*8-floodTotal*80)},
{n:'Wirtschaft',i:'💰',v:clamp(pop*3+fld*2-ruin*5)},
]
document.getElementById('params').innerHTML=ps.map(p=>{
const col=p.v<30?'var(--coral)':p.v>55?'var(--moss)':'var(--sand)'
return `<div class="param"><div class="param-i">${p.i}</div><div class="param-inf"><div class="param-n">${p.n}</div><div class="param-bar"><div class="param-bar-f" style="width:${p.v}%;background:${col}"></div></div></div><div class="param-v" style="color:${col}">${p.v}</div></div>`
}).join('')
document.getElementById('log').innerHTML=eventLog.slice(-10).reverse().map(e=>`<div class="log ${e.c}">${e.t}</div>`).join('')
// Wenn selektiertes Tool nicht mehr leistbar → abwaehlen
if(selectedTool){const st=TOOLS.find(t=>t.id===selectedTool);if(st&&budget<st.cost)selectedTool=null}
document.getElementById('tools').innerHTML=TOOLS.map(t=>{
const af=budget>=t.cost;return `<div class="tool ${selectedTool===t.id&&af?'on':''} ${!af?'off':''}" data-t="${t.id}"><span class="tool-e">${t.e}</span><div class="tool-inf"><div class="tool-nm">${t.nm}</div><div class="tool-ds">${t.ds}</div></div><span class="tool-c">💰${t.cost}</span></div>`
}).join('')
document.querySelectorAll('.tool').forEach(b=>b.addEventListener('click',()=>{selectedTool=selectedTool===b.dataset.t?null:b.dataset.t;renderUI()}))
// === GRAPH ===
drawGraph()
}
function drawGraph(){
const gc=document.getElementById('graph')
if(!gc||history.length<2)return
const gctx=gc.getContext('2d')
const rect=gc.parentElement.getBoundingClientRect()
const dpr=devicePixelRatio||1
gc.width=rect.width*dpr;gc.height=90*dpr;gctx.setTransform(dpr,0,0,dpr,0,0)
const gW=rect.width,gH=90
gctx.clearRect(0,0,gW,gH)
// Hintergrund-Zonen (Toleranzen)
gctx.fillStyle='rgba(180,60,50,0.06)';gctx.fillRect(0,gH*0.75,gW,gH*0.25) // Gefahrenzone unten
gctx.fillStyle='rgba(90,140,90,0.06)';gctx.fillRect(0,0,gW,gH*0.35) // Gute Zone oben
gctx.fillStyle='rgba(200,180,80,0.06)';gctx.fillRect(0,gH*0.35,gW,gH*0.4) // Mittelzone
// Trennlinien
gctx.strokeStyle='rgba(0,0,0,0.06)';gctx.lineWidth=0.5;gctx.setLineDash([3,3])
gctx.beginPath();gctx.moveTo(0,gH*0.35);gctx.lineTo(gW,gH*0.35);gctx.stroke()
gctx.beginPath();gctx.moveTo(0,gH*0.75);gctx.lineTo(gW,gH*0.75);gctx.stroke()
gctx.setLineDash([])
// Labels
gctx.font='7px Inter,sans-serif';gctx.fillStyle='rgba(0,0,0,0.2)';gctx.textAlign='right'
gctx.fillText('gut',gW-3,gH*0.2);gctx.fillText('kritisch',gW-3,gH*0.88)
const lines=[
{key:'pop',color:'#6a6aaa',label:'👥'},
{key:'food',color:'#8a9a30',label:'🌾'},
{key:'bio',color:'#3a8a3a',label:'🦎'},
{key:'safety',color:'#4a7c8a',label:'🛡️'},
{key:'economy',color:'#c4a35a',label:'💰'},
]
// Auto-scale: finde max ueber alle Werte
let maxVal=100
for(const h of history)for(const l of lines){const v=h[l.key]||0;if(v>maxVal)maxVal=v}
maxVal=Math.max(100,maxVal*1.1)
const pad=2,plotW=gW-pad*2,plotH=gH-pad*2
for(const line of lines){
gctx.strokeStyle=line.color;gctx.lineWidth=1.2;gctx.beginPath()
history.forEach((h,i)=>{
const x=pad+(i/(Math.max(1,MAX_YEARS-1)))*plotW
const y=pad+plotH-(((h[line.key]||0)/maxVal)*plotH)
i===0?gctx.moveTo(x,y):gctx.lineTo(x,y)
})
gctx.stroke()
// Label am Ende
if(history.length>0){
const last=history[history.length-1]
const lx=pad+((history.length-1)/(Math.max(1,MAX_YEARS-1)))*plotW
const ly=pad+plotH-(((last[line.key]||0)/maxVal)*plotH)
gctx.font='8px serif';gctx.textAlign='left';gctx.fillStyle=line.color
gctx.fillText(line.label,lx+3,ly+3)
}
}
}
// === EVENTS ===
const $cv2=$cv
$cv2.addEventListener('click',e=>{
const r=$cv2.getBoundingClientRect()
const gc=Math.floor((e.clientX-r.left)/tw), gr=Math.floor((e.clientY-r.top)/th)
applyAt(gr,gc)
})
$cv2.addEventListener('mousemove',e=>{
const r=$cv2.getBoundingClientRect()
const gc=Math.floor((e.clientX-r.left)/tw), gr=Math.floor((e.clientY-r.top)/th)
if(gr>=0&&gr<ROWS&&gc>=0&&gc<COLS){
const t=land[gr][gc],dist=distToRiver(gr,gc).toFixed(1)
const onR=isRiverTile(gr,gc)
let info=onR?`🌊 Fluss (Befestigung: ${riverEng[gr]})`:
{meadow:'🌱 Wiese',forest:'🌳 Wald',field:`🌾 Feld (Ertrag: ${tileYield[gr]?.[gc]||0})`,
settlement:`🏠 Siedlung (${tilePop[gr]?.[gc]||0}/8)`,levee:'🧱 Deich',ruin:'🏚️ Ruine',dead_tree:'🪵 Toter Baum'}[t]||t
info+=`<br>Flussabstand: ${dist}`
document.getElementById('info').innerHTML=info
}
})
document.querySelectorAll('.spd button').forEach(b=>b.addEventListener('click',()=>{
speed=parseInt(b.dataset.s);document.querySelectorAll('.spd button').forEach(x=>x.classList.remove('on'));b.classList.add('on')
}))
function showEnd(){
document.getElementById('end-t').textContent='100 Jahre sind um!'
let p=0;for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)p+=tilePop[r][c]
document.getElementById('end-p').textContent=`💰${budget} | 👥${p} Bewohner | 🏠${land.flat().filter(t=>t==='settlement').length} Siedlungen`
document.getElementById('ov-end').classList.remove('hidden')
}
document.getElementById('btn-go').addEventListener('click',()=>{document.getElementById('ov-intro').classList.add('hidden');renderUI()})
document.getElementById('btn-again').addEventListener('click',()=>{document.getElementById('ov-end').classList.add('hidden');initGame();renderUI()})
document.getElementById('btn-reset').addEventListener('click',()=>{if(confirm('Neu?')){initGame();renderUI()}})
// === GAME LOOP ===
let lastT=0
function loop(ts){if(!lastT)lastT=ts;const dt=ts-lastT;lastT=ts
if(speed>0&&!gameOver){tickAccum+=dt*speed;while(tickAccum>=MS_PER_TICK){tickAccum-=MS_PER_TICK;tick()}}
requestAnimationFrame(loop)}
initGame();resize();window.addEventListener('resize',resize);renderUI();draw();requestAnimationFrame(loop)
</script>
</body>
</html>