Files
RPG_City_Maker_Reborn/main.go
T

978 lines
32 KiB
Go
Raw Normal View History

package main
2026-02-05 15:11:55 -06:00
///okie dokie now let's add a major feature: building generation. We should create a new tab for buildings in the program and create a new file buildings.go for the generation logic. I'd like there to be several settings for the building generation. First is the number of buildings, which should number from 0 to 10000. Secondly is the minimum and maximum building size, which should exactly mirror how tree size is determined, with the exception that it's not the diameter of a circle, but instead the crictical dimension of a shape (see below). Next is distribution, ranging from 0% to 100%. At 0% distribution buildings will be placed directly next to roads and each other. At 100% distribution they will be scattered randomly around the map. We need a setting for building shape represented as a dropdown. The first setting will be "squares" (where each building is just a square, with the critical dimension being side length) then "circles" (critical dimension diameter) then "rectangles" (critical dimension longest side length, side lengths are determined randomly from min and max with the longest side length being equal to or shorter then the max and the shortest stide length being above or equal to the minimum size). No part of a building should be placed on water. No part of a building should be placed on a road. We should generate trees after buildings and not allow the center of a tree to be placed on a building.
import (
2026-02-02 13:26:28 -06:00
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"image"
"image/color"
2026-02-02 13:26:28 -06:00
"image/jpeg"
"image/png"
2026-02-02 13:26:28 -06:00
"log"
"os"
"path/filepath"
2026-02-02 13:26:28 -06:00
"strconv"
2026-02-02 12:24:14 -06:00
"strings"
2026-02-02 13:26:28 -06:00
"time"
2026-02-02 12:24:14 -06:00
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
2026-02-02 12:24:14 -06:00
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
2026-02-02 12:24:14 -06:00
"github.com/chai2010/webp"
)
func main() {
2026-02-02 12:24:14 -06:00
a := app.NewWithID("com.example.rpgcitymaker")
2026-01-28 12:35:10 -06:00
a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()})
w := a.NewWindow("RPG City Maker")
w.Resize(fyne.NewSize(800, 600))
settings, err := LoadSettings()
if err != nil {
log.Println("Error loading settings:", err)
// Use default settings if loading fails
2026-01-27 12:00:19 -06:00
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
}
canvasImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
heightmapImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
2026-02-02 13:26:28 -06:00
var lakes [][]image.Point
var riverPixels []image.Point
var treePixels []image.Point
2026-02-05 15:11:55 -06:00
var buildingPixels []image.Point
2026-02-04 14:19:18 -06:00
var roadPixels []image.Point
2026-02-04 16:37:50 -06:00
var bridgePixels []image.Point
2026-02-02 13:26:28 -06:00
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Failed to get user config dir:", err)
}
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
canvasPath := filepath.Join(appConfigDir, "canvas.png")
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
w.SetOnClosed(func() {
if err := settings.Save(); err != nil {
log.Println("Error saving settings:", err)
}
if canvasImg.Image != nil {
canvasFile, err := os.Create(canvasPath)
if err != nil {
log.Println("Failed to create canvas file:", err)
} else {
defer canvasFile.Close()
if err := png.Encode(canvasFile, canvasImg.Image); err != nil {
log.Println("Failed to encode canvas image:", err)
}
}
}
if heightmapImg.Image != nil {
heightmapFile, err := os.Create(heightmapPath)
if err != nil {
log.Println("Failed to create heightmap file:", err)
} else {
defer heightmapFile.Close()
if err := png.Encode(heightmapFile, heightmapImg.Image); err != nil {
log.Println("Failed to encode heightmap image:", err)
}
}
}
})
canvasFile, err := os.Open(canvasPath)
if err == nil {
defer canvasFile.Close()
img, err := png.Decode(canvasFile)
if err == nil {
canvasImg.Image = img
}
}
heightmapFile, err := os.Open(heightmapPath)
if err == nil {
defer heightmapFile.Close()
img, err := png.Decode(heightmapFile)
if err == nil {
heightmapImg.Image = img
}
}
if canvasImg.Image == nil || heightmapImg.Image == nil {
2026-02-04 14:19:18 -06:00
// Initial image generation
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider(settings.Seed)
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-02-02 13:26:28 -06:00
var lakeImage image.Image
2026-02-04 14:19:18 -06:00
2026-02-04 15:15:47 -06:00
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-02-02 13:26:28 -06:00
var riverImage image.Image
2026-02-04 14:19:18 -06:00
2026-02-04 15:15:47 -06:00
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
2026-02-04 14:19:18 -06:00
2026-01-30 10:38:00 -06:00
finalImage := riverImage.(*image.RGBA)
2026-01-30 10:55:15 -06:00
2026-02-04 14:19:18 -06:00
var roadImage *image.RGBA
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
2026-01-30 10:55:15 -06:00
allWaterPixels := append(flatLakePixels, riverPixels...)
2026-02-04 16:37:50 -06:00
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next())
2026-02-04 14:50:36 -06:00
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
2026-01-30 10:55:15 -06:00
2026-02-05 15:11:55 -06:00
buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
2026-02-04 14:19:18 -06:00
2026-02-04 16:13:06 -06:00
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
2026-02-04 14:19:18 -06:00
heightmapImg.Image = compositeImg
2026-02-04 14:19:18 -06:00
canvasImg.Image = finalImage
2026-02-04 14:19:18 -06:00
}
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
detailSlider := widget.NewSlider(1, 16)
detailSlider.OnChanged = func(val float64) {
settings.Detail = val
detailLabel.SetText(fmt.Sprintf("Detail: %.0f", settings.Detail))
}
detailSlider.SetValue(settings.Detail)
roughnessLabel := widget.NewLabel(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
roughnessSlider := widget.NewSlider(0, 100)
roughnessSlider.OnChanged = func(val float64) {
settings.Roughness = val
roughnessLabel.SetText(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
}
roughnessSlider.SetValue(settings.Roughness)
lakesLabel := widget.NewLabel(fmt.Sprintf("Lakes: %d", settings.Lakes))
lakesSlider := widget.NewSlider(0, 15)
lakesSlider.OnChanged = func(val float64) {
settings.Lakes = int(val)
lakesLabel.SetText(fmt.Sprintf("Lakes: %d", settings.Lakes))
}
lakesSlider.SetValue(float64(settings.Lakes))
2026-01-27 12:00:19 -06:00
lakeSizeLowerLabel := widget.NewLabel(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower))
lakeSizeLowerSlider := widget.NewSlider(1, 100)
lakeSizeUpperLabel := widget.NewLabel(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
lakeSizeUpperSlider := widget.NewSlider(1, 100)
lakeSizeLowerSlider.OnChanged = func(val float64) {
settings.LakeSizeLower = val
if settings.LakeSizeLower > settings.LakeSizeUpper {
settings.LakeSizeUpper = settings.LakeSizeLower
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
}
lakeSizeLowerLabel.SetText(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower))
}
2026-01-27 12:00:19 -06:00
lakeSizeLowerSlider.SetValue(settings.LakeSizeLower)
lakeSizeUpperSlider.OnChanged = func(val float64) {
settings.LakeSizeUpper = val
if settings.LakeSizeUpper < settings.LakeSizeLower {
settings.LakeSizeLower = settings.LakeSizeUpper
lakeSizeLowerSlider.SetValue(settings.LakeSizeLower)
}
lakeSizeUpperLabel.SetText(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
}
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
2026-01-30 10:38:00 -06:00
riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers))
riversSlider := widget.NewSlider(0, 5)
riversSlider.OnChanged = func(val float64) {
settings.Rivers = int(val)
riversLabel.SetText(fmt.Sprintf("Rivers: %d", settings.Rivers))
}
riversSlider.SetValue(float64(settings.Rivers))
minRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth))
minRiverWidthSlider := widget.NewSlider(1, 100)
maxRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth))
maxRiverWidthSlider := widget.NewSlider(1, 100)
minRiverWidthSlider.OnChanged = func(val float64) {
settings.MinRiverWidth = val
if settings.MinRiverWidth > settings.MaxRiverWidth {
settings.MaxRiverWidth = settings.MinRiverWidth
maxRiverWidthSlider.SetValue(settings.MaxRiverWidth)
}
minRiverWidthLabel.SetText(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth))
}
minRiverWidthSlider.SetValue(settings.MinRiverWidth)
maxRiverWidthSlider.OnChanged = func(val float64) {
settings.MaxRiverWidth = val
if settings.MaxRiverWidth < settings.MinRiverWidth {
settings.MinRiverWidth = settings.MaxRiverWidth
minRiverWidthSlider.SetValue(settings.MinRiverWidth)
}
maxRiverWidthLabel.SetText(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth))
}
maxRiverWidthSlider.SetValue(settings.MaxRiverWidth)
riverCurvynessLabel := widget.NewLabel(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
riverCurvynessSlider := widget.NewSlider(0, 100)
riverCurvynessSlider.OnChanged = func(val float64) {
settings.RiverCurvyness = val
riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
}
riverCurvynessSlider.SetValue(settings.RiverCurvyness)
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))
maxTreeSizeSlider := widget.NewSlider(1, 150)
minTreeSizeSlider.OnChanged = func(val float64) {
settings.MinTreeSize = val
if settings.MinTreeSize > settings.MaxTreeSize {
settings.MaxTreeSize = settings.MinTreeSize
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
}
minTreeSizeLabel.SetText(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
}
minTreeSizeSlider.SetValue(settings.MinTreeSize)
maxTreeSizeSlider.OnChanged = func(val float64) {
settings.MaxTreeSize = val
if settings.MaxTreeSize < settings.MinTreeSize {
settings.MinTreeSize = settings.MaxTreeSize
minTreeSizeSlider.SetValue(settings.MinTreeSize)
}
maxTreeSizeLabel.SetText(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
}
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
treeCoverageLabel := widget.NewLabel(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
treeCoverageSlider := widget.NewSlider(1, 100)
treeCoverageSlider.OnChanged = func(val float64) {
settings.TreeCoverage = val
treeCoverageLabel.SetText(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
}
treeCoverageSlider.SetValue(settings.TreeCoverage)
treeClumpinessLabel := widget.NewLabel(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
treeClumpinessSlider := widget.NewSlider(0, 100)
treeClumpinessSlider.OnChanged = func(val float64) {
settings.TreeClumpiness = val
treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
}
treeClumpinessSlider.SetValue(settings.TreeClumpiness)
2026-02-04 14:19:18 -06:00
numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
2026-02-05 08:35:22 -06:00
numRoadsSlider := widget.NewSlider(0, 2000)
2026-02-04 14:19:18 -06:00
numRoadsSlider.OnChanged = func(val float64) {
settings.NumRoads = int(val)
numRoadsLabel.SetText(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
}
numRoadsSlider.SetValue(float64(settings.NumRoads))
minRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth))
minRoadWidthSlider := widget.NewSlider(1, 150)
maxRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth))
maxRoadWidthSlider := widget.NewSlider(1, 150)
minRoadWidthSlider.OnChanged = func(val float64) {
settings.MinRoadWidth = val
if settings.MinRoadWidth > settings.MaxRoadWidth {
settings.MaxRoadWidth = settings.MinRoadWidth
maxRoadWidthSlider.SetValue(settings.MaxRoadWidth)
}
minRoadWidthLabel.SetText(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth))
}
minRoadWidthSlider.SetValue(settings.MinRoadWidth)
maxRoadWidthSlider.OnChanged = func(val float64) {
settings.MaxRoadWidth = val
if settings.MaxRoadWidth < settings.MinRoadWidth {
settings.MinRoadWidth = settings.MaxRoadWidth
minRoadWidthSlider.SetValue(settings.MinRoadWidth)
}
maxRoadWidthLabel.SetText(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth))
}
maxRoadWidthSlider.SetValue(settings.MaxRoadWidth)
roadExitsLabel := widget.NewLabel(fmt.Sprintf("Road Exits: %d", settings.RoadExits))
2026-02-04 14:50:36 -06:00
roadExitsSlider := widget.NewSlider(0, 100)
2026-02-04 14:19:18 -06:00
roadExitsSlider.OnChanged = func(val float64) {
if val > float64(settings.NumRoads) {
val = float64(settings.NumRoads)
roadExitsSlider.SetValue(val)
}
settings.RoadExits = int(val)
roadExitsLabel.SetText(fmt.Sprintf("Road Exits: %d", settings.RoadExits))
}
roadExitsSlider.SetValue(float64(settings.RoadExits))
roadCurvynessLabel := widget.NewLabel(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness))
roadCurvynessSlider := widget.NewSlider(0, 100)
roadCurvynessSlider.OnChanged = func(val float64) {
settings.RoadCurvyness = val
roadCurvynessLabel.SetText(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness))
}
roadCurvynessSlider.SetValue(settings.RoadCurvyness)
roadDistributionLabel := widget.NewLabel(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution))
roadDistributionSlider := widget.NewSlider(0, 100)
roadDistributionSlider.OnChanged = func(val float64) {
settings.RoadDistribution = val
roadDistributionLabel.SetText(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution))
}
roadDistributionSlider.SetValue(settings.RoadDistribution)
errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255})
errorLabel.TextSize = 12
errorLabel.Hide()
2026-01-28 12:35:10 -06:00
var generateBtn *widget.Button
progressBar := NewTextOverlayProgressBar()
2026-02-02 12:40:25 -06:00
exportCanvasBtn := widget.NewButton("Export Canvas", func() {
2026-02-02 12:24:14 -06:00
showSaveDialog(w, canvasImg.Image, settings)
})
2026-02-02 12:40:25 -06:00
exportHeightmapBtn := widget.NewButton("Export Heightmap", func() {
showSaveDialog(w, heightmapImg.Image, settings)
})
2026-02-02 13:26:28 -06:00
exportMasksBtn := widget.NewButton("Export Masks", func() {
2026-02-05 15:11:55 -06:00
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels)
2026-02-02 13:26:28 -06:00
})
2026-01-28 12:35:10 -06:00
generateBtn = widget.NewButton("Generate", func() {
go func() {
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
generateBtn.Disable()
})
defer func() {
fyne.Do(func() {
generateBtn.Enable()
})
}()
2026-01-28 12:35:10 -06:00
2026-02-05 15:11:55 -06:00
steps := 9
2026-01-28 12:35:10 -06:00
currentStep := 0
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider(settings.Seed)
2026-01-28 12:35:10 -06:00
// Step 1: Generating Heightmap
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Heightmap", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-04 15:15:47 -06:00
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
2026-01-28 12:35:10 -06:00
// Step 2: Generating Lakes
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Lakes", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-02 13:26:28 -06:00
var lakeImage image.Image
2026-02-04 15:15:47 -06:00
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
// Step 3: Generating Rivers
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Rivers", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-02 13:26:28 -06:00
var riverImage image.Image
2026-02-04 15:15:47 -06:00
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
finalImage := riverImage.(*image.RGBA)
2026-02-04 14:19:18 -06:00
2026-02-04 14:50:36 -06:00
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
allWaterPixels := append(flatLakePixels, riverPixels...)
2026-02-04 14:19:18 -06:00
// Step 4: Generating Roads
currentStep++
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Roads", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
var roadImage *image.RGBA
2026-02-04 16:37:50 -06:00
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // Draw roadImage over finalImage
2026-02-04 14:19:18 -06:00
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
2026-02-05 15:11:55 -06:00
// Step 5: Generating Buildings
currentStep++
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Buildings", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
// Step 6: Darkening Water Areas
2026-01-30 10:38:00 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Water Areas", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
2026-02-04 16:13:06 -06:00
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
2026-01-30 10:38:00 -06:00
2026-02-05 15:11:55 -06:00
// Step 7: Applying Roughness
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Applying Roughness", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
2026-01-28 12:35:10 -06:00
2026-02-05 15:11:55 -06:00
// Step 8: Generating Trees
2026-01-28 13:06:01 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-05 15:11:55 -06:00
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
2026-01-28 13:06:01 -06:00
2026-02-05 15:11:55 -06:00
// Step 9: Finalizing Images
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
heightmapImg.Image = compositeImg
heightmapImg.Refresh()
canvasImg.Image = finalImage
canvasImg.Refresh()
2026-01-28 12:35:10 -06:00
2026-02-02 10:54:09 -06:00
progressBar.SetText("Generation Complete!")
})
2026-01-28 12:35:10 -06:00
}()
})
widthEntry := widget.NewEntry()
widthEntry.SetText(strconv.Itoa(settings.Width))
widthEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Width must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Width = val
}
heightEntry := widget.NewEntry()
heightEntry.SetText(strconv.Itoa(settings.Height))
heightEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Height must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Height = val
}
seedEntry := widget.NewEntry()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
seedEntry.OnChanged = func(s string) {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
errorLabel.Text = "Seed must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Seed = val
}
randomizeBtn := widget.NewButton("Randomize", func() {
settings.Seed = time.Now().UnixNano()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
})
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
detailLabel,
detailSlider,
roughnessLabel,
roughnessSlider,
2026-02-02 10:15:26 -06:00
widget.NewLabel(""), // Spacer
minTreeSizeLabel,
minTreeSizeSlider,
maxTreeSizeLabel,
maxTreeSizeSlider,
treeCoverageLabel,
treeCoverageSlider,
treeClumpinessLabel,
treeClumpinessSlider,
))
waterTab := container.NewTabItem("Water", container.NewVBox(
lakesLabel,
lakesSlider,
2026-01-27 12:00:19 -06:00
lakeSizeLowerLabel,
lakeSizeLowerSlider,
lakeSizeUpperLabel,
lakeSizeUpperSlider,
2026-01-30 10:38:00 -06:00
widget.NewLabel(""), // Spacer
riversLabel,
riversSlider,
minRiverWidthLabel,
minRiverWidthSlider,
maxRiverWidthLabel,
maxRiverWidthSlider,
riverCurvynessLabel,
riverCurvynessSlider,
))
2026-02-04 14:19:18 -06:00
roadDistributionSlider.SetValue(settings.RoadDistribution)
roadsTab := container.NewTabItem("Roads", container.NewVBox(
numRoadsLabel,
numRoadsSlider,
minRoadWidthLabel,
minRoadWidthSlider,
maxRoadWidthLabel,
maxRoadWidthSlider,
roadExitsLabel,
roadExitsSlider,
roadCurvynessLabel,
roadCurvynessSlider,
roadDistributionLabel,
roadDistributionSlider,
))
2026-02-05 15:11:55 -06:00
numBuildingsLabel := widget.NewLabel(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings))
numBuildingsSlider := widget.NewSlider(0, 10000)
numBuildingsSlider.OnChanged = func(val float64) {
settings.NumBuildings = int(val)
numBuildingsLabel.SetText(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings))
}
numBuildingsSlider.SetValue(float64(settings.NumBuildings))
minBuildingSizeLabel := widget.NewLabel(fmt.Sprintf("Min Building Size: %.0fpx", settings.MinBuildingSize))
minBuildingSizeSlider := widget.NewSlider(1, 150)
maxBuildingSizeLabel := widget.NewLabel(fmt.Sprintf("Max Building Size: %.0fpx", settings.MaxBuildingSize))
maxBuildingSizeSlider := widget.NewSlider(1, 150)
minBuildingSizeSlider.OnChanged = func(val float64) {
settings.MinBuildingSize = val
if settings.MinBuildingSize > settings.MaxBuildingSize {
settings.MaxBuildingSize = settings.MinBuildingSize
maxBuildingSizeSlider.SetValue(settings.MaxBuildingSize)
}
minBuildingSizeLabel.SetText(fmt.Sprintf("Min Building Size: %.0fpx", settings.MinBuildingSize))
}
minBuildingSizeSlider.SetValue(settings.MinBuildingSize)
maxBuildingSizeSlider.OnChanged = func(val float64) {
settings.MaxBuildingSize = val
if settings.MaxBuildingSize < settings.MinBuildingSize {
settings.MinBuildingSize = settings.MaxBuildingSize
minBuildingSizeSlider.SetValue(settings.MinBuildingSize)
}
maxBuildingSizeLabel.SetText(fmt.Sprintf("Max Building Size: %.0fpx", settings.MaxBuildingSize))
}
maxBuildingSizeSlider.SetValue(settings.MaxBuildingSize)
buildingDistributionLabel := widget.NewLabel(fmt.Sprintf("Building Distribution: %.0f%%", settings.BuildingDistribution))
buildingDistributionSlider := widget.NewSlider(0, 100)
buildingDistributionSlider.OnChanged = func(val float64) {
settings.BuildingDistribution = val
buildingDistributionLabel.SetText(fmt.Sprintf("Building Distribution: %.0f%%", settings.BuildingDistribution))
}
buildingDistributionSlider.SetValue(settings.BuildingDistribution)
buildingShapeLabel := widget.NewLabel("Building Shape:")
buildingShapeSelect := widget.NewSelect([]string{"squares", "circles", "rectangles"}, func(s string) {
settings.BuildingShape = s
})
buildingShapeSelect.SetSelected(settings.BuildingShape)
buildingsTab := container.NewTabItem("Buildings", container.NewVBox(
numBuildingsLabel,
numBuildingsSlider,
minBuildingSizeLabel,
minBuildingSizeSlider,
maxBuildingSizeLabel,
maxBuildingSizeSlider,
buildingDistributionLabel,
buildingDistributionSlider,
buildingShapeLabel,
buildingShapeSelect,
))
imageTab := container.NewTabItem("Image", container.NewVBox(
widget.NewLabel("Width:"),
2026-02-04 14:19:18 -06:00
widthEntry, widget.NewLabel("Height:"),
heightEntry,
widget.NewLabel("Seed:"),
seedEntry,
randomizeBtn,
generateBtn,
errorLabel,
2026-01-28 12:35:10 -06:00
progressBar,
2026-02-02 12:24:14 -06:00
widget.NewSeparator(),
2026-02-02 12:40:25 -06:00
exportCanvasBtn,
exportHeightmapBtn,
2026-02-02 13:26:28 -06:00
exportMasksBtn,
))
tabs := container.NewAppTabs(
imageTab,
terrainTab,
2026-02-02 10:15:26 -06:00
waterTab,
2026-02-04 14:19:18 -06:00
roadsTab,
2026-02-05 15:11:55 -06:00
buildingsTab,
)
left := container.NewVBox(
2026-02-02 10:15:26 -06:00
widget.NewLabel("RPG City Maker Reborn"),
tabs,
)
right := container.NewGridWithRows(2,
canvasImg,
heightmapImg,
)
split := container.NewHSplit(
left,
right,
)
split.SetOffset(0.3)
w.SetContent(split)
w.ShowAndRun()
}
2026-02-02 13:26:28 -06:00
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
var err error
switch format {
case "PNG":
err = png.Encode(buf, img)
case "JPG":
err = jpeg.Encode(buf, img, nil)
case "WEBP":
err = webp.Encode(buf, img, &webp.Options{Lossless: true})
}
return buf, err
}
2026-02-05 15:11:55 -06:00
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point) {
2026-02-02 13:26:28 -06:00
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("masks_folder")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
packageSelect := widget.NewSelect([]string{"Folder", "tar.gz", "zip"}, nil)
packageSelect.SetSelected("Folder")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Folder Name:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
widget.NewLabel("Packaging:"),
packageSelect,
)
saveDialog := dialog.NewCustom("Export Masks", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
folderName := fileNameEntry.Text
if folderName == "" {
return
}
imgFormat := strings.ToLower(formatSelect.Selected)
bounds := canvasImg.Bounds()
// Create mask images
lakeMask := image.NewGray(bounds)
for _, lake := range lakes {
for _, p := range lake {
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
riverMask := image.NewGray(bounds)
for _, p := range riverPixels {
riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
treeMask := image.NewGray(bounds)
for _, p := range treePixels {
treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-04 14:19:18 -06:00
roadMask := image.NewGray(bounds)
for _, p := range roadPixels {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-04 16:37:50 -06:00
bridgeMask := image.NewGray(bounds)
for _, p := range bridgePixels {
bridgeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-05 15:11:55 -06:00
buildingMask := image.NewGray(bounds)
for _, p := range buildingPixels {
buildingMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-02 13:26:28 -06:00
imagesToSave := map[string]image.Image{
2026-02-05 15:11:55 -06:00
"canvas." + imgFormat: canvasImg,
"heightmap." + imgFormat: heightmapImg,
"lakes_mask." + imgFormat: lakeMask,
"rivers_mask." + imgFormat: riverMask,
"trees_mask." + imgFormat: treeMask,
"roads_mask." + imgFormat: roadMask,
"bridges_mask." + imgFormat: bridgeMask,
"buildings_mask." + imgFormat: buildingMask,
2026-02-02 13:26:28 -06:00
}
switch packageSelect.Selected {
case "Folder":
exportPath := filepath.Join(pathLabel.Text, folderName)
if err := os.MkdirAll(exportPath, 0755); err != nil {
log.Println("Error creating directory:", err)
return
}
settings.LastExportPath = exportPath
for name, img := range imagesToSave {
saveImage(img, filepath.Join(exportPath, name))
}
case "tar.gz":
filePath := filepath.Join(pathLabel.Text, folderName+".tar.gz")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
gw := gzip.NewWriter(file)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
hdr := &tar.Header{
Name: name,
Mode: 0644,
Size: int64(buf.Len()),
}
if err := tw.WriteHeader(hdr); err != nil {
log.Printf("Error writing tar header for %s: %v\n", name, err)
continue
}
if _, err := tw.Write(buf.Bytes()); err != nil {
log.Printf("Error writing tar data for %s: %v\n", name, err)
}
}
case "zip":
filePath := filepath.Join(pathLabel.Text, folderName+".zip")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
zw := zip.NewWriter(file)
defer zw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
f, err := zw.Create(name)
if err != nil {
log.Printf("Error creating zip entry for %s: %v\n", name, err)
continue
}
if _, err := f.Write(buf.Bytes()); err != nil {
log.Printf("Error writing zip data for %s: %v\n", name, err)
}
}
}
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}
func saveImage(img image.Image, path string) {
file, err := os.Create(path)
if err != nil {
log.Println("Error creating file:", err)
return
}
defer file.Close()
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".png":
err = png.Encode(file, img)
case ".jpg":
err = jpeg.Encode(file, img, nil)
case ".webp":
err = webp.Encode(file, img, &webp.Options{Lossless: true})
}
if err != nil {
log.Println("Failed to encode image:", err)
}
}
2026-02-02 12:24:14 -06:00
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("image")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Filename:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
)
saveDialog := dialog.NewCustom("Export Image", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
fileName := fileNameEntry.Text
if fileName == "" {
return
}
filePath := filepath.Join(pathLabel.Text, fileName+"."+strings.ToLower(formatSelect.Selected))
settings.LastExportPath = filepath.Dir(filePath)
2026-02-02 13:26:28 -06:00
saveImage(img, filePath)
2026-02-02 12:24:14 -06:00
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}