00df599197
- distToRiver: Distance-Cache pro Tick statt O(ROWS) pro Tile pro Frame - eventLog: Gekappt auf 50 Eintraege - Reduziert CPU-Last von ~300k Berechnungen/sec auf ~5k/sec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
873 lines
41 KiB
HTML
873 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 ===
|
|
// Distance-Cache: wird einmal pro Tick aktualisiert statt pro Frame pro Tile
|
|
let distCache = []
|
|
function rebuildDistCache() {
|
|
for (let r = 0; r < ROWS; r++) {
|
|
if (!distCache[r]) distCache[r] = []
|
|
for (let c = 0; c < COLS; c++) distCache[r][c] = _calcDist(r, c)
|
|
}
|
|
}
|
|
function distToRiver(r, c) {
|
|
if (distCache[r] && distCache[r][c] !== undefined) return distCache[r][c]
|
|
return _calcDist(r, c)
|
|
}
|
|
function _calcDist(r, c) {
|
|
let minD = 999
|
|
for (let i = 0; i < ROWS; i++) {
|
|
const rc = riverColSmooth[i]
|
|
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++
|
|
rebuildDistCache() // Cache neu bauen (einmal pro Tick statt pro Frame pro Tile)
|
|
|
|
// EventLog cappen
|
|
if (eventLog.length > 50) eventLog.length = 50
|
|
|
|
// 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>
|