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>
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* SIM-12: Flussmanagement — Canvas Renderer
|
||||
*
|
||||
* Zeichnet eine Landschaft mit Fluss, Vegetation, Siedlung, Auen.
|
||||
* Alle visuellen Elemente reagieren auf den Simulationszustand.
|
||||
*/
|
||||
|
||||
import type { State, Controls } from './logic'
|
||||
|
||||
export class FlussRenderer {
|
||||
private ctx: CanvasRenderingContext2D
|
||||
private w: number
|
||||
private h: number
|
||||
private time = 0
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
this.ctx = canvas.getContext('2d')!
|
||||
this.w = canvas.width
|
||||
this.h = canvas.height
|
||||
}
|
||||
|
||||
resize(): void {
|
||||
const rect = this.canvas.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
this.canvas.width = rect.width * dpr
|
||||
this.canvas.height = rect.height * dpr
|
||||
this.ctx.scale(dpr, dpr)
|
||||
this.w = rect.width
|
||||
this.h = rect.height
|
||||
}
|
||||
|
||||
render(state: State, controls: Controls): void {
|
||||
this.time += 0.02
|
||||
const ctx = this.ctx
|
||||
const w = this.w
|
||||
const h = this.h
|
||||
|
||||
// Hintergrund — Himmel
|
||||
const skyGrad = ctx.createLinearGradient(0, 0, 0, h * 0.4)
|
||||
skyGrad.addColorStop(0, '#87CEEB')
|
||||
skyGrad.addColorStop(1, '#B0E0E6')
|
||||
ctx.fillStyle = skyGrad
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
// Grund — Erde/Gras
|
||||
const groundY = h * 0.35
|
||||
const groundGrad = ctx.createLinearGradient(0, groundY, 0, h)
|
||||
const greenIntensity = Math.round(80 + state.biodiversity * 0.8)
|
||||
groundGrad.addColorStop(0, `rgb(${120 - state.soilFertility * 0.3}, ${greenIntensity}, ${60 - state.erosion * 0.3})`)
|
||||
groundGrad.addColorStop(1, `rgb(${140 - state.soilFertility * 0.2}, ${100 + state.biodiversity * 0.4}, ${70})`)
|
||||
ctx.fillStyle = groundGrad
|
||||
ctx.fillRect(0, groundY, w, h - groundY)
|
||||
|
||||
// Berge im Hintergrund
|
||||
this.drawMountains(ctx, w, h)
|
||||
|
||||
// Auen-Bereich (wenn freigegeben)
|
||||
if (controls.floodplainRelease > 10) {
|
||||
this.drawFloodplains(ctx, w, h, controls.floodplainRelease, state)
|
||||
}
|
||||
|
||||
// Fluss zeichnen
|
||||
this.drawRiver(ctx, w, h, state, controls)
|
||||
|
||||
// Deiche
|
||||
if (controls.levees > 10) {
|
||||
this.drawLevees(ctx, w, h, controls.levees)
|
||||
}
|
||||
|
||||
// Hochwasser-Overlay
|
||||
if (state.floodLocal > 50) {
|
||||
this.drawFlood(ctx, w, h, state.floodLocal)
|
||||
}
|
||||
|
||||
// Vegetation / Baeume (Biodiversitaet)
|
||||
this.drawVegetation(ctx, w, h, state.biodiversity, state.soilFertility)
|
||||
|
||||
// Siedlung
|
||||
this.drawSettlement(ctx, w, h, state.economy, state.usableLand)
|
||||
|
||||
// Felder (Landwirtschaft)
|
||||
this.drawFarms(ctx, w, h, state.soilFertility, controls.irrigation)
|
||||
|
||||
// Erosionsspuren
|
||||
if (state.erosion > 40) {
|
||||
this.drawErosion(ctx, w, h, state.erosion)
|
||||
}
|
||||
|
||||
// Wolken
|
||||
this.drawClouds(ctx, w, h)
|
||||
}
|
||||
|
||||
private drawMountains(ctx: CanvasRenderingContext2D, w: number, h: number): void {
|
||||
const baseY = h * 0.35
|
||||
ctx.fillStyle = '#8BA89A'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, baseY)
|
||||
ctx.lineTo(w * 0.1, baseY - h * 0.15)
|
||||
ctx.lineTo(w * 0.2, baseY - h * 0.08)
|
||||
ctx.lineTo(w * 0.35, baseY - h * 0.2)
|
||||
ctx.lineTo(w * 0.5, baseY - h * 0.05)
|
||||
ctx.lineTo(w * 0.65, baseY - h * 0.18)
|
||||
ctx.lineTo(w * 0.8, baseY - h * 0.1)
|
||||
ctx.lineTo(w * 0.9, baseY - h * 0.14)
|
||||
ctx.lineTo(w, baseY)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// Schneedecke
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.5)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(w * 0.33, baseY - h * 0.2)
|
||||
ctx.lineTo(w * 0.35, baseY - h * 0.2)
|
||||
ctx.lineTo(w * 0.37, baseY - h * 0.17)
|
||||
ctx.lineTo(w * 0.31, baseY - h * 0.17)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
private drawRiver(ctx: CanvasRenderingContext2D, w: number, h: number, state: State, controls: Controls): void {
|
||||
const riverWidth = 20 + (100 - controls.straightening) * 0.15
|
||||
const meander = (100 - controls.straightening) * 0.4 // Maeander-Amplitude
|
||||
const riverY = h * 0.55
|
||||
|
||||
// Flussverlauf (von links nach rechts)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, riverY)
|
||||
|
||||
const steps = 50
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const x = (i / steps) * w
|
||||
const progress = i / steps
|
||||
const wave = Math.sin(progress * Math.PI * 3 + this.time) * meander
|
||||
const y = riverY + wave
|
||||
ctx.lineTo(x, y)
|
||||
}
|
||||
// Untere Kante (Flussbreite)
|
||||
for (let i = steps; i >= 0; i--) {
|
||||
const x = (i / steps) * w
|
||||
const progress = i / steps
|
||||
const wave = Math.sin(progress * Math.PI * 3 + this.time) * meander
|
||||
const y = riverY + wave + riverWidth
|
||||
ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.closePath()
|
||||
|
||||
// Flussfarbe: klar (biodiversitaet hoch) bis trueb (erosion hoch)
|
||||
const clarity = Math.max(0, Math.min(1, (state.biodiversity - state.erosion * 0.5) / 80))
|
||||
const r = Math.round(30 + (1 - clarity) * 60)
|
||||
const g = Math.round(100 + clarity * 50)
|
||||
const b = Math.round(160 + clarity * 40)
|
||||
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.85)`
|
||||
ctx.fill()
|
||||
|
||||
// Wasseroberflaeche Glanz
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'
|
||||
ctx.lineWidth = 1
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const sx = Math.random() * w
|
||||
const sy = riverY + Math.sin(sx / w * Math.PI * 3 + this.time) * meander + riverWidth * 0.3
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(sx, sy)
|
||||
ctx.lineTo(sx + 15 + Math.random() * 20, sy - 1)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
private drawLevees(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number): void {
|
||||
const leveeH = 3 + intensity * 0.08
|
||||
const riverY = h * 0.55
|
||||
ctx.fillStyle = '#8B7355'
|
||||
|
||||
// Oberer Deich
|
||||
ctx.fillRect(0, riverY - leveeH - 5, w, leveeH)
|
||||
// Unterer Deich
|
||||
ctx.fillRect(0, riverY + 30, w, leveeH)
|
||||
}
|
||||
|
||||
private drawFloodplains(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number, state: State): void {
|
||||
const riverY = h * 0.55
|
||||
const extent = intensity * 0.3
|
||||
const alpha = 0.15 + intensity * 0.002
|
||||
|
||||
ctx.fillStyle = `rgba(100, 180, 140, ${alpha})`
|
||||
// Aue oben
|
||||
ctx.fillRect(0, riverY - extent - 20, w, extent)
|
||||
// Aue unten
|
||||
ctx.fillRect(0, riverY + 35, w, extent)
|
||||
|
||||
// Schilf in den Auen
|
||||
if (intensity > 30) {
|
||||
ctx.fillStyle = '#5A8A3E'
|
||||
for (let i = 0; i < intensity * 0.3; i++) {
|
||||
const x = (i * 47 + 20) % w
|
||||
const y = riverY - 25 - Math.random() * extent * 0.5
|
||||
this.drawReeds(ctx, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawReeds(ctx: CanvasRenderingContext2D, x: number, y: number): void {
|
||||
ctx.save()
|
||||
ctx.strokeStyle = '#4A7A2E'
|
||||
ctx.lineWidth = 1.5
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const lean = Math.sin(this.time * 2 + x * 0.1) * 3
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + i * 3, y)
|
||||
ctx.quadraticCurveTo(x + i * 3 + lean, y - 8, x + i * 3 + lean * 1.5, y - 15)
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
private drawFlood(ctx: CanvasRenderingContext2D, w: number, h: number, intensity: number): void {
|
||||
const alpha = Math.min(0.4, (intensity - 50) / 100)
|
||||
const extent = (intensity - 50) * 0.8
|
||||
const riverY = h * 0.55
|
||||
|
||||
ctx.fillStyle = `rgba(70, 130, 180, ${alpha})`
|
||||
// Ueberflutung breitet sich vom Fluss aus
|
||||
ctx.fillRect(0, riverY - extent, w, extent * 2 + 30)
|
||||
|
||||
// Wellenlinien
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.5})`
|
||||
ctx.lineWidth = 1
|
||||
for (let row = 0; row < 3; row++) {
|
||||
ctx.beginPath()
|
||||
const baseY = riverY - extent + row * extent * 0.6
|
||||
for (let x = 0; x < w; x += 5) {
|
||||
const y = baseY + Math.sin(x * 0.05 + this.time * 3 + row) * 3
|
||||
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
private drawVegetation(ctx: CanvasRenderingContext2D, w: number, h: number, biodiversity: number, fertility: number): void {
|
||||
const treeCount = Math.floor(biodiversity * 0.15)
|
||||
const groundY = h * 0.35
|
||||
|
||||
for (let i = 0; i < treeCount; i++) {
|
||||
const seed = i * 137.5 // goldener Winkel fuer Verteilung
|
||||
const x = (seed % w)
|
||||
const yBase = groundY + 10 + (seed * 7 % (h * 0.15))
|
||||
|
||||
// Nur Baeume die nicht im Flussbereich sind
|
||||
if (yBase > h * 0.5 && yBase < h * 0.65) continue
|
||||
|
||||
const treeH = 12 + (fertility * 0.1) + (i % 5) * 2
|
||||
const green = Math.round(60 + biodiversity * 0.8 + (i % 3) * 20)
|
||||
|
||||
// Stamm
|
||||
ctx.fillStyle = '#6B4423'
|
||||
ctx.fillRect(x - 1.5, yBase - treeH * 0.4, 3, treeH * 0.4)
|
||||
|
||||
// Krone
|
||||
ctx.fillStyle = `rgb(${40 + (i % 20)}, ${green}, ${30 + (i % 15)})`
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, yBase - treeH * 0.5, treeH * 0.35, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
private drawSettlement(ctx: CanvasRenderingContext2D, w: number, h: number, economy: number, usableLand: number): void {
|
||||
const houseCount = Math.floor(3 + economy * 0.08)
|
||||
const startX = w * 0.6
|
||||
const baseY = h * 0.42
|
||||
|
||||
for (let i = 0; i < houseCount; i++) {
|
||||
const x = startX + (i % 4) * 30 + Math.floor(i / 4) * 15
|
||||
const y = baseY + Math.floor(i / 4) * 20
|
||||
const houseH = 12 + (economy * 0.05)
|
||||
|
||||
if (x > w - 20) continue
|
||||
|
||||
// Haus
|
||||
ctx.fillStyle = i < 3 ? '#D4A574' : '#C4956A'
|
||||
ctx.fillRect(x, y - houseH, 16, houseH)
|
||||
|
||||
// Dach
|
||||
ctx.fillStyle = '#8B4513'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x - 3, y - houseH)
|
||||
ctx.lineTo(x + 8, y - houseH - 8)
|
||||
ctx.lineTo(x + 19, y - houseH)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// Fenster
|
||||
ctx.fillStyle = '#FFF8DC'
|
||||
ctx.fillRect(x + 3, y - houseH + 3, 4, 4)
|
||||
ctx.fillRect(x + 9, y - houseH + 3, 4, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private drawFarms(ctx: CanvasRenderingContext2D, w: number, h: number, fertility: number, irrigation: number): void {
|
||||
const farmArea = h * 0.75
|
||||
const rows = Math.floor(3 + fertility * 0.04)
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const y = farmArea + r * 12
|
||||
const x = w * 0.05 + r * 20
|
||||
|
||||
// Feldstreifen
|
||||
const green = Math.round(100 + fertility * 0.8)
|
||||
const brown = Math.round(180 - fertility * 0.5)
|
||||
ctx.fillStyle = r % 2 === 0
|
||||
? `rgb(${brown}, ${green}, 60)`
|
||||
: `rgb(${brown - 20}, ${Math.min(180, green + 20)}, 40)`
|
||||
ctx.fillRect(x, y, w * 0.25, 8)
|
||||
|
||||
// Bewaesserungskanaele
|
||||
if (irrigation > 20) {
|
||||
ctx.strokeStyle = 'rgba(70, 130, 200, 0.4)'
|
||||
ctx.lineWidth = 1
|
||||
ctx.setLineDash([3, 3])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y + 4)
|
||||
ctx.lineTo(x + w * 0.25, y + 4)
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawErosion(ctx: CanvasRenderingContext2D, w: number, h: number, erosion: number): void {
|
||||
const count = Math.floor((erosion - 40) * 0.2)
|
||||
ctx.fillStyle = 'rgba(139, 115, 85, 0.3)'
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x = (i * 89 + 30) % w
|
||||
const y = h * 0.6 + (i * 43 % (h * 0.2))
|
||||
ctx.beginPath()
|
||||
ctx.ellipse(x, y, 8 + erosion * 0.05, 3, 0.3, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
private drawClouds(ctx: CanvasRenderingContext2D, w: number, h: number): void {
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cx = (i * 250 + this.time * 15) % (w + 100) - 50
|
||||
const cy = 30 + i * 25
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, 20, 0, Math.PI * 2)
|
||||
ctx.arc(cx + 15, cy - 5, 15, 0, Math.PI * 2)
|
||||
ctx.arc(cx + 30, cy, 18, 0, Math.PI * 2)
|
||||
ctx.arc(cx - 12, cy + 2, 14, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user