2026-01-26 14:29:05 -06:00
package main
import (
2026-02-02 13:26:28 -06:00
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
2026-01-26 14:29:05 -06:00
"fmt"
2026-01-27 15:16:23 -06:00
"image"
"image/color"
2026-02-02 13:26:28 -06:00
"image/jpeg"
2026-01-27 15:16:23 -06:00
"image/png"
2026-02-02 13:26:28 -06:00
"log"
2026-01-27 15:16:23 -06:00
"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
2026-01-26 14:29:05 -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"
2026-01-26 14:29:05 -06:00
"fyne.io/fyne/v2/widget"
2026-02-02 12:24:14 -06:00
"github.com/chai2010/webp"
2026-01-26 14:29:05 -06:00
)
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 ()})
2026-01-26 14:29:05 -06:00
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 }
2026-01-26 14:29:05 -06:00
}
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
2026-01-27 15:16:23 -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
2026-01-27 15:16:23 -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
2026-01-27 15:16:23 -06:00
heightmapImg . Image = compositeImg
2026-02-04 14:19:18 -06:00
2026-01-27 15:16:23 -06:00
canvasImg . Image = finalImage
2026-02-04 14:19:18 -06:00
2026-01-27 15:16:23 -06:00
}
2026-01-26 14:29:05 -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-26 14:29:05 -06:00
}
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-26 14:29:05 -06:00
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 )
2026-01-27 15:16:23 -06:00
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 )
2026-01-26 14:29:05 -06:00
2026-01-27 15:16:23 -06:00
minTreeSizeSlider . OnChanged = func ( val float64 ) {
settings . MinTreeSize = val
if settings . MinTreeSize > settings . MaxTreeSize {
settings . MaxTreeSize = settings . MinTreeSize
maxTreeSizeSlider . SetValue ( settings . MaxTreeSize )
2026-01-26 14:29:05 -06:00
}
2026-01-27 15:16:23 -06:00
minTreeSizeLabel . SetText ( fmt . Sprintf ( "Min Tree Size: %.0fpx" , settings . MinTreeSize ))
2026-01-26 14:29:05 -06:00
}
2026-01-27 15:16:23 -06:00
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 )
2026-01-27 15:16:23 -06:00
errorLabel := canvas . NewText ( "" , color . RGBA { R : 255 , A : 255 })
errorLabel . TextSize = 12
errorLabel . Hide ()
2026-01-26 14:29:05 -06:00
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
}()
2026-01-26 14:29:05 -06:00
})
2026-01-27 15:16:23 -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 ))
})
2026-01-26 14:29:05 -06:00
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 (
2026-01-26 14:29:05 -06:00
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-01-26 14:29:05 -06:00
))
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 ,
))
2026-01-26 14:29:05 -06:00
imageTab := container . NewTabItem ( "Image" , container . NewVBox (
widget . NewLabel ( "Width:" ),
2026-02-04 14:19:18 -06:00
widthEntry , widget . NewLabel ( "Height:" ),
2026-01-26 14:29:05 -06:00
heightEntry ,
2026-01-27 15:16:23 -06:00
widget . NewLabel ( "Seed:" ),
seedEntry ,
randomizeBtn ,
2026-01-26 14:29:05 -06:00
generateBtn ,
2026-01-27 15:16:23 -06:00
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 ,
2026-01-26 14:29:05 -06:00
))
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 ,
2026-01-26 14:29:05 -06:00
)
left := container . NewVBox (
2026-02-02 10:15:26 -06:00
widget . NewLabel ( "RPG City Maker Reborn" ),
2026-01-26 14:29:05 -06:00
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 ()
}