package main import ( "image/color" "math" "math/rand" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) // GroundTile repräsentiert ein Boden-Segment type GroundTile struct { X float64 DirtVariants []DirtPatch Stones []Stone } // DirtPatch ist ein Dirt-Fleck im Boden type DirtPatch struct { OffsetX float64 OffsetY float64 Width float64 Height float64 Color color.RGBA } // Stone ist ein Stein auf dem Boden type Stone struct { X float64 Y float64 Size float64 Color color.RGBA Shape int // 0=rund, 1=eckig } // GroundCache speichert generierte Tiles var groundCache = make(map[int]GroundTile) // ClearGroundCache leert den Cache (z.B. bei Änderungen) func ClearGroundCache() { groundCache = make(map[int]GroundTile) } // GenerateGroundTile generiert ein prozedurales Boden-Segment (gecacht) func GenerateGroundTile(tileIdx int) GroundTile { // Prüfe Cache if cached, exists := groundCache[tileIdx]; exists { return cached } // Deterministischer Seed basierend auf Tile-Index rng := rand.New(rand.NewSource(int64(tileIdx * 12345))) tile := GroundTile{ X: float64(tileIdx) * 128.0, DirtVariants: make([]DirtPatch, 0), Stones: make([]Stone, 0), } // Zufällige Dirt-Patches generieren (20-30 pro Tile, über die ganze Höhe) numDirt := 20 + rng.Intn(10) dirtHeight := 5000.0 // Gesamte Dirt-Höhe bis tief in die Erde for i := 0; i < numDirt; i++ { darkness := uint8(70 + rng.Intn(40)) // Verschiedene Brauntöne tile.DirtVariants = append(tile.DirtVariants, DirtPatch{ OffsetX: rng.Float64() * 128, OffsetY: rng.Float64()*dirtHeight + 20, // Über die ganze Dirt-Schicht verteilt Width: 10 + rng.Float64()*30, Height: 10 + rng.Float64()*25, Color: color.RGBA{darkness, darkness - 10, darkness - 20, 255}, }) } // Steine IN der Erde generieren (30-50 pro Tile, nur eckig, tief verteilt) numStones := 30 + rng.Intn(20) for i := 0; i < numStones; i++ { tile.Stones = append(tile.Stones, Stone{ X: rng.Float64() * 128, Y: rng.Float64()*dirtHeight + 20, // Tief in der Erde verteilt Size: 4 + rng.Float64()*8, // Verschiedene Größen Color: color.RGBA{100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 255}, Shape: 1, // Nur eckig (keine runden Steine mehr) }) } // In Cache speichern groundCache[tileIdx] = tile return tile } // RenderGround rendert den Boden mit Bewegung func (g *Game) RenderGround(screen *ebiten.Image, cameraX float64) { // Tatsächliche Canvas-Größe verwenden canvasW, _ := screen.Size() // Boden bleibt an fester Position (RefFloorY) - wichtig für Spielphysik! // Erweitere Boden nach unten weit über Canvas-Rand hinaus (5000 Pixel tief) floorY := float32(RefFloorY) floorH := float32(5000) // Tief in die Erde // 1. Basis Gras-Schicht (volle Canvas-Breite, nur dünne Grasnarbe) vector.DrawFilledRect(screen, 0, floorY, float32(canvasW), 20, ColGrass, false) // 2. Dirt-Schicht (Basis, volle Canvas-Breite, tief nach unten) vector.DrawFilledRect(screen, 0, floorY+20, float32(canvasW), floorH-20, ColDirt, false) // 3. Prozedurale Dirt-Patches und Steine (bewegen sich mit Kamera) // Berechne welche Tiles sichtbar sind (basierend auf Canvas-Breite) tileWidth := 128.0 startTile := int(math.Floor(cameraX / tileWidth)) endTile := int(math.Ceil((cameraX + float64(canvasW)) / tileWidth)) // Tiles rendern for tileIdx := startTile; tileIdx <= endTile; tileIdx++ { tile := GenerateGroundTile(tileIdx) // Dirt-Patches rendern for _, dirt := range tile.DirtVariants { worldX := tile.X + dirt.OffsetX screenX := float32(worldX - cameraX) screenY := float32(RefFloorY) + float32(dirt.OffsetY) // Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden) if screenX+float32(dirt.Width) > 0 && screenX < float32(canvasW) { vector.DrawFilledRect(screen, screenX, screenY, float32(dirt.Width), float32(dirt.Height), dirt.Color, false) } } // Steine rendern (auf dem Gras) for _, stone := range tile.Stones { worldX := tile.X + stone.X screenX := float32(worldX - cameraX) screenY := float32(RefFloorY) + float32(stone.Y) // Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden) if screenX > -20 && screenX < float32(canvasW)+20 { if stone.Shape == 0 { // Runder Stein vector.DrawFilledCircle(screen, screenX, screenY, float32(stone.Size/2), stone.Color, false) // Highlight für 3D-Effekt highlightCol := color.RGBA{ uint8(math.Min(float64(stone.Color.R)+40, 255)), uint8(math.Min(float64(stone.Color.G)+40, 255)), uint8(math.Min(float64(stone.Color.B)+40, 255)), 200, } vector.DrawFilledCircle(screen, screenX-float32(stone.Size*0.15), screenY-float32(stone.Size*0.15), float32(stone.Size/4), highlightCol, false) } else { // Eckiger Stein vector.DrawFilledRect(screen, screenX-float32(stone.Size/2), screenY-float32(stone.Size/2), float32(stone.Size), float32(stone.Size), stone.Color, false) // Schatten für 3D-Effekt shadowCol := color.RGBA{ uint8(float64(stone.Color.R) * 0.6), uint8(float64(stone.Color.G) * 0.6), uint8(float64(stone.Color.B) * 0.6), 150, } vector.DrawFilledRect(screen, screenX-float32(stone.Size/2)+2, screenY-float32(stone.Size/2)+2, float32(stone.Size), float32(stone.Size), shadowCol, false) // Original drüber vector.DrawFilledRect(screen, screenX-float32(stone.Size/2), screenY-float32(stone.Size/2), float32(stone.Size), float32(stone.Size), stone.Color, false) } } } } // Cache aufräumen (nur Tiles außerhalb des Sichtbereichs entfernen) if len(groundCache) > 100 { for idx := range groundCache { if idx < startTile-10 || idx > endTile+10 { delete(groundCache, idx) } } } }