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 (15-25 pro Tile, kompakt) numDirt := 15 + rng.Intn(10) maxDirtHeight := 100.0 // Kompakte Erde-Tiefe (100px) 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()*maxDirtHeight + 20, // Nur im sichtbaren Bereich Width: 10 + rng.Float64()*30, Height: 10 + rng.Float64()*25, Color: color.RGBA{darkness, darkness - 10, darkness - 20, 255}, }) } // Steine IN der Erde generieren (20-30 pro Tile, nur eckig, kompakt) numStones := 20 + rng.Intn(10) for i := 0; i < numStones; i++ { tile.Stones = append(tile.Stones, Stone{ X: rng.Float64() * 128, Y: rng.Float64()*maxDirtHeight + 20, // Nur im sichtbaren Bereich 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, canvasH := screen.Size() // Gameplay-Boden-Position (wo Spieler laufen) - mit echter Canvas-Höhe! gameFloorY := float32(GetFloorYFromHeight(canvasH)) // Erde-Tiefe - kompakt (100px) damit Gameplay optimal Platz hat maxDirtDepth := float32(100.0) // Berechne wie viel Erde sichtbar sein soll (bis zum Bildschirmrand) remainingSpace := float32(canvasH) - gameFloorY visibleDirtHeight := maxDirtDepth if remainingSpace < maxDirtDepth { visibleDirtHeight = remainingSpace } // 1. Basis Gras-Schicht (volle Canvas-Breite, nur dünne Grasnarbe) vector.DrawFilledRect(screen, 0, gameFloorY, float32(canvasW), 20, ColGrass, false) // 2. Dirt-Schicht (Basis, volle Canvas-Breite, nur sichtbarer Bereich) if visibleDirtHeight > 20 { vector.DrawFilledRect(screen, 0, gameFloorY+20, float32(canvasW), visibleDirtHeight-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 := gameFloorY + 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 (in der Erde) for _, stone := range tile.Stones { worldX := tile.X + stone.X screenX := float32(worldX - cameraX) screenY := gameFloorY + 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) } } } }