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-12 14:33:47 -06:00
"fyne.io/fyne/v2/data/binding"
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-05 17:50:55 -06:00
// Initialize the Fyne application and window
2026-02-02 12:24:14 -06:00
a := app . NewWithID ( "com.example.rpgcitymaker" )
2026-01-26 14:29:05 -06:00
w := a . NewWindow ( "RPG City Maker" )
w . Resize ( fyne . NewSize ( 800 , 600 ))
2026-02-05 17:50:55 -06:00
// Load settings from file, or use defaults if loading fails
2026-01-26 14:29:05 -06:00
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
}
2026-02-05 17:50:55 -06:00
// Create canvas objects for displaying the generated images
2026-01-26 14:29:05 -06:00
canvasImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
heightmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
2026-02-09 10:13:36 -06:00
bumpmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
2026-01-26 14:29:05 -06:00
2026-02-05 17:50:55 -06:00
// Initialize slices to store generated map features
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-09 09:41:35 -06:00
var buildings [][] 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-02-05 17:50:55 -06:00
// Set up application configuration directory
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" )
2026-02-09 10:13:36 -06:00
bumpmapPath := filepath . Join ( appConfigDir , "bumpmap.png" )
2026-02-05 17:50:55 -06:00
// Save settings and images on window close
2026-01-27 15:16:23 -06:00
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 )
}
}
}
2026-02-09 10:13:36 -06:00
if bumpmapImg . Image != nil {
bumpmapFile , err := os . Create ( bumpmapPath )
if err != nil {
log . Println ( "Failed to create bumpmap file:" , err )
} else {
defer bumpmapFile . Close ()
if err := png . Encode ( bumpmapFile , bumpmapImg . Image ); err != nil {
log . Println ( "Failed to encode bumpmap image:" , err )
}
}
}
2026-01-27 15:16:23 -06:00
})
2026-02-05 17:50:55 -06:00
// Load previously saved images
2026-01-27 15:16:23 -06:00
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
}
}
2026-02-09 10:13:36 -06:00
bumpmapFile , err := os . Open ( bumpmapPath )
if err == nil {
defer bumpmapFile . Close ()
img , err := png . Decode ( bumpmapFile )
if err == nil {
bumpmapImg . Image = img
}
}
2026-02-05 17:50:55 -06:00
// Generate initial images if none are loaded
2026-01-27 15:16:23 -06:00
if canvasImg . Image == nil || heightmapImg . Image == nil {
2026-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Step 1: Generating Heightmap
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-05 17:50:55 -06:00
// Step 2: Generating Lakes
2026-02-02 13:26:28 -06:00
var lakeImage image . Image
2026-02-04 14:19:18 -06:00
2026-02-19 10:29:17 -06:00
lakeImage , lakes = GenerateLakes ( settings . Width , settings . Height , settings . Lakes , settings . LakeSizeLower , settings . LakeSizeUpper , seedProvider . Next (), settings . LakeEdgeRoughness , settings . LakeShape )
2026-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Step 3: Generating Rivers
2026-02-02 13:26:28 -06:00
var riverImage image . Image
2026-02-04 14:19:18 -06:00
2026-02-18 14:33:09 -06:00
riverImage , riverPixels = GenerateRivers ( settings . Width , settings . Height , settings . Rivers , settings . MinRiverWidth , settings . MaxRiverWidth , settings . RiverCurvyness , lakeImage , lakes , seedProvider . Next (), noiseImg , settings . RiverWidthVariability , settings . RiverEdgeRoughness )
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-05 17:50:55 -06:00
// Step 4: Generating Roads
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 17:50:55 -06:00
// Step 5: Generating Buildings
2026-02-09 09:41:35 -06:00
buildings , buildingPixels = GenerateBuildings ( finalImage , settings . Width , settings . Height , settings , roadPixels , allWaterPixels , seedProvider . Next ())
2026-02-05 15:11:55 -06:00
2026-02-05 17:50:55 -06:00
// Step 6: Generating Trees
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-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Step 7: Darkening Water Areas
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas ( noiseImg , allWaterPixels )
2026-02-04 14:19:18 -06:00
2026-02-09 09:41:35 -06:00
// Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
2026-02-04 16:13:06 -06:00
2026-02-09 09:41:35 -06:00
// Step 9: Flattening Road Areas
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , roadPixels )
// Step 10: Applying Roughness
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
2026-02-04 14:19:18 -06:00
2026-02-09 10:13:36 -06:00
// Step 11: Generating Bump Map
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
2026-01-27 15:16:23 -06:00
heightmapImg . Image = compositeImg
2026-02-09 10:13:36 -06:00
bumpmapImg . Image = bumpMap
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-02-12 14:33:47 -06:00
var generateBtn * widget . Button
errorStates := make ( map [ string ] bool )
// ... (other code)
// Main generation button and logic
generateBtn = widget . NewButton ( "Generate" , func () {
// ... (generation logic)
})
// Function to update the generate button state
updateGenerateBtnState := func () {
for _ , hasError := range errorStates {
if hasError {
generateBtn . Disable ()
return
}
}
generateBtn . Enable ()
}
detailSlider := newNumericInputSlider ( 1 , 16 , settings . Detail , "%.0f" , "Detail" )
detailSlider . entry . OnChanged = func ( s string ) {
detailSlider . validate ( s , func ( hasError bool ) {
errorStates [ "detail" ] = hasError
updateGenerateBtnState ()
})
}
detailSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := detailSlider . value . Get ()
2026-01-26 14:29:05 -06:00
settings . Detail = val
2026-02-12 14:33:47 -06:00
}))
2026-01-26 14:29:05 -06:00
2026-02-12 14:33:47 -06:00
roughnessSlider := newNumericInputSlider ( 0 , 100 , settings . Roughness , "%.0f%%" , "Roughness" )
roughnessSlider . entry . OnChanged = func ( s string ) {
roughnessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roughness" ] = hasError
updateGenerateBtnState ()
})
}
roughnessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roughnessSlider . value . Get ()
2026-01-26 14:29:05 -06:00
settings . Roughness = val
2026-02-12 14:33:47 -06:00
}))
2026-02-05 17:50:55 -06:00
// Create UI elements for controlling lake generation settings
2026-02-12 14:33:47 -06:00
lakesSlider := newNumericInputSlider ( 0 , 15 , float64 ( settings . Lakes ), "%.0f" , "Lakes" )
lakesSlider . entry . OnChanged = func ( s string ) {
lakesSlider . validate ( s , func ( hasError bool ) {
errorStates [ "lakes" ] = hasError
updateGenerateBtnState ()
})
}
lakesSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakesSlider . value . Get ()
2026-01-26 14:29:05 -06:00
settings . Lakes = int ( val )
2026-02-12 14:33:47 -06:00
}))
lakeSizeLowerSlider := newNumericInputSlider ( 1 , 100 , settings . LakeSizeLower , "%.0f%%" , "Min Lake Size" )
lakeSizeLowerSlider . entry . OnChanged = func ( s string ) {
lakeSizeLowerSlider . validate ( s , func ( hasError bool ) {
errorStates [ "lakeSizeLower" ] = hasError
updateGenerateBtnState ()
})
2026-01-26 14:29:05 -06:00
}
2026-02-12 14:33:47 -06:00
lakeSizeLowerSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakeSizeLowerSlider . value . Get ()
2026-01-27 12:00:19 -06:00
settings . LakeSizeLower = val
2026-02-12 14:33:47 -06:00
}))
2026-01-27 12:00:19 -06:00
2026-02-12 14:33:47 -06:00
lakeSizeUpperSlider := newNumericInputSlider ( 1 , 100 , settings . LakeSizeUpper , "%.0f%%" , "Max Lake Size" )
lakeSizeUpperSlider . entry . OnChanged = func ( s string ) {
lakeSizeUpperSlider . validate ( s , func ( hasError bool ) {
errorStates [ "lakeSizeUpper" ] = hasError
updateGenerateBtnState ()
})
}
lakeSizeUpperSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakeSizeUpperSlider . value . Get ()
2026-01-27 12:00:19 -06:00
settings . LakeSizeUpper = val
2026-02-12 14:33:47 -06:00
}))
2026-02-18 15:14:47 -06:00
lakeEdgeRoughnessSlider := newNumericInputSlider ( 0 , 100 , settings . LakeEdgeRoughness , "%.0f%%" , "Lake Edge Roughness" )
lakeEdgeRoughnessSlider . entry . OnChanged = func ( s string ) {
lakeEdgeRoughnessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "lakeEdgeRoughness" ] = hasError
updateGenerateBtnState ()
})
}
lakeEdgeRoughnessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakeEdgeRoughnessSlider . value . Get ()
settings . LakeEdgeRoughness = val
}))
2026-02-19 10:29:17 -06:00
lakeShapeLabel := widget . NewLabel ( "Lake Shape:" )
lakeShapeSelect := widget . NewSelect ([] string { "circle" , "oval" , "procedural" }, func ( s string ) {
settings . LakeShape = s
})
lakeShapeSelect . SetSelected ( settings . LakeShape )
2026-02-12 14:33:47 -06:00
riversSlider := newNumericInputSlider ( 0 , 5 , float64 ( settings . Rivers ), "%.0f" , "Rivers" )
riversSlider . entry . OnChanged = func ( s string ) {
riversSlider . validate ( s , func ( hasError bool ) {
errorStates [ "rivers" ] = hasError
updateGenerateBtnState ()
})
2026-01-27 12:00:19 -06:00
}
2026-02-12 14:33:47 -06:00
riversSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riversSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . Rivers = int ( val )
2026-02-12 14:33:47 -06:00
}))
minRiverWidthSlider := newNumericInputSlider ( 1 , 100 , settings . MinRiverWidth , "%.0f%%" , "Min River Width" )
minRiverWidthSlider . entry . OnChanged = func ( s string ) {
minRiverWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minRiverWidth" ] = hasError
updateGenerateBtnState ()
})
2026-01-30 10:38:00 -06:00
}
2026-02-12 14:33:47 -06:00
minRiverWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRiverWidthSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . MinRiverWidth = val
2026-02-12 14:33:47 -06:00
}))
2026-01-30 10:38:00 -06:00
2026-02-12 14:33:47 -06:00
maxRiverWidthSlider := newNumericInputSlider ( 1 , 100 , settings . MaxRiverWidth , "%.0f%%" , "Max River Width" )
maxRiverWidthSlider . entry . OnChanged = func ( s string ) {
maxRiverWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxRiverWidth" ] = hasError
updateGenerateBtnState ()
})
}
maxRiverWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxRiverWidthSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . MaxRiverWidth = val
2026-02-12 14:33:47 -06:00
}))
2026-01-30 10:38:00 -06:00
2026-02-12 14:33:47 -06:00
riverCurvynessSlider := newNumericInputSlider ( 0 , 100 , settings . RiverCurvyness , "%.0f%%" , "River Curvyness" )
riverCurvynessSlider . entry . OnChanged = func ( s string ) {
riverCurvynessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "riverCurvyness" ] = hasError
updateGenerateBtnState ()
})
}
riverCurvynessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riverCurvynessSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . RiverCurvyness = val
2026-02-12 14:33:47 -06:00
}))
2026-02-18 14:33:09 -06:00
riverWidthVariabilitySlider := newNumericInputSlider ( 0 , 100 , settings . RiverWidthVariability , "%.0f%%" , "River Width Variability" )
riverWidthVariabilitySlider . entry . OnChanged = func ( s string ) {
riverWidthVariabilitySlider . validate ( s , func ( hasError bool ) {
errorStates [ "riverWidthVariability" ] = hasError
updateGenerateBtnState ()
})
}
riverWidthVariabilitySlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riverWidthVariabilitySlider . value . Get ()
settings . RiverWidthVariability = val
}))
riverEdgeRoughnessSlider := newNumericInputSlider ( 0 , 100 , settings . RiverEdgeRoughness , "%.0f%%" , "River Edge Roughness" )
riverEdgeRoughnessSlider . entry . OnChanged = func ( s string ) {
riverEdgeRoughnessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "riverEdgeRoughness" ] = hasError
updateGenerateBtnState ()
})
}
riverEdgeRoughnessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riverEdgeRoughnessSlider . value . Get ()
settings . RiverEdgeRoughness = val
}))
2026-02-12 14:33:47 -06:00
minTreeSizeSlider := newNumericInputSlider ( 1 , 150 , settings . MinTreeSize , "%.0fpx" , "Min Tree Size" )
minTreeSizeSlider . entry . OnChanged = func ( s string ) {
minTreeSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minTreeSize" ] = hasError
updateGenerateBtnState ()
})
2026-01-30 10:38:00 -06:00
}
2026-02-12 14:33:47 -06:00
minTreeSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minTreeSizeSlider . value . Get ()
2026-01-27 15:16:23 -06:00
settings . MinTreeSize = val
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-12 14:33:47 -06:00
maxTreeSizeSlider := newNumericInputSlider ( 1 , 150 , settings . MaxTreeSize , "%.0fpx" , "Max Tree Size" )
maxTreeSizeSlider . entry . OnChanged = func ( s string ) {
maxTreeSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxTreeSize" ] = hasError
updateGenerateBtnState ()
})
}
maxTreeSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxTreeSizeSlider . value . Get ()
2026-01-27 15:16:23 -06:00
settings . MaxTreeSize = val
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-12 14:33:47 -06:00
treeCoverageSlider := newNumericInputSlider ( 1 , 100 , settings . TreeCoverage , "%.0f%%" , "Tree Coverage" )
treeCoverageSlider . entry . OnChanged = func ( s string ) {
treeCoverageSlider . validate ( s , func ( hasError bool ) {
errorStates [ "treeCoverage" ] = hasError
updateGenerateBtnState ()
})
}
treeCoverageSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := treeCoverageSlider . value . Get ()
2026-01-27 15:16:23 -06:00
settings . TreeCoverage = val
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-12 14:33:47 -06:00
treeClumpinessSlider := newNumericInputSlider ( 0 , 100 , settings . TreeClumpiness , "%.0f%%" , "Tree Clumpiness" )
treeClumpinessSlider . entry . OnChanged = func ( s string ) {
treeClumpinessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "treeClumpiness" ] = hasError
updateGenerateBtnState ()
})
}
treeClumpinessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := treeClumpinessSlider . value . Get ()
2026-01-27 15:16:23 -06:00
settings . TreeClumpiness = val
2026-02-12 14:33:47 -06:00
}))
numRoadsSlider := newNumericInputSlider ( 0 , 2000 , float64 ( settings . NumRoads ), "%.0f" , "Number of Roads" )
numRoadsSlider . entry . OnChanged = func ( s string ) {
numRoadsSlider . validate ( s , func ( hasError bool ) {
errorStates [ "numRoads" ] = hasError
updateGenerateBtnState ()
})
2026-01-27 15:16:23 -06:00
}
2026-02-12 14:33:47 -06:00
numRoadsSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := numRoadsSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . NumRoads = int ( val )
2026-02-12 14:33:47 -06:00
}))
minRoadWidthSlider := newNumericInputSlider ( 1 , 150 , settings . MinRoadWidth , "%.0fpx" , "Min Road Width" )
minRoadWidthSlider . entry . OnChanged = func ( s string ) {
minRoadWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minRoadWidth" ] = hasError
updateGenerateBtnState ()
})
2026-02-04 14:19:18 -06:00
}
2026-02-12 14:33:47 -06:00
minRoadWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRoadWidthSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . MinRoadWidth = val
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
maxRoadWidthSlider := newNumericInputSlider ( 1 , 150 , settings . MaxRoadWidth , "%.0fpx" , "Max Road Width" )
maxRoadWidthSlider . entry . OnChanged = func ( s string ) {
maxRoadWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxRoadWidth" ] = hasError
updateGenerateBtnState ()
})
}
maxRoadWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxRoadWidthSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . MaxRoadWidth = val
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
roadExitsSlider := newNumericInputSlider ( 0 , 100 , float64 ( settings . RoadExits ), "%.0f" , "Road Exits" )
roadExitsSlider . entry . OnChanged = func ( s string ) {
roadExitsSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roadExits" ] = hasError
updateGenerateBtnState ()
})
}
roadExitsSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roadExitsSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . RoadExits = int ( val )
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
roadCurvynessSlider := newNumericInputSlider ( 0 , 100 , settings . RoadCurvyness , "%.0f%%" , "Road Curvyness" )
roadCurvynessSlider . entry . OnChanged = func ( s string ) {
roadCurvynessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roadCurvyness" ] = hasError
updateGenerateBtnState ()
})
}
roadCurvynessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roadCurvynessSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . RoadCurvyness = val
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
roadDistributionSlider := newNumericInputSlider ( 0 , 100 , settings . RoadDistribution , "%.0f%%" , "Distribution" )
roadDistributionSlider . entry . OnChanged = func ( s string ) {
roadDistributionSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roadDistribution" ] = hasError
updateGenerateBtnState ()
})
2026-02-04 14:19:18 -06:00
}
2026-02-12 14:33:47 -06:00
roadDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roadDistributionSlider . value . Get ()
settings . RoadDistribution = val
}))
2026-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Create UI elements for error display and action buttons
2026-02-12 14:33:47 -06:00
errorLabel := widget . NewLabel ( "" )
errorLabel . Wrapping = fyne . TextWrapWord
2026-01-27 15:16:23 -06:00
errorLabel . Hide ()
2026-02-13 09:29:06 -06:00
progressLabel := widget . NewLabel ( "" )
2026-02-12 14:33:47 -06:00
progressBar := widget . NewProgressBar ()
2026-02-13 09:29:06 -06:00
progressContainer := container . NewVBox ( progressLabel , progressBar )
progressContainer . Hide ()
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-09 10:13:36 -06:00
exportBumpmapBtn := widget . NewButton ( "Export Bump Map" , func () {
showSaveDialog ( w , bumpmapImg . Image , settings )
})
2026-02-02 13:26:28 -06:00
exportMasksBtn := widget . NewButton ( "Export Masks" , func () {
2026-02-09 10:19:05 -06:00
showMasksSaveDialog ( w , canvasImg . Image , heightmapImg . Image , bumpmapImg . Image , settings , lakes , riverPixels , treePixels , roadPixels , bridgePixels , buildingPixels , buildings )
2026-02-02 13:26:28 -06:00
})
2026-02-05 17:50:55 -06:00
// Main generation button and logic
2026-01-28 12:35:10 -06:00
generateBtn = widget . NewButton ( "Generate" , func () {
go func () {
2026-02-13 09:29:06 -06:00
// Disable button and show progress bar during generation
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
generateBtn . Disable ()
2026-02-13 09:29:06 -06:00
progressContainer . Show ()
2026-02-02 10:54:09 -06:00
})
defer func () {
2026-02-13 09:29:06 -06:00
// Re-enable button and hide progress bar after generation
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
generateBtn . Enable ()
2026-02-13 09:29:06 -06:00
progressContainer . Hide ()
2026-02-02 10:54:09 -06:00
})
}()
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider ( settings . Seed )
2026-01-28 12:35:10 -06:00
// Step 1: Generating Heightmap
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 1 of 11: Generating Heightmap" )
progressBar . SetValue ( 1.0 / 11.0 )
})
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
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 2 of 11: Generating Lakes" )
progressBar . SetValue ( 2.0 / 11.0 )
})
2026-02-02 13:26:28 -06:00
var lakeImage image . Image
2026-02-19 10:29:17 -06:00
lakeImage , lakes = GenerateLakes ( settings . Width , settings . Height , settings . Lakes , settings . LakeSizeLower , settings . LakeSizeUpper , seedProvider . Next (), settings . LakeEdgeRoughness , settings . LakeShape )
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
// Step 3: Generating Rivers
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 3 of 11: Generating Rivers" )
progressBar . SetValue ( 3.0 / 11.0 )
})
2026-02-02 13:26:28 -06:00
var riverImage image . Image
2026-02-18 14:33:09 -06:00
riverImage , riverPixels = GenerateRivers ( settings . Width , settings . Height , settings . Rivers , settings . MinRiverWidth , settings . MaxRiverWidth , settings . RiverCurvyness , lakeImage , lakes , seedProvider . Next (), noiseImg , settings . RiverWidthVariability , settings . RiverEdgeRoughness )
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
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 4 of 11: Generating Roads" )
progressBar . SetValue ( 4.0 / 11.0 )
})
2026-02-04 14:19:18 -06:00
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
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 5 of 11: Generating Buildings" )
progressBar . SetValue ( 5.0 / 11.0 )
})
2026-02-09 09:41:35 -06:00
buildings , buildingPixels = GenerateBuildings ( finalImage , settings . Width , settings . Height , settings , roadPixels , allWaterPixels , seedProvider . Next ())
2026-02-05 15:11:55 -06:00
// Step 6: Darkening Water Areas
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 6 of 11: Darkening Water Areas" )
progressBar . SetValue ( 6.0 / 11.0 )
})
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas ( noiseImg , allWaterPixels )
2026-02-09 09:41:35 -06:00
// Step 7: Flattening Building Areas
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 7 of 11: Flattening Building Areas" )
progressBar . SetValue ( 7.0 / 11.0 )
})
2026-02-09 09:41:35 -06:00
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 8 of 11: Flattening Road Areas" )
progressBar . SetValue ( 8.0 / 11.0 )
})
2026-02-09 09:41:35 -06:00
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , roadPixels )
// Step 8: Applying Roughness
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 9 of 11: Applying Roughness" )
progressBar . SetValue ( 9.0 / 11.0 )
})
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
2026-01-28 12:35:10 -06:00
2026-02-09 09:41:35 -06:00
// Step 9: Generating Trees
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 10 of 11: Generating Trees" )
progressBar . SetValue ( 10.0 / 11.0 )
})
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-09 10:13:36 -06:00
// Step 10: Generating Bump Map
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
progressLabel . SetText ( "Step 11 of 11: Generating Bump Map" )
progressBar . SetValue ( 1.0 )
})
2026-02-09 10:13:36 -06:00
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
// Step 11: Finalizing Images
2026-02-12 14:33:47 -06:00
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
heightmapImg . Image = compositeImg
heightmapImg . Refresh ()
2026-02-09 10:13:36 -06:00
bumpmapImg . Image = bumpMap
bumpmapImg . Refresh ()
2026-02-02 10:54:09 -06:00
canvasImg . Image = finalImage
canvasImg . Refresh ()
})
2026-01-28 12:35:10 -06:00
}()
2026-01-26 14:29:05 -06:00
})
2026-02-05 17:50:55 -06:00
// Create UI elements for image dimensions and seed
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-02-05 17:50:55 -06:00
// Create tabs for organizing settings
2026-01-26 14:29:05 -06:00
terrainTab := container . NewTabItem ( "Terrain" , container . NewVBox (
detailSlider ,
roughnessSlider ,
2026-02-02 10:15:26 -06:00
widget . NewLabel ( "" ), // Spacer
minTreeSizeSlider ,
maxTreeSizeSlider ,
treeCoverageSlider ,
treeClumpinessSlider ,
))
waterTab := container . NewTabItem ( "Water" , container . NewVBox (
2026-01-26 14:29:05 -06:00
lakesSlider ,
2026-01-27 12:00:19 -06:00
lakeSizeLowerSlider ,
lakeSizeUpperSlider ,
2026-02-18 15:14:47 -06:00
lakeEdgeRoughnessSlider ,
2026-02-19 10:29:17 -06:00
lakeShapeLabel ,
lakeShapeSelect ,
2026-01-30 10:38:00 -06:00
widget . NewLabel ( "" ), // Spacer
riversSlider ,
minRiverWidthSlider ,
maxRiverWidthSlider ,
riverCurvynessSlider ,
2026-02-18 14:33:09 -06:00
riverWidthVariabilitySlider ,
riverEdgeRoughnessSlider ,
2026-01-26 14:29:05 -06:00
))
2026-02-04 14:19:18 -06:00
roadsTab := container . NewTabItem ( "Roads" , container . NewVBox (
numRoadsSlider ,
minRoadWidthSlider ,
maxRoadWidthSlider ,
roadExitsSlider ,
roadCurvynessSlider ,
roadDistributionSlider ,
))
2026-02-12 14:33:47 -06:00
numBuildingsSlider := newNumericInputSlider ( 0 , 10000 , float64 ( settings . NumBuildings ), "%.0f" , "Number of Buildings" )
numBuildingsSlider . entry . OnChanged = func ( s string ) {
numBuildingsSlider . validate ( s , func ( hasError bool ) {
errorStates [ "numBuildings" ] = hasError
updateGenerateBtnState ()
})
}
numBuildingsSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := numBuildingsSlider . value . Get ()
2026-02-05 15:11:55 -06:00
settings . NumBuildings = int ( val )
2026-02-12 14:33:47 -06:00
}))
minBuildingSizeSlider := newNumericInputSlider ( 1 , 150 , settings . MinBuildingSize , "%.0fpx" , "Min Building Size" )
minBuildingSizeSlider . entry . OnChanged = func ( s string ) {
minBuildingSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minBuildingSize" ] = hasError
updateGenerateBtnState ()
})
2026-02-05 15:11:55 -06:00
}
2026-02-12 14:33:47 -06:00
minBuildingSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minBuildingSizeSlider . value . Get ()
2026-02-05 15:11:55 -06:00
settings . MinBuildingSize = val
2026-02-12 14:33:47 -06:00
}))
2026-02-05 15:11:55 -06:00
2026-02-12 14:33:47 -06:00
maxBuildingSizeSlider := newNumericInputSlider ( 1 , 150 , settings . MaxBuildingSize , "%.0fpx" , "Max Building Size" )
maxBuildingSizeSlider . entry . OnChanged = func ( s string ) {
maxBuildingSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxBuildingSize" ] = hasError
updateGenerateBtnState ()
})
}
maxBuildingSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxBuildingSizeSlider . value . Get ()
2026-02-05 15:11:55 -06:00
settings . MaxBuildingSize = val
2026-02-12 14:33:47 -06:00
}))
2026-02-05 15:11:55 -06:00
2026-02-12 14:33:47 -06:00
buildingDistributionSlider := newNumericInputSlider ( 0 , 100 , settings . BuildingDistribution , "%.0f%%" , "Building Distribution" )
buildingDistributionSlider . entry . OnChanged = func ( s string ) {
buildingDistributionSlider . validate ( s , func ( hasError bool ) {
errorStates [ "buildingDistribution" ] = hasError
updateGenerateBtnState ()
})
2026-02-05 15:11:55 -06:00
}
2026-02-12 14:33:47 -06:00
buildingDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingDistributionSlider . value . Get ()
settings . BuildingDistribution = val
}))
2026-02-05 15:11:55 -06:00
buildingShapeLabel := widget . NewLabel ( "Building Shape:" )
2026-02-06 10:21:40 -06:00
buildingShapeSelect := widget . NewSelect ([] string { "squares" , "circles" , "rectangles" , "mixed" , "procedural" }, func ( s string ) {
2026-02-05 15:11:55 -06:00
settings . BuildingShape = s
})
buildingShapeSelect . SetSelected ( settings . BuildingShape )
2026-02-06 09:01:09 -06:00
// Create sliders for building shape ratios (visible only when "mixed" is selected)
squareRatioSlider := widget . NewSlider ( 0 , 100 )
circleRatioSlider := widget . NewSlider ( 0 , 100 )
rectangleRatioSlider := widget . NewSlider ( 0 , 100 )
squareRatioLabel := widget . NewLabel ( fmt . Sprintf ( "Squares: %.0f%%" , settings . BuildingShapeRatios [ "squares" ]))
circleRatioLabel := widget . NewLabel ( fmt . Sprintf ( "Circles: %.0f%%" , settings . BuildingShapeRatios [ "circles" ]))
rectangleRatioLabel := widget . NewLabel ( fmt . Sprintf ( "Rectangles: %.0f%%" , settings . BuildingShapeRatios [ "rectangles" ]))
squareRatioSlider . SetValue ( settings . BuildingShapeRatios [ "squares" ])
circleRatioSlider . SetValue ( settings . BuildingShapeRatios [ "circles" ])
rectangleRatioSlider . SetValue ( settings . BuildingShapeRatios [ "rectangles" ])
ratioContainer := container . NewVBox (
squareRatioLabel ,
squareRatioSlider ,
circleRatioLabel ,
circleRatioSlider ,
rectangleRatioLabel ,
rectangleRatioSlider ,
)
2026-02-12 14:33:47 -06:00
minBuildingComplexitySlider := newNumericInputSlider ( 1 , 6 , float64 ( settings . MinBuildingComplexity ), "%.0f" , "Min Building Complexity" )
minBuildingComplexitySlider . entry . OnChanged = func ( s string ) {
minBuildingComplexitySlider . validate ( s , func ( hasError bool ) {
errorStates [ "minBuildingComplexity" ] = hasError
updateGenerateBtnState ()
})
}
minBuildingComplexitySlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minBuildingComplexitySlider . value . Get ()
2026-02-06 10:21:40 -06:00
settings . MinBuildingComplexity = int ( val )
2026-02-12 14:33:47 -06:00
}))
2026-02-06 10:21:40 -06:00
2026-02-12 14:33:47 -06:00
maxBuildingComplexitySlider := newNumericInputSlider ( 1 , 6 , float64 ( settings . MaxBuildingComplexity ), "%.0f" , "Max Building Complexity" )
maxBuildingComplexitySlider . entry . OnChanged = func ( s string ) {
maxBuildingComplexitySlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxBuildingComplexity" ] = hasError
updateGenerateBtnState ()
})
}
maxBuildingComplexitySlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxBuildingComplexitySlider . value . Get ()
2026-02-06 10:21:40 -06:00
settings . MaxBuildingComplexity = int ( val )
2026-02-12 14:33:47 -06:00
}))
2026-02-06 10:21:40 -06:00
2026-02-12 14:33:47 -06:00
buildingComplexityRatioSlider := newNumericInputSlider ( 0 , 100 , settings . BuildingComplexityRatio , "%.0f%%" , "Building Complexity Ratio" )
buildingComplexityRatioSlider . entry . OnChanged = func ( s string ) {
buildingComplexityRatioSlider . validate ( s , func ( hasError bool ) {
errorStates [ "buildingComplexityRatio" ] = hasError
updateGenerateBtnState ()
})
2026-02-06 10:21:40 -06:00
}
2026-02-12 14:33:47 -06:00
buildingComplexityRatioSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingComplexityRatioSlider . value . Get ()
settings . BuildingComplexityRatio = val
}))
2026-02-06 10:21:40 -06:00
proceduralContainer := container . NewVBox (
minBuildingComplexitySlider ,
maxBuildingComplexitySlider ,
buildingComplexityRatioSlider ,
)
2026-02-06 09:01:09 -06:00
updateRatioSliders := func () {
squareRatioSlider . SetValue ( settings . BuildingShapeRatios [ "squares" ])
circleRatioSlider . SetValue ( settings . BuildingShapeRatios [ "circles" ])
rectangleRatioSlider . SetValue ( settings . BuildingShapeRatios [ "rectangles" ])
squareRatioLabel . SetText ( fmt . Sprintf ( "Squares: %.0f%%" , settings . BuildingShapeRatios [ "squares" ]))
circleRatioLabel . SetText ( fmt . Sprintf ( "Circles: %.0f%%" , settings . BuildingShapeRatios [ "circles" ]))
rectangleRatioLabel . SetText ( fmt . Sprintf ( "Rectangles: %.0f%%" , settings . BuildingShapeRatios [ "rectangles" ]))
}
squareRatioSlider . OnChanged = func ( val float64 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
return
}
oldVal := settings . BuildingShapeRatios [ "squares" ]
diff := val - oldVal
settings . BuildingShapeRatios [ "squares" ] = val
settings . BuildingShapeRatios [ "circles" ] -= diff / 2
settings . BuildingShapeRatios [ "rectangles" ] -= diff / 2
// Clamp values to 0-100 range
if settings . BuildingShapeRatios [ "circles" ] < 0 {
settings . BuildingShapeRatios [ "rectangles" ] += settings . BuildingShapeRatios [ "circles" ]
settings . BuildingShapeRatios [ "circles" ] = 0
}
if settings . BuildingShapeRatios [ "rectangles" ] < 0 {
settings . BuildingShapeRatios [ "circles" ] += settings . BuildingShapeRatios [ "rectangles" ]
settings . BuildingShapeRatios [ "rectangles" ] = 0
}
if settings . BuildingShapeRatios [ "circles" ] > 100 {
settings . BuildingShapeRatios [ "rectangles" ] += settings . BuildingShapeRatios [ "circles" ] - 100
settings . BuildingShapeRatios [ "circles" ] = 100
}
if settings . BuildingShapeRatios [ "rectangles" ] > 100 {
settings . BuildingShapeRatios [ "circles" ] += settings . BuildingShapeRatios [ "rectangles" ] - 100
settings . BuildingShapeRatios [ "rectangles" ] = 100
}
updateRatioSliders ()
}
circleRatioSlider . OnChanged = func ( val float64 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
return
}
oldVal := settings . BuildingShapeRatios [ "circles" ]
diff := val - oldVal
settings . BuildingShapeRatios [ "circles" ] = val
settings . BuildingShapeRatios [ "squares" ] -= diff / 2
settings . BuildingShapeRatios [ "rectangles" ] -= diff / 2
// Clamp values to 0-100 range
if settings . BuildingShapeRatios [ "squares" ] < 0 {
settings . BuildingShapeRatios [ "rectangles" ] += settings . BuildingShapeRatios [ "squares" ]
settings . BuildingShapeRatios [ "squares" ] = 0
}
if settings . BuildingShapeRatios [ "rectangles" ] < 0 {
settings . BuildingShapeRatios [ "squares" ] += settings . BuildingShapeRatios [ "rectangles" ]
settings . BuildingShapeRatios [ "rectangles" ] = 0
}
if settings . BuildingShapeRatios [ "squares" ] > 100 {
settings . BuildingShapeRatios [ "rectangles" ] += settings . BuildingShapeRatios [ "squares" ] - 100
settings . BuildingShapeRatios [ "squares" ] = 100
}
if settings . BuildingShapeRatios [ "rectangles" ] > 100 {
settings . BuildingShapeRatios [ "squares" ] += settings . BuildingShapeRatios [ "rectangles" ] - 100
settings . BuildingShapeRatios [ "rectangles" ] = 100
}
updateRatioSliders ()
}
rectangleRatioSlider . OnChanged = func ( val float64 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
return
}
oldVal := settings . BuildingShapeRatios [ "rectangles" ]
diff := val - oldVal
settings . BuildingShapeRatios [ "rectangles" ] = val
settings . BuildingShapeRatios [ "squares" ] -= diff / 2
settings . BuildingShapeRatios [ "circles" ] -= diff / 2
// Clamp values to 0-100 range
if settings . BuildingShapeRatios [ "squares" ] < 0 {
settings . BuildingShapeRatios [ "circles" ] += settings . BuildingShapeRatios [ "squares" ]
settings . BuildingShapeRatios [ "squares" ] = 0
}
if settings . BuildingShapeRatios [ "circles" ] < 0 {
settings . BuildingShapeRatios [ "squares" ] += settings . BuildingShapeRatios [ "circles" ]
settings . BuildingShapeRatios [ "circles" ] = 0
}
if settings . BuildingShapeRatios [ "squares" ] > 100 {
settings . BuildingShapeRatios [ "circles" ] += settings . BuildingShapeRatios [ "squares" ] - 100
settings . BuildingShapeRatios [ "squares" ] = 100
}
if settings . BuildingShapeRatios [ "circles" ] > 100 {
settings . BuildingShapeRatios [ "squares" ] += settings . BuildingShapeRatios [ "circles" ] - 100
settings . BuildingShapeRatios [ "circles" ] = 100
}
updateRatioSliders ()
}
buildingShapeSelect . OnChanged = func ( s string ) {
settings . BuildingShape = s
if s == "mixed" {
2026-02-06 10:21:40 -06:00
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if s == "procedural" {
proceduralContainer . Show ()
2026-02-06 09:01:09 -06:00
ratioContainer . Show ()
} else {
2026-02-06 10:21:40 -06:00
proceduralContainer . Hide ()
2026-02-06 09:01:09 -06:00
ratioContainer . Hide ()
}
}
// Initial visibility check
2026-02-06 10:21:40 -06:00
if settings . BuildingShape == "mixed" {
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if settings . BuildingShape == "procedural" {
proceduralContainer . Show ()
ratioContainer . Show ()
} else {
proceduralContainer . Hide ()
2026-02-06 09:01:09 -06:00
ratioContainer . Hide ()
}
2026-02-05 15:11:55 -06:00
buildingsTab := container . NewTabItem ( "Buildings" , container . NewVBox (
numBuildingsSlider ,
minBuildingSizeSlider ,
maxBuildingSizeSlider ,
buildingDistributionSlider ,
buildingShapeLabel ,
buildingShapeSelect ,
2026-02-06 10:21:40 -06:00
proceduralContainer ,
2026-02-06 09:01:09 -06:00
ratioContainer ,
2026-02-05 15:11:55 -06:00
))
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-02-13 09:29:06 -06:00
progressContainer ,
2026-02-02 12:24:14 -06:00
widget . NewSeparator (),
2026-02-02 12:40:25 -06:00
exportCanvasBtn ,
exportHeightmapBtn ,
2026-02-09 10:13:36 -06:00
exportBumpmapBtn ,
2026-02-02 13:26:28 -06:00
exportMasksBtn ,
2026-01-26 14:29:05 -06:00
))
2026-02-05 17:50:55 -06:00
// Create the main layout using a horizontal split
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 ,
)
2026-02-09 10:35:28 -06:00
right := container . NewMax ()
var tappableCanvas , tappableHeightmap , tappableBumpmap * tappableImage
updateRightPanel := func () {
var top , bottom fyne . CanvasObject
switch settings . ImageViewState {
case 1 :
top = tappableCanvas
bottom = container . NewGridWithColumns ( 2 , tappableHeightmap , tappableBumpmap )
case 2 :
top = tappableHeightmap
bottom = container . NewGridWithColumns ( 2 , tappableCanvas , tappableBumpmap )
case 3 :
top = tappableBumpmap
bottom = container . NewGridWithColumns ( 2 , tappableCanvas , tappableHeightmap )
default :
right . Objects = [] fyne . CanvasObject { container . NewGridWithRows ( 3 , tappableCanvas , tappableHeightmap , tappableBumpmap )}
right . Refresh ()
return
}
split := container . NewVSplit ( top , bottom )
split . Offset = 0.8
right . Objects = [] fyne . CanvasObject { split }
right . Refresh ()
}
tappableCanvas = newTappableImage ( container . NewMax ( canvasImg ), func () {
if settings . ImageViewState == 1 {
settings . ImageViewState = 0
} else {
settings . ImageViewState = 1
}
updateRightPanel ()
})
tappableHeightmap = newTappableImage ( container . NewMax ( heightmapImg ), func () {
if settings . ImageViewState == 2 {
settings . ImageViewState = 0
} else {
settings . ImageViewState = 2
}
updateRightPanel ()
})
tappableBumpmap = newTappableImage ( container . NewMax ( bumpmapImg ), func () {
if settings . ImageViewState == 3 {
settings . ImageViewState = 0
} else {
settings . ImageViewState = 3
}
updateRightPanel ()
})
updateRightPanel ()
2026-01-26 14:29:05 -06:00
split := container . NewHSplit (
left ,
right ,
)
split . SetOffset ( 0.3 )
2026-02-05 17:50:55 -06:00
// Set the window content and start the application
2026-01-26 14:29:05 -06:00
w . SetContent ( split )
w . ShowAndRun ()
}
2026-02-05 17:50:55 -06:00
// getImageData encodes an image to the specified format and returns the data as a byte buffer.
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 17:50:55 -06:00
// showMasksSaveDialog displays a dialog for saving the generated masks.
2026-02-09 10:19:05 -06:00
func showMasksSaveDialog ( win fyne . Window , canvasImg , heightmapImg , bumpmapImg image . Image , settings * Settings , lakes [][] image . Point , riverPixels , treePixels , roadPixels , bridgePixels , buildingPixels [] image . Point , buildings [][] image . Point ) {
2026-02-05 17:50:55 -06:00
// Create UI elements for the save dialog
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 ()
2026-02-05 17:50:55 -06:00
// Create mask images from the generated data
2026-02-02 13:26:28 -06:00
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 )
2026-02-09 09:41:35 -06:00
for _ , building := range buildings {
for _ , p := range building {
buildingMask . SetGray ( p . X , p . Y , color . Gray { Y : 255 })
}
2026-02-05 15:11:55 -06:00
}
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 ,
2026-02-09 10:19:05 -06:00
"bump_map." + imgFormat : bumpmapImg ,
2026-02-05 15:11:55 -06:00
"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
}
2026-02-05 17:50:55 -06:00
// Save the images based on the selected packaging option
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 ()
}
2026-02-05 17:50:55 -06:00
// saveImage saves an image to the specified path.
2026-02-02 13:26:28 -06:00
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
2026-02-05 17:50:55 -06:00
// showSaveDialog displays a dialog for saving an image.
2026-02-02 12:24:14 -06:00
func showSaveDialog ( win fyne . Window , img image . Image , settings * Settings ) {
2026-02-05 17:50:55 -06:00
// Create UI elements for the save dialog
2026-02-02 12:24:14 -06:00
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 ()
}