fixed up comments

This commit is contained in:
Grimsace
2026-02-05 17:50:55 -06:00
parent 8d746d5c0b
commit 7440b000c1
8 changed files with 239 additions and 89 deletions
+33 -8
View File
@@ -8,14 +8,18 @@ import (
"sort"
)
// GenerateBuildings creates and places buildings on the map.
func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) []image.Point {
// Early exit if no buildings are to be generated
if settings.NumBuildings == 0 {
return nil
}
// Initialize random number generator
randSrc := rand.New(rand.NewSource(seed))
buildingColor := color.RGBA{R: 128, G: 128, B: 128, A: 255} // Gray color for buildings
// Create lookup maps for water and road pixels for efficient collision detection
isWater := make(map[image.Point]bool)
for _, p := range allWaterPixels {
isWater[p] = true
@@ -26,9 +30,12 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
isRoad[p] = true
}
// Initialize building data structures
isBuilding := make(map[image.Point]bool)
var buildingPixels []image.Point
var anchorPoints []image.Point
// Determine anchor points for building placement
if len(roadPixels) > 0 {
anchorPoints = roadPixels
} else {
@@ -42,16 +49,20 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
}
}
}
// Early exit if no anchor points are available
if len(anchorPoints) == 0 {
return nil
}
// Sort anchor points to have a deterministic order if needed, although we are selecting randomly
// Sort anchor points for deterministic placement
sort.Slice(anchorPoints, func(i, j int) bool {
if anchorPoints[i].Y != anchorPoints[j].Y {
return anchorPoints[i].Y < anchorPoints[j].Y
}
return anchorPoints[i].X < anchorPoints[j].X
})
// Collect all land points for random placement
landPoints := make([]image.Point, 0, width*height)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
@@ -61,25 +72,29 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
}
}
}
// Main loop for placing buildings
buildingsPlaced := 0
searchTries := 100 // Number of attempts to find a spot for a building
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
maxPlacementAttempts--
// Select an anchor point for the new building
var anchor image.Point
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
// Place near roads
// Place near roads or other existing features
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
} else {
// Place randomly on land
// Place randomly on any available land
if len(landPoints) == 0 {
continue // No land to place buildings on
}
anchor = landPoints[randSrc.Intn(len(landPoints))]
}
// Search for a valid building location around the anchor
for i := 0; i < searchTries; i++ {
searchRadius := float64(i) * 2.0 // Search in expanding circles
angle := randSrc.Float64() * 2 * math.Pi
@@ -88,6 +103,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
X: anchor.X + int(dist*math.Cos(angle)),
Y: anchor.Y + int(dist*math.Sin(angle)),
}
// For fully random distribution, pick any point on the map
if settings.BuildingDistribution == 100 {
center = image.Point{
X: randSrc.Intn(width),
@@ -95,36 +111,42 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
}
}
// Ensure the center point is within the map boundaries
if center.X < 0 || center.Y < 0 || center.X >= width || center.Y >= height {
continue
}
// Attempt to create a building at the selected center
size := settings.MinBuildingSize + randSrc.Float64()*(settings.MaxBuildingSize-settings.MinBuildingSize)
pixels, ok := getBuildingPixels(center, size, settings.BuildingShape, isWater, isRoad, isBuilding, width, height, randSrc)
if ok {
// If successful, draw the building and update data structures
for _, p := range pixels {
img.Set(p.X, p.Y, buildingColor)
isBuilding[p] = true
buildingPixels = append(buildingPixels, p)
}
buildingsPlaced++
break // Found a spot, move to next building
break // Move to the next building
}
}
}
return buildingPixels
}
// getBuildingPixels determines the pixels for a single building based on its shape and checks for collisions.
func getBuildingPixels(center image.Point, size float64, shape string, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) {
var pixels []image.Point
var halfSize = int(size / 2)
// Generate pixels based on the selected building shape
switch shape {
case "squares":
for y := center.Y - halfSize; y <= center.Y+halfSize; y++ {
for x := center.X - halfSize; x <= center.X+halfSize; x++ {
p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
return nil, false
return nil, false // Collision detected
}
pixels = append(pixels, p)
}
@@ -137,13 +159,14 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
if dx*dx+dy*dy <= r2 {
p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
return nil, false
return nil, false // Collision detected
}
pixels = append(pixels, p)
}
}
}
case "rectangles":
// Create rectangles with varied aspect ratios
longSide := size
shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize)
var w, h int
@@ -154,17 +177,19 @@ func getBuildingPixels(center image.Point, size float64, shape string, isWater,
}
halfW, halfH := w/2, h/2
// Check for collisions and gather pixels
for y := center.Y - halfH; y <= center.Y+halfH; y++ {
for x := center.X - halfW; x <= center.X+halfW; x++ {
p := image.Point{X: x, Y: y}
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
return nil, false
return nil, false // Collision detected
}
pixels = append(pixels, p)
}
}
}
// Final check to ensure pixels were generated
if len(pixels) == 0 {
return nil, false
}
+10 -1
View File
@@ -6,6 +6,7 @@ import (
)
// TextOverlayProgressBar is a custom widget that displays a progress bar with text overlay.
// This allows showing progress information (e.g., step description) directly on the progress bar.
type TextOverlayProgressBar struct {
widget.BaseWidget
progressBar *widget.ProgressBar
@@ -22,7 +23,7 @@ func NewTextOverlayProgressBar() *TextOverlayProgressBar {
return p
}
// SetValue sets the progress value.
// SetValue sets the progress value of the underlying progress bar.
func (p *TextOverlayProgressBar) SetValue(v float64) {
p.progressBar.SetValue(v)
}
@@ -41,31 +42,38 @@ func (p *TextOverlayProgressBar) CreateRenderer() fyne.WidgetRenderer {
}
}
// textOverlayProgressBarRenderer is the renderer for the TextOverlayProgressBar.
// It handles the layout and rendering of the progress bar and the overlay text.
type textOverlayProgressBarRenderer struct {
progressBar *widget.ProgressBar
label *widget.Label
objects []fyne.CanvasObject
}
// Layout defines the size and position of the progress bar and the label.
func (r *textOverlayProgressBarRenderer) Layout(size fyne.Size) {
r.progressBar.Resize(size)
r.label.Resize(size)
r.label.Move(fyne.NewPos(0, 0))
}
// MinSize returns the minimum size of the widget.
func (r *textOverlayProgressBarRenderer) MinSize() fyne.Size {
return r.progressBar.MinSize()
}
// Refresh redraws the widget.
func (r *textOverlayProgressBarRenderer) Refresh() {
r.progressBar.Refresh()
r.label.Refresh()
}
// Objects returns the canvas objects that make up the widget.
func (r *textOverlayProgressBarRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
// Destroy is a no-op for this renderer.
func (r *textOverlayProgressBarRenderer) Destroy() {}
// CustomTheme is a custom theme to make the progress bar thinner.
@@ -79,6 +87,7 @@ func NewCustomTheme(theme fyne.Theme) *CustomTheme {
}
// Size returns the size for a given themeable item.
// It overrides the default progress bar height to make it thinner.
func (t *CustomTheme) Size(name fyne.ThemeSizeName) float32 {
if name == "progressBar.height" {
return 10
+43 -16
View File
@@ -28,11 +28,13 @@ import (
)
func main() {
// Initialize the Fyne application and window
a := app.NewWithID("com.example.rpgcitymaker")
a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()})
w := a.NewWindow("RPG City Maker")
w.Resize(fyne.NewSize(800, 600))
// Load settings from file, or use defaults if loading fails
settings, err := LoadSettings()
if err != nil {
log.Println("Error loading settings:", err)
@@ -40,14 +42,15 @@ func main() {
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
}
// Create canvas objects for displaying the generated images
canvasImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
heightmapImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
// Initialize slices to store generated map features
var lakes [][]image.Point
var riverPixels []image.Point
var treePixels []image.Point
@@ -55,6 +58,7 @@ func main() {
var roadPixels []image.Point
var bridgePixels []image.Point
// Set up application configuration directory
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Failed to get user config dir:", err)
@@ -62,6 +66,8 @@ func main() {
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
canvasPath := filepath.Join(appConfigDir, "canvas.png")
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
// Save settings and images on window close
w.SetOnClosed(func() {
if err := settings.Save(); err != nil {
log.Println("Error saving settings:", err)
@@ -91,6 +97,8 @@ func main() {
}
}
})
// Load previously saved images
canvasFile, err := os.Open(canvasPath)
if err == nil {
defer canvasFile.Close()
@@ -109,16 +117,19 @@ func main() {
}
}
// Generate initial images if none are loaded
if canvasImg.Image == nil || heightmapImg.Image == nil {
// Initial image generation
// Step 1: Generating Heightmap
seedProvider := NewSeedProvider(settings.Seed)
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
// Step 2: Generating Lakes
var lakeImage image.Image
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
// Step 3: Generating Rivers
var riverImage image.Image
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
@@ -136,6 +147,7 @@ func main() {
}
allWaterPixels := append(flatLakePixels, riverPixels...)
// Step 4: Generating Roads
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next())
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
@@ -146,14 +158,19 @@ func main() {
}
}
// Step 5: Generating Buildings
buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
// Step 6: Generating Trees
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
// Step 7: Darkening Water Areas
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
// Step 8: Flattening Road Areas
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
// Step 9: Applying Roughness
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
heightmapImg.Image = compositeImg
@@ -161,7 +178,7 @@ func main() {
canvasImg.Image = finalImage
}
// Create UI elements for controlling terrain generation settings
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
detailSlider := widget.NewSlider(1, 16)
detailSlider.OnChanged = func(val float64) {
@@ -177,7 +194,7 @@ func main() {
roughnessLabel.SetText(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
}
roughnessSlider.SetValue(settings.Roughness)
// Create UI elements for controlling lake generation settings
lakesLabel := widget.NewLabel(fmt.Sprintf("Lakes: %d", settings.Lakes))
lakesSlider := widget.NewSlider(0, 15)
lakesSlider.OnChanged = func(val float64) {
@@ -210,7 +227,7 @@ func main() {
lakeSizeUpperLabel.SetText(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
}
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
// Create UI elements for controlling river generation settings
riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers))
riversSlider := widget.NewSlider(0, 5)
riversSlider.OnChanged = func(val float64) {
@@ -251,7 +268,7 @@ func main() {
riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
}
riverCurvynessSlider.SetValue(settings.RiverCurvyness)
// Create UI elements for controlling tree generation settings
minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
minTreeSizeSlider := widget.NewSlider(1, 150)
maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
@@ -292,7 +309,7 @@ func main() {
treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
}
treeClumpinessSlider.SetValue(settings.TreeClumpiness)
// Create UI elements for controlling road generation settings
numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
numRoadsSlider := widget.NewSlider(0, 2000)
numRoadsSlider.OnChanged = func(val float64) {
@@ -354,6 +371,7 @@ func main() {
}
roadDistributionSlider.SetValue(settings.RoadDistribution)
// Create UI elements for error display and action buttons
errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255})
errorLabel.TextSize = 12
errorLabel.Hide()
@@ -369,18 +387,20 @@ func main() {
exportMasksBtn := widget.NewButton("Export Masks", func() {
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels)
})
// Main generation button and logic
generateBtn = widget.NewButton("Generate", func() {
go func() {
// Disable button during generation
fyne.Do(func() {
generateBtn.Disable()
})
defer func() {
// Re-enable button after generation
fyne.Do(func() {
generateBtn.Enable()
})
}()
// Set up progress bar
steps := 9
currentStep := 0
@@ -483,7 +503,7 @@ func main() {
})
}()
})
// Create UI elements for image dimensions and seed
widthEntry := widget.NewEntry()
widthEntry.SetText(strconv.Itoa(settings.Width))
widthEntry.OnChanged = func(s string) {
@@ -532,7 +552,7 @@ func main() {
settings.Seed = time.Now().UnixNano()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
})
// Create tabs for organizing settings
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
detailLabel,
detailSlider,
@@ -587,7 +607,7 @@ func main() {
roadDistributionLabel,
roadDistributionSlider,
))
// Create UI elements for building generation settings
numBuildingsLabel := widget.NewLabel(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings))
numBuildingsSlider := widget.NewSlider(0, 10000)
numBuildingsSlider.OnChanged = func(val float64) {
@@ -663,7 +683,7 @@ func main() {
exportHeightmapBtn,
exportMasksBtn,
))
// Create the main layout using a horizontal split
tabs := container.NewAppTabs(
imageTab,
terrainTab,
@@ -687,10 +707,12 @@ func main() {
right,
)
split.SetOffset(0.3)
// Set the window content and start the application
w.SetContent(split)
w.ShowAndRun()
}
// getImageData encodes an image to the specified format and returns the data as a byte buffer.
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
var err error
@@ -705,7 +727,9 @@ func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
return buf, err
}
// showMasksSaveDialog displays a dialog for saving the generated masks.
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point) {
// Create UI elements for the save dialog
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("masks_folder")
@@ -752,7 +776,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
imgFormat := strings.ToLower(formatSelect.Selected)
bounds := canvasImg.Bounds()
// Create mask images
// Create mask images from the generated data
lakeMask := image.NewGray(bounds)
for _, lake := range lakes {
for _, p := range lake {
@@ -790,7 +814,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
"bridges_mask." + imgFormat: bridgeMask,
"buildings_mask." + imgFormat: buildingMask,
}
// Save the images based on the selected packaging option
switch packageSelect.Selected {
case "Folder":
exportPath := filepath.Join(pathLabel.Text, folderName)
@@ -888,6 +912,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, s
saveDialog.Show()
}
// saveImage saves an image to the specified path.
func saveImage(img image.Image, path string) {
file, err := os.Create(path)
if err != nil {
@@ -910,7 +935,9 @@ func saveImage(img image.Image, path string) {
}
}
// showSaveDialog displays a dialog for saving an image.
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
// Create UI elements for the save dialog
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("image")
+39 -12
View File
@@ -11,17 +11,20 @@ import (
"unsafe"
)
// PointOfInterest represents a location on the map where roads may start, end, or intersect.
type PointOfInterest struct {
X, Y int
Connections int
IsExit bool
}
// PathPoint represents a single point in a road's path, with a flag to indicate if it's a bridge.
type PathPoint struct {
Point image.Point
IsBridge bool
}
// Road represents a connection between two Points of Interest.
type Road struct {
Start, End *PointOfInterest
Width int
@@ -29,27 +32,33 @@ type Road struct {
Importance int
}
// GenerateRoads is the main function for creating roads on the map.
func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) {
// Step 1: Initialize a transparent image for drawing roads
img := image.NewRGBA(image.Rect(0, 0, width, height))
// Transparent background
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.Transparent)
}
}
// Step 2: Set up random number generator and colors
randSrc := rand.New(rand.NewSource(seed))
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
// Step 3: Generate Points of Interest (POIs)
pois := generatePOIs(width, height, settings, allWaterPixels, randSrc)
if len(pois) == 0 {
return nil, nil, img
}
// Step 4: Connect POIs to form roads
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
// Step 5: Assign widths to the roads based on their importance
assignRoadWidths(roads, settings)
// Step 6: Draw the roads on the image
var allRoadPixels []image.Point
var allBridgePixels []image.Point
for _, road := range roads {
@@ -61,6 +70,7 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image,
return allRoadPixels, allBridgePixels, img
}
// generatePOIs creates the initial set of points where roads will originate.
func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest {
numPOIs := settings.NumRoads / 2
if numPOIs == 0 {
@@ -81,15 +91,16 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
centerX := width / 2
centerY := height / 2
// Distribution affects the radius
// Distribution affects the radius of POI generation
maxRadius := math.Min(float64(width)/2, float64(height)/2)
radius := maxRadius * (settings.RoadDistribution / 100.0)
for i := 0; i < numPOIs; i++ {
var x, y int
found := false
for j := 0; j < 100; j++ { // 100 retries to find a land spot
for j := 0; j < 100; j++ { // Retries to find a land spot
if i < numExits {
// Create POIs at the map edges
side := randSrc.Intn(4)
switch side {
case 0: // Top
@@ -106,6 +117,7 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
y = randSrc.Intn(height)
}
} else {
// Create POIs within the map
angle := randSrc.Float64() * 2 * math.Pi
r := math.Sqrt(randSrc.Float64()) * radius
x = int(float64(centerX) + r*math.Cos(angle))
@@ -125,6 +137,8 @@ func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.
return pois
}
// connectPOIs creates roads by connecting the generated Points of Interest.
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road {
if len(pois) < 2 {
return nil
@@ -137,7 +151,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
visited := make(map[*PointOfInterest]bool)
existingRoads := make(map[string]bool)
// Find the center-most POI
// Find the center-most POI to start connecting from
centerX := width / 2
centerY := height / 2
var startNode *PointOfInterest
@@ -160,9 +174,11 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
visited[startNode] = true
// Use average dimension for controlling road path calculation
avgDim := float64(width+height) / 2.0
numControlPoints := max(int(avgDim*0.03), 60)
// Connect all POIs using a minimum spanning tree-like algorithm
for len(visited) < len(pois) {
var closest *PointOfInterest
var fromNode *PointOfInterest
@@ -176,7 +192,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
if !visited[other] {
dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2))
// Check if road exists
// Check if a road already exists between these two POIs
key := fmt.Sprintf("%p-%p", poi, other)
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
key = fmt.Sprintf("%p-%p", other, poi)
@@ -185,7 +201,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
continue
}
// Don't connect two exit points
// Avoid connecting two exit points directly
if poi.IsExit && other.IsExit {
continue
}
@@ -204,7 +220,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
fromNode.Connections++
closest.Connections++
// Add road to existing roads map
// Add road to existing roads map to prevent duplicates
key := fmt.Sprintf("%p-%p", fromNode, closest)
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
key = fmt.Sprintf("%p-%p", closest, fromNode)
@@ -222,7 +238,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
}
}(fromNode, closest)
} else {
// No more reachable POIs
// No more reachable POIs, break the loop
break
}
}
@@ -236,6 +252,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
roads = append(roads, road)
}
// Calculate road importance based on the number of connections at its endpoints
for _, road := range roads {
road.Importance = road.Start.Connections + road.End.Connections
}
@@ -243,11 +260,13 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
return roads
}
// assignRoadWidths sets the width of each road based on its importance.
func assignRoadWidths(roads []*Road, settings *Settings) {
if len(roads) == 0 {
return
}
// Sort roads by importance in descending order
sort.Slice(roads, func(i, j int) bool {
return roads[i].Importance > roads[j].Importance
})
@@ -259,11 +278,13 @@ func assignRoadWidths(roads []*Road, settings *Settings) {
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
}
// Assign widths, with more important roads being wider
for i, road := range roads {
road.Width = int(maxWidth - float64(i)*widthStep)
}
}
// drawRoad draws a single road on the image, including bridges.
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
var roadPixels []image.Point
var bridgePixels []image.Point
@@ -285,6 +306,7 @@ func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.
return roadPixels, bridgePixels
}
// bresenhamRoad uses Bresenham's line algorithm to create a path between control points.
func bresenhamRoad(path []image.Point) []image.Point {
if len(path) < 2 {
return path
@@ -324,6 +346,7 @@ func bresenhamRoad(path []image.Point) []image.Point {
return fullPath
}
// calculateRoadPath computes the path for a road, including curves and bridges.
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint {
dx := end.X - start.X
dy := end.Y - start.Y
@@ -338,7 +361,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
return []PathPoint{{Point: image.Point{X: start.X, Y: start.Y}, IsBridge: waterMap[image.Point{X: start.X, Y: start.Y}]}}
}
// Adjust curviness based on distance
// Adjust curviness based on the distance between the POIs
distanceFactor := math.Min(1.0, dist/(avgDim*0.5))
adjustedCurvyness := curvyness * distanceFactor
@@ -351,6 +374,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
return pathPoints
}
// Use sine waves to create curves in the road
type wave struct {
amplitude float64
numWaves float64
@@ -365,20 +389,21 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
}
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
// Main wave
// Main wave for overall curve
waves[0] = wave{
amplitude: amp,
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
phase: randSrc.Float64() * 2 * math.Pi,
}
// Smaller wave for detours
// Smaller wave for minor detours and a more natural look
waves[1] = wave{
amplitude: amp / 4,
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
phase: randSrc.Float64() * 2 * math.Pi,
}
// Generate control points for the curve
controlPoints := make([]image.Point, numControlPoints+1)
for i := 0; i <= numControlPoints; i++ {
t := float64(i) / float64(numControlPoints)
@@ -401,6 +426,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
}
// Create the final path using Bresenham's algorithm between control points
points := bresenhamRoad(controlPoints)
pathPoints := make([]PathPoint, len(points))
for i, p := range points {
@@ -409,7 +435,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
return pathPoints
}
// Bresenham's line algorithm for drawing segments of the curve
// drawLine draws a line with a specified width on the image.
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
var points []image.Point
dx := abs(x1 - x0)
@@ -452,6 +478,7 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [
return points
}
// abs returns the absolute value of an integer.
func abs(x int) int {
if x < 0 {
return -x
+5
View File
@@ -2,16 +2,21 @@ package main
import "math/rand"
// SeedProvider is a simple struct that provides a stream of random seeds
// from a single initial seed. This ensures that the entire map generation
// process is deterministic if the same initial seed is used.
type SeedProvider struct {
rand *rand.Rand
}
// NewSeedProvider creates a new SeedProvider with the given initial seed.
func NewSeedProvider(seed int64) *SeedProvider {
return &SeedProvider{
rand: rand.New(rand.NewSource(seed)),
}
}
// Next returns the next random seed in the sequence.
func (sp *SeedProvider) Next() int64 {
return sp.rand.Int63()
}
+47 -23
View File
@@ -7,48 +7,64 @@ import (
"time"
)
// Settings holds all the user-configurable parameters for map generation.
type Settings struct {
Detail float64 `json:"detail"`
Roughness float64 `json:"roughness"`
Width int `json:"width"`
Height int `json:"height"`
Lakes int `json:"lakes"`
LakeSizeLower float64 `json:"lake_size_lower"`
LakeSizeUpper float64 `json:"lake_size_upper"`
MinTreeSize float64 `json:"min_tree_size"`
MaxTreeSize float64 `json:"max_tree_size"`
TreeCoverage float64 `json:"tree_coverage"`
TreeClumpiness float64 `json:"tree_clumpiness"`
Seed int64 `json:"seed"`
Rivers int `json:"rivers"`
MinRiverWidth float64 `json:"min_river_width"`
MaxRiverWidth float64 `json:"max_river_width"`
RiverCurvyness float64 `json:"river_curvyness"`
NumRoads int `json:"num_roads"`
MinRoadWidth float64 `json:"min_road_width"`
MaxRoadWidth float64 `json:"max_road_width"`
RoadExits int `json:"road_exits"`
RoadCurvyness float64 `json:"road_curvyness"`
RoadDistribution float64 `json:"road_distribution"`
// Terrain settings
Detail float64 `json:"detail"`
Roughness float64 `json:"roughness"`
Width int `json:"width"`
Height int `json:"height"`
// Water settings
Lakes int `json:"lakes"`
LakeSizeLower float64 `json:"lake_size_lower"`
LakeSizeUpper float64 `json:"lake_size_upper"`
Rivers int `json:"rivers"`
MinRiverWidth float64 `json:"min_river_width"`
MaxRiverWidth float64 `json:"max_river_width"`
RiverCurvyness float64 `json:"river_curvyness"`
// Tree settings
MinTreeSize float64 `json:"min_tree_size"`
MaxTreeSize float64 `json:"max_tree_size"`
TreeCoverage float64 `json:"tree_coverage"`
TreeClumpiness float64 `json:"tree_clumpiness"`
// Road settings
NumRoads int `json:"num_roads"`
MinRoadWidth float64 `json:"min_road_width"`
MaxRoadWidth float64 `json:"max_road_width"`
RoadExits int `json:"road_exits"`
RoadCurvyness float64 `json:"road_curvyness"`
RoadDistribution float64 `json:"road_distribution"`
// Building settings
NumBuildings int `json:"num_buildings"`
MinBuildingSize float64 `json:"min_building_size"`
MaxBuildingSize float64 `json:"max_building_size"`
BuildingDistribution float64 `json:"building_distribution"`
BuildingShape string `json:"building_shape"`
LastExportPath string `json:"last_export_path"`
// General settings
Seed int64 `json:"seed"`
LastExportPath string `json:"last_export_path"`
}
// Save saves the current settings to a JSON file in the user's config directory.
func (s *Settings) Save() error {
// Get the user's config directory
configDir, err := os.UserConfigDir()
if err != nil {
return err
}
// Create the application's config directory if it doesn't exist
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
return err
}
// Create and open the settings file
configFile := filepath.Join(appConfigDir, "settings.json")
file, err := os.Create(configFile)
if err != nil {
@@ -56,19 +72,25 @@ func (s *Settings) Save() error {
}
defer file.Close()
// Encode the settings as JSON and write to the file
encoder := json.NewEncoder(file)
return encoder.Encode(s)
}
// LoadSettings loads the settings from a JSON file in the user's config directory.
// If the file doesn't exist, it returns a default set of settings.
func LoadSettings() (*Settings, error) {
// Get the user's config directory
configDir, err := os.UserConfigDir()
if err != nil {
return nil, err
}
// Open the settings file
configFile := filepath.Join(configDir, "rpgcitymakerreborn", "settings.json")
file, err := os.Open(configFile)
if err != nil {
// If the file doesn't exist, return default settings
if os.IsNotExist(err) {
homeDir, err := os.UserHomeDir()
if err != nil {
@@ -109,12 +131,14 @@ func LoadSettings() (*Settings, error) {
}
defer file.Close()
// Decode the JSON data into a Settings struct
var settings Settings
decoder := json.NewDecoder(file)
if err := decoder.Decode(&settings); err != nil {
return nil, err
}
// Ensure LastExportPath is set to a default value if it's empty
if settings.LastExportPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
+25 -12
View File
@@ -14,13 +14,16 @@ import (
"github.com/ojrac/opensimplex-go"
)
// Constants for Perlin noise generation
const (
alpha = 2.
beta = 2.
n = 3
)
// GenerateHeightmap creates a grayscale image representing the terrain's elevation using Perlin noise.
func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) image.Image {
// Initialize Perlin noise generator
p := perlin.NewPerlin(alpha, beta, n, seed)
img := image.NewGray(image.Rect(0, 0, width, height))
@@ -28,6 +31,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
scale = 100.0
}
// Use multiple goroutines to speed up noise generation
numGoroutines := runtime.NumCPU()
var wg sync.WaitGroup
rowsPerGoroutine := height / numGoroutines
@@ -43,6 +47,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
defer wg.Done()
for y := startY; y < endY; y++ {
for x := 0; x < width; x++ {
// Combine multiple octaves of noise for more detail
var noise float64
frequency := 1.0
amplitude := 1.0
@@ -55,6 +60,7 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
frequency *= 2.0
}
// Normalize the noise value and set the pixel color
noise /= maxAmplitude
grayColor := uint8((noise + 1) * 127.5)
img.SetGray(x, y, color.Gray{Y: grayColor})
@@ -67,11 +73,13 @@ func GenerateHeightmap(width, height, octaves int, scale float64, seed int64) im
return img
}
// ApplyRoughness adds a visual roughness effect to the heightmap.
func ApplyRoughness(heightmap image.Image, roughness float64) image.Image {
bounds := heightmap.Bounds()
composite := image.NewRGBA(bounds)
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
// The alpha value of the overlay determines the roughness effect
alphaValue := 255 - uint8(roughness*2.55)
overlay := image.NewUniform(color.RGBA{R: 128, G: 128, B: 128, A: alphaValue})
draw.Draw(composite, bounds, overlay, image.Point{}, draw.Over)
@@ -91,11 +99,11 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
lakeMask.Set(p.X, p.Y, black)
}
// Apply a Gaussian blur to the lake mask
// Apply a Gaussian blur to the lake mask to create smooth edges
blurRadius := float64(width) * 0.05
blurredLakeMask := imaging.Blur(lakeMask, blurRadius)
// Composite the blurred lake mask onto the heightmap with 50% opacity
// Composite the blurred lake mask onto the heightmap with some opacity
composite := image.NewRGBA(bounds)
draw.Draw(composite, bounds, heightmap, image.Point{}, draw.Src)
draw.DrawMask(composite, bounds, blurredLakeMask, image.Point{}, image.NewUniform(color.Alpha{192}), image.Point{}, draw.Over)
@@ -103,31 +111,33 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag
return composite
}
// FlattenRoadAreas smoothens the terrain under roads.
func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Image {
bounds := heightmap.Bounds()
width := bounds.Dx()
// Create a new image with the road pixels drawn on it.
// Create a mask with the road pixels
roadMask := image.NewGray(bounds)
for _, p := range roadPixels {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
// Blur the road mask.
// Blur the road mask to create a smooth transition
blurRadius := float64(width) * 0.01
blurredRoadMask := imaging.Blur(roadMask, blurRadius)
// Create a new image to store the blurred heightmap.
// Blur the entire heightmap
blurredHeightmap := imaging.Blur(heightmap, blurRadius)
// Create a new composite image.
// Create a new composite image
composite := image.NewRGBA(bounds)
// Interpolate between the original and blurred heightmap based on the road mask
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
maskAlpha, _, _, _ := blurredRoadMask.At(x, y).RGBA()
if maskAlpha > 0 {
// Linearly interpolate between the original and blurred heightmap based on the mask alpha.
// Linearly interpolate between the original and blurred heightmap
originalColor := heightmap.At(x, y)
blurredColor := blurredHeightmap.At(x, y)
@@ -151,11 +161,12 @@ func FlattenRoadAreas(heightmap image.Image, roadPixels []image.Point) image.Ima
return composite
}
// GenerateTrees places trees on the map.
func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point {
width := img.Bounds().Dx()
height := img.Bounds().Dy()
// 1. Calculate number of trees to place from coverage %.
// Step 1: Calculate the number of trees to place based on coverage percentage.
avgTreeSize := (minTreeSize + maxTreeSize) / 2
if avgTreeSize <= 0 {
return nil
@@ -172,7 +183,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
return nil
}
// 2. Generate a simplex noise map for tree placement.
// Step 2: Generate a simplex noise map to guide tree placement.
noise := opensimplex.New(seed)
treeNoiseMap := image.NewGray(image.Rect(0, 0, width, height))
treeNoiseZoom := 0.05
@@ -185,6 +196,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
}
threshold := uint8(255 * (1 - (treeCoverage / 100.0)))
// Create lookup maps for water, roads, and buildings for efficient collision detection
isLake := make(map[image.Point]bool)
for _, p := range lakePixels {
isLake[p] = true
@@ -202,7 +214,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
randSrc := rand.New(rand.NewSource(seed))
// 3. Determine initial clump trees
// Step 3: Determine initial points for clumps of trees.
numClumpTrees := min(int(treeClumpiness), numTreesToPlace)
initialPoints := make([]image.Point, 0, numClumpTrees)
@@ -216,14 +228,14 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
}
}
// 4. Place remaining trees using Bridson's Algorithm
// Step 4: Place remaining trees using Poisson Disc Sampling for a natural distribution.
minRadius := minTreeSize
allPoints := poissonDiscSampling(width, height, minRadius, 30, initialPoints, func(p image.Point) bool {
return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] && !isRoad[p] && !isBuilding[p]
}, seed)
var treePixels []image.Point
// 5. Draw the trees.
// Step 5: Draw the trees on the image.
numGoroutines := runtime.NumCPU()
if len(allPoints) < numGoroutines {
numGoroutines = len(allPoints)
@@ -282,6 +294,7 @@ func GenerateTrees(img *image.RGBA, lakePixels, roadPixels, buildingPixels []ima
return treePixels
}
// poissonDiscSampling generates points that are randomly distributed but no closer than a given minimum radius.
func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoints []image.Point, isValid func(image.Point) bool, seed int64) []image.Point {
randSrc := rand.New(rand.NewSource(seed))
points := initialPoints
+37 -17
View File
@@ -12,13 +12,15 @@ import (
"github.com/ojrac/opensimplex-go"
)
// lakePixel represents a potential pixel to be added to a lake during growth
// lakePixel represents a potential pixel to be added to a lake during growth.
// It is used in a priority queue to determine the next pixel to add.
type lakePixel struct {
point image.Point
score float64
index int // required for heap.Interface
}
// priorityQueue implements a max-heap for lakePixel structs.
type priorityQueue []*lakePixel
func (pq priorityQueue) Len() int { return len(pq) }
@@ -44,7 +46,9 @@ func (pq *priorityQueue) Pop() any {
return item
}
// GenerateLakes creates lakes on the map using a growth algorithm.
func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, heightmap image.Image, seed int64) (image.Image, [][]image.Point) {
// Initialize a white canvas to draw the lakes on
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
@@ -55,7 +59,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
var allLakes [][]image.Point
randSrc := rand.New(rand.NewSource(seed))
// 1. Divide the image into a grid
// Step 1: Divide the image into a grid to distribute the lakes.
gridDim := int(math.Ceil(math.Sqrt(float64(numLakes))))
if gridDim == 0 {
return canvas, nil
@@ -66,7 +70,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
return canvas, nil
}
// 2. Create a list of chunk indices and shuffle them to randomize lake placement
// Step 2: Create a shuffled list of chunk indices to randomize lake placement.
chunkIndices := make([]int, gridDim*gridDim)
for i := range chunkIndices {
chunkIndices[i] = i
@@ -78,7 +82,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
totalArea := float64(width * height)
noiseGen := opensimplex.New(seed)
// 3. Generate a lake in a subset of the chunks
// Step 3: Generate a lake in a subset of the chunks.
for i := range numLakes {
if i >= len(chunkIndices) {
break
@@ -86,7 +90,7 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
var currentLake []image.Point
// Each lake gets a random size within the defined range
// Each lake gets a random size within the defined range.
lakeSize := lakeSizeLower
if lakeSizeUpper > lakeSizeLower {
lakeSize = lakeSizeLower + randSrc.Float64()*(lakeSizeUpper-lakeSizeLower)
@@ -107,21 +111,21 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
(chunkGridY+1)*chunkHeight,
)
// Use the growth algorithm within the chunk
// Use a priority queue-based growth algorithm within the chunk.
pq := &priorityQueue{}
heap.Init(pq)
visited := make(map[image.Point]bool)
// Start near the center of the chunk
// Start the growth near the center of the chunk.
startPt := image.Point{
X: chunkRect.Min.X + chunkWidth/2,
Y: chunkRect.Min.Y + chunkHeight/2,
}
// just in case the center is out of bounds
if !startPt.In(chunkRect) {
continue
}
// Use noise to create a more natural lake shape.
seedX := randSrc.Float64() * 10000.0
seedY := randSrc.Float64() * 10000.0
radius := math.Sqrt(float64(targetPixelsPerLake) / math.Pi)
@@ -134,23 +138,23 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
distPenalty := math.Pow(dist/radius, 3.0)
luma, _, _, _ := heightmap.At(pt.X, pt.Y).RGBA()
heightmapVal := float64(luma) / 65535.0
heightmapEffect := (0.5 - heightmapVal) * 1.5
heightmapEffect := (0.5 - heightmapVal) * 1.5 // Encourage growth in lower areas
return noise - distPenalty + heightmapEffect
}
heap.Push(pq, &lakePixel{point: startPt, score: getScore(startPt)})
visited[startPt] = true
// Grow the lake until it reaches its target size.
lakeCount := 0
for pq.Len() > 0 && lakeCount < targetPixelsPerLake {
current := heap.Pop(pq).(*lakePixel)
// The pixel is valid, claim it.
canvas.Set(current.point.X, current.point.Y, color.RGBA{R: 0, G: 0, B: 255, A: 255})
currentLake = append(currentLake, current.point)
lakeCount++
// Add neighbors, constrained to the chunk rectangle
// Add neighbors to the priority queue.
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
if dx == 0 && dy == 0 {
@@ -178,12 +182,14 @@ func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper flo
return canvas, allLakes
}
// River represents a river on the map.
type River struct {
Width float64
Start, End image.Point
Points []image.Point
}
// GenerateRivers creates rivers on the map.
func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image) (image.Image, []image.Point) {
if numRivers == 0 {
return inputImage, nil
@@ -199,6 +205,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
randSrc := rand.New(rand.NewSource(seed))
avgDim := float64(width+height) / 2.0
// Create a map of water pixels for collision detection.
isWater := make(map[image.Point]bool)
lakePixelMap := make(map[image.Point]int)
for i, lake := range lakes {
@@ -208,6 +215,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
}
}
// Create rivers with varying widths.
rivers := make([]River, numRivers)
for i := range numRivers {
widthPercent := float64(i) / float64(numRivers-1)
@@ -217,31 +225,36 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
rivers[i].Width = maxWidth - widthPercent*(maxWidth-minWidth)
}
// Sort rivers by width in descending order.
sort.Slice(rivers, func(i, j int) bool {
return rivers[i].Width > rivers[j].Width
})
numControlPoints := max(int(avgDim*0.03), 60)
// Generate each river.
for i := range rivers {
r := &rivers[i]
// Determine the start and end edges of the river.
startEdge := randSrc.Intn(4)
endEdge := (startEdge + randSrc.Intn(3) + 1) % 4
r.Start = getPointOnEdge(width, height, startEdge, randSrc)
r.End = getPointOnEdge(width, height, endEdge, randSrc)
// Calculate the river's path.
path := calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints)
// Check for intersections with other water bodies.
for _, p := range path {
if isWater[p] {
if lakeIndex, isLake := lakePixelMap[p]; isLake {
// Intersection is with a lake, find its center
// If the river intersects with a lake, end the river at the lake's center.
lakeCenter := findCenter(lakes[lakeIndex])
r.End = lakeCenter
} else {
// Intersection is with another river
// If the river intersects with another river, end it at the intersection point.
r.End = p
}
path = calculateRiverPath(r.Start, r.End, curvyness/100.0, avgDim, randSrc, numControlPoints)
@@ -249,11 +262,11 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
}
}
// Draw the river on the canvas.
riverWidthPx := (r.Width / 100.0) * avgDim
radius := riverWidthPx / 2.0
for _, p := range path {
// When drawing river pixels, add them to isWater to detect river-river intersections
drawCircle(canvas, p, radius, color.RGBA{R: 0, G: 0, B: 255, A: 255}, &allRiverPixels, isWater, heightmap)
}
r.Points = path
@@ -262,6 +275,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
return canvas, allRiverPixels
}
// bresenhamRiver creates a path between control points using Bresenham's line algorithm.
func bresenhamRiver(path []image.Point) []image.Point {
if len(path) < 2 {
return path
@@ -301,6 +315,7 @@ func bresenhamRiver(path []image.Point) []image.Point {
return fullPath
}
// calculateRiverPath computes the path for a river, including curves.
func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int) []image.Point {
dx := end.X - start.X
dy := end.Y - start.Y
@@ -314,6 +329,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
return bresenhamRiver([]image.Point{start, end})
}
// Use sine waves to create curves in the river.
type wave struct {
amplitude float64
numWaves float64
@@ -339,6 +355,7 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
amp /= 3
}
// Generate control points for the curve.
controlPoints := make([]image.Point, numControlPoints+1)
for i := 0; i <= numControlPoints; i++ {
t := float64(i) / float64(numControlPoints)
@@ -357,9 +374,11 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
}
// Create the final path using Bresenham's algorithm between control points.
return bresenhamRiver(controlPoints)
}
// findCenter finds the center of a slice of points.
func findCenter(pixels []image.Point) image.Point {
if len(pixels) == 0 {
return image.Point{}
@@ -375,6 +394,7 @@ func findCenter(pixels []image.Point) image.Point {
}
}
// getPointOnEdge returns a random point on a specified edge of the map.
func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
switch edge {
case 0: // Top
@@ -387,6 +407,8 @@ func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
return image.Point{X: 0, Y: randSrc.Intn(height)}
}
}
// drawCircle draws a circle on the image and adds its pixels to the given slice.
func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Color, pixels *[]image.Point, isWater map[image.Point]bool, heightmap image.Image) {
bounds := img.Bounds()
r2 := radius * radius
@@ -405,12 +427,10 @@ func drawCircle(img *image.RGBA, center image.Point, radius float64, c color.Col
if dist2 <= r2 {
if !isWater[p] {
// Roughen the outer 15% of the river
// Roughen the outer 15% of the river based on the heightmap.
if dist2 > innerR2 {
luma, _, _, _ := heightmap.At(x, y).RGBA()
// Normalize luma to 0-1 range
heightmapVal := float64(luma) / 65535.0
// Roughen the edges based on the heightmap
if heightmapVal < 0.5 {
continue
}