2026-03-12 12:22:09 -05:00
package main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"log"
2026-03-23 10:03:22 -05:00
"math"
2026-03-12 12:22:09 -05:00
"os"
"path/filepath"
"strconv"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"github.com/chai2010/webp"
)
const generationStepTimeout = time . Minute
func main () {
// Initialize the Fyne application and window
a := app . NewWithID ( "com.example.rpgcitymaker" )
w := a . NewWindow ( "RPG City Maker" )
w . Resize ( fyne . NewSize ( 800 , 600 ))
// Load settings from file, or use defaults if loading fails
settings , err := LoadSettings ()
if err != nil {
log . Println ( "Error loading settings:" , err )
// Use default settings if loading fails
settings = & Settings { Detail : 1 , Roughness : 0 , Width : 300 , Height : 300 , Lakes : 0 , LakeSizeLower : 1 , LakeSizeUpper : 5 }
}
// Create canvas objects for displaying the generated images
canvasImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
heightmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
bumpmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
// Initialize slices to store generated map features
var lakes [][] image . Point
var buildings [][] image . Point
var waterMask * PixelMask
var riverMask * PixelMask
var treeMask * PixelMask
var buildingMask * PixelMask
var roadMask * PixelMask
var bridgeMask * PixelMask
var exitRoadMask * PixelMask
var wallMask * PixelMask
var turretMask * PixelMask
// Set up application configuration directory
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" )
bumpmapPath := filepath . Join ( appConfigDir , "bumpmap.png" )
// Save settings and images on window close
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 )
}
}
}
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 )
}
}
}
})
// Load previously saved images
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
}
}
bumpmapFile , err := os . Open ( bumpmapPath )
if err == nil {
defer bumpmapFile . Close ()
img , err := png . Decode ( bumpmapFile )
if err == nil {
bumpmapImg . Image = img
}
}
// Generate initial images if none are loaded
if canvasImg . Image == nil || heightmapImg . Image == nil {
// Step 1: Generating Heightmap
seedProvider := NewSeedProvider ( settings . Seed )
noiseImg := GenerateHeightmap ( settings . Width , settings . Height , int ( settings . Detail ), 100.0 , seedProvider . Next ())
prevBuildings := settings . NumBuildings
cappedBuildings := capRequestedBuildingsToFit ( settings , settings . Width , settings . Height )
if cappedBuildings < prevBuildings {
log . Printf ( "Building count capped from %d to %d based on image size and average building size.\n" , prevBuildings , cappedBuildings )
}
// Step 2: Generating Lakes (timeout: 1 minute)
var lakeImage image . Image = image . NewRGBA ( image . Rect ( 0 , 0 , settings . Width , settings . Height ))
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
img image . Image
lks [][] image . Point
} {
img , lks := GenerateLakes ( settings . Width , settings . Height , settings . Lakes , settings . LakeSizeLower , settings . LakeSizeUpper , seedProvider . Next (), settings . LakeEdgeRoughness , settings . LakeShape )
return struct {
img image . Image
lks [][] image . Point
}{ img : img , lks : lks }
}); ok {
lakeImage = out . img
lakes = out . lks
} else {
log . Println ( "GenerateLakes timed out after 1 minute; continuing." )
lakes = nil
}
// Step 3: Generating Rivers (timeout: 1 minute)
riverBase := cloneToRGBA ( lakeImage , settings . Width , settings . Height )
var finalImage * image . RGBA = riverBase
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
img image . Image
rmsk * PixelMask
} {
img , rmsk := GenerateRivers ( settings . Width , settings . Height , settings . Rivers , settings . MinRiverWidth , settings . MaxRiverWidth , settings . RiverCurvyness , riverBase , lakes , seedProvider . Next (), noiseImg , settings . RiverWidthVariability , settings . RiverEdgeRoughness )
return struct {
img image . Image
rmsk * PixelMask
}{ img : img , rmsk : rmsk }
}); ok {
riverMask = out . rmsk
finalImage = cloneToRGBA ( out . img , settings . Width , settings . Height )
} else {
log . Println ( "GenerateRivers timed out after 1 minute; continuing." )
riverMask = NewPixelMask ( settings . Width , settings . Height )
}
waterMask = BuildMaskFromLakes ( settings . Width , settings . Height , lakes )
if riverMask != nil {
waterMask . Merge ( riverMask )
}
// Step 4: Preparing Road Nodes
var roadNodes [] * PointOfInterest
var roadTarget int
var edgeToEdgeOnly bool
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
pois [] * PointOfInterest
target int
edgeToEdge bool
} {
pois , target , edgeToEdge := PrepareRoadNodes ( settings . Width , settings . Height , settings , waterMask , seedProvider . Next ())
return struct {
pois [] * PointOfInterest
target int
edgeToEdge bool
}{ pois : pois , target : target , edgeToEdge : edgeToEdge }
}); ok {
roadNodes , roadTarget , edgeToEdgeOnly = out . pois , out . target , out . edgeToEdge
} else {
log . Println ( "PrepareRoadNodes timed out after 1 minute; continuing." )
roadNodes = nil
roadTarget = 0
edgeToEdgeOnly = false
}
// Step 5: Generating Fortifications
var wallLayout * FortificationLayout
fortBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
layout * FortificationLayout
} {
layout , _ := GenerateFortifications ( fortBase , settings . Width , settings . Height , settings , waterMask , roadNodes , seedProvider . Next ())
return struct {
layout * FortificationLayout
}{ layout : layout }
}); ok {
wallLayout = out . layout
if wallLayout != nil {
wallMask = wallLayout . Mask
} else {
wallMask = NewPixelMask ( settings . Width , settings . Height )
}
finalImage = fortBase
} else {
log . Println ( "GenerateFortifications timed out after 1 minute; continuing." )
wallLayout = & FortificationLayout { Mask : NewPixelMask ( settings . Width , settings . Height )}
wallMask = wallLayout . Mask
}
// Step 6: Generating Roads
var roadAnchors [] image . Point
var roadList [] * Road
roadBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
rl [] * Road
} {
rd , br , ex , anc , rl := GenerateRoadsWithPOIs ( roadBase , settings . Width , settings . Height , settings , waterMask , wallLayout , roadNodes , roadTarget , edgeToEdgeOnly , seedProvider . Next ())
return struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
rl [] * Road
}{ rd : rd , br : br , ex : ex , anc : anc , rl : rl }
}); ok {
roadMask , bridgeMask , exitRoadMask , roadAnchors , roadList = out . rd , out . br , out . ex , out . anc , out . rl
drawWallMask ( roadBase , wallMask )
turretMask = GenerateTurrets ( roadBase , settings . Width , settings . Height , settings , wallLayout , waterMask , roadMask , roadList )
finalImage = roadBase
} else {
log . Println ( "GenerateRoads timed out after 1 minute; continuing." )
roadMask = NewPixelMask ( settings . Width , settings . Height )
bridgeMask = NewPixelMask ( settings . Width , settings . Height )
exitRoadMask = NewPixelMask ( settings . Width , settings . Height )
turretMask = NewPixelMask ( settings . Width , settings . Height )
roadAnchors = nil
}
placementMask := cloneMask ( roadMask )
if placementMask == nil {
placementMask = NewPixelMask ( settings . Width , settings . Height )
}
if wallMask != nil {
placementMask . Merge ( wallMask )
}
if turretMask != nil {
placementMask . Merge ( turretMask )
}
// Step 7: Generating Buildings
buildingBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
blds [][] image . Point
bmsk * PixelMask
} {
blds , bmsk := GenerateBuildings ( buildingBase , settings . Width , settings . Height , settings , roadAnchors , waterMask , placementMask , exitRoadMask , seedProvider . Next ())
return struct {
blds [][] image . Point
bmsk * PixelMask
}{ blds : blds , bmsk : bmsk }
}); ok {
buildings , buildingMask = out . blds , out . bmsk
finalImage = buildingBase
} else {
log . Println ( "GenerateBuildings timed out after 1 minute; continuing." )
buildings = nil
buildingMask = NewPixelMask ( settings . Width , settings . Height )
}
// Step 8: Generating Trees
treeBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () * PixelMask {
return GenerateTrees ( treeBase , waterMask , placementMask , buildingMask , settings . MinTreeSize , settings . MaxTreeSize , settings . TreeCoverage , settings . TreeClumpiness , seedProvider . Next ())
}); ok {
treeMask = out
2026-03-23 10:03:22 -05:00
if wallMask != nil {
drawWallMask ( treeBase , wallMask )
}
if turretMask != nil {
drawTurretMask ( treeBase , turretMask )
}
2026-03-12 12:22:09 -05:00
finalImage = treeBase
} else {
log . Println ( "GenerateTrees timed out after 1 minute; continuing." )
treeMask = NewPixelMask ( settings . Width , settings . Height )
}
// Step 7: Darkening Water Areas
darkenedHeightmap := DarkenLakeAreas ( noiseImg , waterMask )
// Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
// Step 9: Flattening Road and Wall Areas
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , placementMask )
// Step 10: Applying Roughness
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
// Step 11: Generating Bump Map
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
heightmapImg . Image = compositeImg
bumpmapImg . Image = bumpMap
canvasImg . Image = finalImage
}
var generateBtn * widget . Button
errorStates := make ( map [ string ] bool )
var applyingPreset bool
var clearPresetSelection func ()
markPresetDirty := func () {
if applyingPreset {
return
}
if clearPresetSelection != nil {
clearPresetSelection ()
}
}
// ... (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 ()
settings . Detail = val
markPresetDirty ()
}))
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 ()
settings . Roughness = val
markPresetDirty ()
}))
// Create UI elements for controlling lake generation settings
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 ()
settings . Lakes = int ( val )
markPresetDirty ()
}))
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 ()
})
}
lakeSizeLowerSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakeSizeLowerSlider . value . Get ()
settings . LakeSizeLower = val
markPresetDirty ()
}))
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 ()
settings . LakeSizeUpper = val
markPresetDirty ()
}))
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
markPresetDirty ()
}))
lakeShapeLabel := widget . NewLabel ( "Lake Shape:" )
lakeShapeSelect := widget . NewSelect ([] string { "circle" , "oval" , "procedural" }, func ( s string ) {
settings . LakeShape = s
markPresetDirty ()
})
lakeShapeSelect . SetSelected ( settings . LakeShape )
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 ()
})
}
riversSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riversSlider . value . Get ()
settings . Rivers = int ( val )
markPresetDirty ()
}))
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 ()
})
}
minRiverWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRiverWidthSlider . value . Get ()
settings . MinRiverWidth = val
markPresetDirty ()
}))
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 ()
settings . MaxRiverWidth = val
markPresetDirty ()
}))
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 ()
settings . RiverCurvyness = val
markPresetDirty ()
}))
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
markPresetDirty ()
}))
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
markPresetDirty ()
}))
minTreeSizeSlider := newNumericInputSliderWithStep ( minTreeSizePercent , maxTreeSizePercent , settings . MinTreeSize , treeSizePercentStep , "%.1f%%" , "Min Tree Size" )
minTreeSizeSlider . entry . OnChanged = func ( s string ) {
minTreeSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minTreeSize" ] = hasError
updateGenerateBtnState ()
})
}
minTreeSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minTreeSizeSlider . value . Get ()
settings . MinTreeSize = val
markPresetDirty ()
}))
maxTreeSizeSlider := newNumericInputSliderWithStep ( minTreeSizePercent , maxTreeSizePercent , settings . MaxTreeSize , treeSizePercentStep , "%.1f%%" , "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 ()
settings . MaxTreeSize = val
markPresetDirty ()
}))
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 ()
settings . TreeCoverage = val
markPresetDirty ()
}))
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 ()
settings . TreeClumpiness = val
markPresetDirty ()
}))
minRoadWidthSlider := newNumericInputSliderWithStep ( minRoadWidthPercent , maxRoadWidthPercent , settings . MinRoadWidth , roadWidthPercentStep , "%.1f%%" , "Min Road Width" )
minRoadWidthSlider . entry . OnChanged = func ( s string ) {
minRoadWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minRoadWidth" ] = hasError
updateGenerateBtnState ()
})
}
minRoadWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRoadWidthSlider . value . Get ()
settings . MinRoadWidth = val
markPresetDirty ()
}))
maxRoadWidthSlider := newNumericInputSliderWithStep ( minRoadWidthPercent , maxRoadWidthPercent , settings . MaxRoadWidth , roadWidthPercentStep , "%.1f%%" , "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 ()
settings . MaxRoadWidth = val
markPresetDirty ()
}))
buildingsPerRoadSlider := newNumericInputSlider ( 1 , 20 , float64 ( settings . BuildingsPerRoad ), "%.0f" , "Buildings Per Road" )
buildingsPerRoadSlider . entry . OnChanged = func ( s string ) {
buildingsPerRoadSlider . validate ( s , func ( hasError bool ) {
errorStates [ "buildingsPerRoad" ] = hasError
updateGenerateBtnState ()
})
}
buildingsPerRoadSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingsPerRoadSlider . value . Get ()
settings . BuildingsPerRoad = int ( val )
markPresetDirty ()
}))
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 ()
settings . RoadExits = int ( val )
markPresetDirty ()
}))
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 ()
settings . RoadCurvyness = val
markPresetDirty ()
}))
roadDistributionSlider := newNumericInputSlider ( 0 , 100 , settings . RoadDistribution , "%.0f%%" , "Distribution" )
roadDistributionSlider . entry . OnChanged = func ( s string ) {
roadDistributionSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roadDistribution" ] = hasError
updateGenerateBtnState ()
})
}
roadDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roadDistributionSlider . value . Get ()
settings . RoadDistribution = val
markPresetDirty ()
}))
minRoadAngleSlider := newNumericInputSlider ( 0 , 180 , settings . MinRoadAngle , "%.0f°" , "Minimum Road Angle" )
minRoadAngleSlider . entry . OnChanged = func ( s string ) {
minRoadAngleSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minRoadAngle" ] = hasError
updateGenerateBtnState ()
})
}
minRoadAngleSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRoadAngleSlider . value . Get ()
settings . MinRoadAngle = val
markPresetDirty ()
}))
minWallWidthSlider := newNumericInputSliderWithStep ( minWallWidthPercent , maxWallWidthPercent , settings . MinWallWidth , wallWidthPercentStep , "%.1f%%" , "Min Wall Width" )
minWallWidthSlider . entry . OnChanged = func ( s string ) {
minWallWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minWallWidth" ] = hasError
updateGenerateBtnState ()
})
}
minWallWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minWallWidthSlider . value . Get ()
settings . MinWallWidth = val
markPresetDirty ()
}))
maxWallWidthSlider := newNumericInputSliderWithStep ( minWallWidthPercent , maxWallWidthPercent , settings . MaxWallWidth , wallWidthPercentStep , "%.1f%%" , "Max Wall Width" )
maxWallWidthSlider . entry . OnChanged = func ( s string ) {
maxWallWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "maxWallWidth" ] = hasError
updateGenerateBtnState ()
})
}
maxWallWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := maxWallWidthSlider . value . Get ()
settings . MaxWallWidth = val
markPresetDirty ()
}))
numWallsSlider := newNumericInputSlider ( 0 , 5 , float64 ( settings . NumWalls ), "%.0f" , "Number of Walls" )
numWallsSlider . entry . OnChanged = func ( s string ) {
numWallsSlider . validate ( s , func ( hasError bool ) {
errorStates [ "numWalls" ] = hasError
updateGenerateBtnState ()
})
}
numWallsSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := numWallsSlider . value . Get ()
settings . NumWalls = int ( val )
markPresetDirty ()
}))
cityCoverageSlider := newNumericInputSlider ( 1 , 100 , settings . CityCoverage , "%.0f%%" , "City Coverage" )
cityCoverageSlider . entry . OnChanged = func ( s string ) {
cityCoverageSlider . validate ( s , func ( hasError bool ) {
errorStates [ "cityCoverage" ] = hasError
updateGenerateBtnState ()
})
}
cityCoverageSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := cityCoverageSlider . value . Get ()
settings . CityCoverage = val
markPresetDirty ()
}))
wallCurvynessSlider := newNumericInputSlider ( 0 , 100 , settings . WallCurvyness , "%.0f%%" , "Wall Curvyness" )
wallCurvynessSlider . entry . OnChanged = func ( s string ) {
wallCurvynessSlider . validate ( s , func ( hasError bool ) {
errorStates [ "wallCurvyness" ] = hasError
updateGenerateBtnState ()
})
}
wallCurvynessSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := wallCurvynessSlider . value . Get ()
settings . WallCurvyness = val
markPresetDirty ()
}))
showTurretsCheck := widget . NewCheck ( "Show Turrets" , func ( v bool ) {
settings . ShowTurrets = v
markPresetDirty ()
})
showTurretsCheck . SetChecked ( settings . ShowTurrets )
turretSizeSlider := newNumericInputSliderWithStep ( minTurretSizePercent , maxTurretSizePercent , settings . TurretSize , turretSizePercentStep , "%.1f%%" , "Turret Size" )
turretSizeSlider . entry . OnChanged = func ( s string ) {
turretSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "turretSize" ] = hasError
updateGenerateBtnState ()
})
}
turretSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := turretSizeSlider . value . Get ()
settings . TurretSize = val
markPresetDirty ()
}))
turretShapeLabel := widget . NewLabel ( "Turret Shape:" )
turretShapeSelect := widget . NewSelect ([] string { "circular" , "square" }, func ( s string ) {
settings . TurretShape = s
markPresetDirty ()
})
turretShapeSelect . SetSelected ( settings . TurretShape )
turretSpacingSlider := newNumericInputSlider ( 0 , 100 , settings . TurretSpacing , "%.0f%%" , "Turret Spacing" )
turretSpacingSlider . entry . OnChanged = func ( s string ) {
turretSpacingSlider . validate ( s , func ( hasError bool ) {
errorStates [ "turretSpacing" ] = hasError
updateGenerateBtnState ()
})
}
turretSpacingSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := turretSpacingSlider . value . Get ()
settings . TurretSpacing = val
markPresetDirty ()
}))
2026-03-23 10:03:22 -05:00
gateCountSlider := newNumericInputSliderWithStep ( 1 , 20 , float64 ( settings . GateCount ), 1 , "%.0f" , "Gate Count" )
gateCountSlider . entry . OnChanged = func ( s string ) {
gateCountSlider . validate ( s , func ( hasError bool ) {
errorStates [ "gateCount" ] = hasError
2026-03-12 12:22:09 -05:00
updateGenerateBtnState ()
})
}
2026-03-23 10:03:22 -05:00
gateCountSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := gateCountSlider . value . Get ()
settings . GateCount = int ( math . Round ( val ))
2026-03-12 12:22:09 -05:00
markPresetDirty ()
}))
turretControls := container . NewVBox (
turretSizeSlider ,
turretShapeLabel ,
turretShapeSelect ,
turretSpacingSlider ,
)
if ! settings . ShowTurrets {
turretControls . Hide ()
}
showTurretsCheck . OnChanged = func ( v bool ) {
settings . ShowTurrets = v
if v {
turretControls . Show ()
} else {
turretControls . Hide ()
}
markPresetDirty ()
}
// Create UI elements for error display and action buttons
var applySettingsToUI func ( * Settings )
errorLabel := widget . NewLabel ( "" )
errorLabel . Wrapping = fyne . TextWrapWord
errorLabel . Hide ()
progressLabel := widget . NewLabel ( "" )
progressBar := widget . NewProgressBar ()
progressContainer := container . NewVBox ( progressLabel , progressBar )
progressContainer . Hide ()
exportCanvasBtn := widget . NewButton ( "Export Canvas" , func () {
showSaveDialog ( w , canvasImg . Image , settings )
})
exportHeightmapBtn := widget . NewButton ( "Export Heightmap" , func () {
showSaveDialog ( w , heightmapImg . Image , settings )
})
exportBumpmapBtn := widget . NewButton ( "Export Bump Map" , func () {
showSaveDialog ( w , bumpmapImg . Image , settings )
})
exportMasksBtn := widget . NewButton ( "Export Masks" , func () {
showMasksSaveDialog ( w , canvasImg . Image , heightmapImg . Image , bumpmapImg . Image , settings , lakes , riverMask , treeMask , roadMask , bridgeMask , wallMask , turretMask , buildingMask )
})
exportSettingsBtn := widget . NewButton ( "Export Settings" , func () {
showSettingsExportDialog ( w , settings )
})
importSettingsBtn := widget . NewButton ( "Import Settings" , func () {
showSettingsImportDialog ( w , settings , applySettingsToUI )
})
// Main generation button and logic
generateBtn = widget . NewButton ( "Generate" , func () {
go func () {
var timedOutSteps [] string
addTimeout := func ( step string ) {
timedOutSteps = append ( timedOutSteps , step )
}
// Disable button and show progress bar during generation
fyne . Do ( func () {
generateBtn . Disable ()
progressContainer . Show ()
})
defer func () {
// Re-enable button and hide progress bar after generation
fyne . Do ( func () {
generateBtn . Enable ()
progressContainer . Hide ()
})
}()
seedProvider := NewSeedProvider ( settings . Seed )
prevBuildings := settings . NumBuildings
cappedBuildings := capRequestedBuildingsToFit ( settings , settings . Width , settings . Height )
if cappedBuildings < prevBuildings {
log . Printf ( "Building count capped from %d to %d based on image size and average building size.\n" , prevBuildings , cappedBuildings )
}
// Step 1: Generating Heightmap
fyne . Do ( func () {
progressLabel . SetText ( "Step 1 of 13: Generating Heightmap" )
progressBar . SetValue ( 1.0 / 13.0 )
})
noiseImg := GenerateHeightmap ( settings . Width , settings . Height , int ( settings . Detail ), 100.0 , seedProvider . Next ())
// Step 2: Generating Lakes
fyne . Do ( func () {
progressLabel . SetText ( "Step 2 of 13: Generating Lakes" )
progressBar . SetValue ( 2.0 / 13.0 )
})
var lakeImage image . Image = image . NewRGBA ( image . Rect ( 0 , 0 , settings . Width , settings . Height ))
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
img image . Image
lks [][] image . Point
} {
img , lks := GenerateLakes ( settings . Width , settings . Height , settings . Lakes , settings . LakeSizeLower , settings . LakeSizeUpper , seedProvider . Next (), settings . LakeEdgeRoughness , settings . LakeShape )
return struct {
img image . Image
lks [][] image . Point
}{ img : img , lks : lks }
}); ok {
lakeImage = out . img
lakes = out . lks
} else {
log . Println ( "GenerateLakes timed out after 1 minute; continuing." )
addTimeout ( "Lakes" )
lakes = nil
}
// Step 3: Generating Rivers
fyne . Do ( func () {
progressLabel . SetText ( "Step 3 of 13: Generating Rivers" )
progressBar . SetValue ( 3.0 / 13.0 )
})
riverBase := cloneToRGBA ( lakeImage , settings . Width , settings . Height )
finalImage := riverBase
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
img image . Image
rmsk * PixelMask
} {
img , rmsk := GenerateRivers ( settings . Width , settings . Height , settings . Rivers , settings . MinRiverWidth , settings . MaxRiverWidth , settings . RiverCurvyness , riverBase , lakes , seedProvider . Next (), noiseImg , settings . RiverWidthVariability , settings . RiverEdgeRoughness )
return struct {
img image . Image
rmsk * PixelMask
}{ img : img , rmsk : rmsk }
}); ok {
riverMask = out . rmsk
finalImage = cloneToRGBA ( out . img , settings . Width , settings . Height )
} else {
log . Println ( "GenerateRivers timed out after 1 minute; continuing." )
addTimeout ( "Rivers" )
riverMask = NewPixelMask ( settings . Width , settings . Height )
}
waterMask = BuildMaskFromLakes ( settings . Width , settings . Height , lakes )
if riverMask != nil {
waterMask . Merge ( riverMask )
}
// Step 4: Preparing Road Nodes
fyne . Do ( func () {
progressLabel . SetText ( "Step 4 of 13: Preparing Road Nodes" )
progressBar . SetValue ( 4.0 / 13.0 )
})
var roadNodes [] * PointOfInterest
var roadTarget int
var edgeToEdgeOnly bool
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
pois [] * PointOfInterest
target int
edgeToEdge bool
} {
pois , target , edgeToEdge := PrepareRoadNodes ( settings . Width , settings . Height , settings , waterMask , seedProvider . Next ())
return struct {
pois [] * PointOfInterest
target int
edgeToEdge bool
}{ pois : pois , target : target , edgeToEdge : edgeToEdge }
}); ok {
roadNodes , roadTarget , edgeToEdgeOnly = out . pois , out . target , out . edgeToEdge
} else {
log . Println ( "PrepareRoadNodes timed out after 1 minute; continuing." )
addTimeout ( "Road Nodes" )
roadNodes = nil
roadTarget = 0
edgeToEdgeOnly = false
}
// Step 5: Generating Fortifications
fyne . Do ( func () {
progressLabel . SetText ( "Step 5 of 13: Generating Fortifications" )
progressBar . SetValue ( 5.0 / 13.0 )
})
var wallLayout * FortificationLayout
fortBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
layout * FortificationLayout
} {
layout , _ := GenerateFortifications ( fortBase , settings . Width , settings . Height , settings , waterMask , roadNodes , seedProvider . Next ())
return struct {
layout * FortificationLayout
}{ layout : layout }
}); ok {
wallLayout = out . layout
if wallLayout != nil {
wallMask = wallLayout . Mask
} else {
wallMask = NewPixelMask ( settings . Width , settings . Height )
}
finalImage = fortBase
} else {
log . Println ( "GenerateFortifications timed out after 1 minute; continuing." )
addTimeout ( "Fortifications" )
wallLayout = & FortificationLayout { Mask : NewPixelMask ( settings . Width , settings . Height )}
wallMask = wallLayout . Mask
}
// Step 6: Generating Roads
fyne . Do ( func () {
progressLabel . SetText ( "Step 6 of 13: Generating Roads" )
progressBar . SetValue ( 6.0 / 13.0 )
})
var roadAnchors [] image . Point
var roadList [] * Road
roadBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
rl [] * Road
} {
rd , br , ex , anc , rl := GenerateRoadsWithPOIs ( roadBase , settings . Width , settings . Height , settings , waterMask , wallLayout , roadNodes , roadTarget , edgeToEdgeOnly , seedProvider . Next ())
return struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
rl [] * Road
}{ rd : rd , br : br , ex : ex , anc : anc , rl : rl }
}); ok {
roadMask , bridgeMask , exitRoadMask , roadAnchors , roadList = out . rd , out . br , out . ex , out . anc , out . rl
drawWallMask ( roadBase , wallMask )
turretMask = GenerateTurrets ( roadBase , settings . Width , settings . Height , settings , wallLayout , waterMask , roadMask , roadList )
finalImage = roadBase
} else {
log . Println ( "GenerateRoads timed out after 1 minute; continuing." )
addTimeout ( "Roads" )
roadMask = NewPixelMask ( settings . Width , settings . Height )
bridgeMask = NewPixelMask ( settings . Width , settings . Height )
exitRoadMask = NewPixelMask ( settings . Width , settings . Height )
turretMask = NewPixelMask ( settings . Width , settings . Height )
roadAnchors = nil
}
placementMask := cloneMask ( roadMask )
if placementMask == nil {
placementMask = NewPixelMask ( settings . Width , settings . Height )
}
if wallMask != nil {
placementMask . Merge ( wallMask )
}
if turretMask != nil {
placementMask . Merge ( turretMask )
}
// Step 7: Generating Buildings
fyne . Do ( func () {
progressLabel . SetText ( "Step 7 of 13: Generating Buildings" )
progressBar . SetValue ( 7.0 / 13.0 )
})
buildingBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
blds [][] image . Point
bmsk * PixelMask
} {
blds , bmsk := GenerateBuildings ( buildingBase , settings . Width , settings . Height , settings , roadAnchors , waterMask , placementMask , exitRoadMask , seedProvider . Next ())
return struct {
blds [][] image . Point
bmsk * PixelMask
}{ blds : blds , bmsk : bmsk }
}); ok {
buildings , buildingMask = out . blds , out . bmsk
finalImage = buildingBase
} else {
log . Println ( "GenerateBuildings timed out after 1 minute; continuing." )
addTimeout ( "Buildings" )
buildings = nil
buildingMask = NewPixelMask ( settings . Width , settings . Height )
}
// Step 8: Darkening Water Areas
fyne . Do ( func () {
progressLabel . SetText ( "Step 8 of 13: Darkening Water Areas" )
progressBar . SetValue ( 8.0 / 13.0 )
})
darkenedHeightmap := DarkenLakeAreas ( noiseImg , waterMask )
// Step 9: Flattening Building Areas
fyne . Do ( func () {
progressLabel . SetText ( "Step 9 of 13: Flattening Building Areas" )
progressBar . SetValue ( 9.0 / 13.0 )
})
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
fyne . Do ( func () {
progressLabel . SetText ( "Step 10 of 13: Flattening Road and Wall Areas" )
progressBar . SetValue ( 10.0 / 13.0 )
})
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , placementMask )
// Step 11: Applying Roughness
fyne . Do ( func () {
progressLabel . SetText ( "Step 11 of 13: Applying Roughness" )
progressBar . SetValue ( 11.0 / 13.0 )
})
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
// Step 12: Generating Trees
fyne . Do ( func () {
progressLabel . SetText ( "Step 12 of 13: Generating Trees" )
progressBar . SetValue ( 12.0 / 13.0 )
})
treeBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () * PixelMask {
return GenerateTrees ( treeBase , waterMask , placementMask , buildingMask , settings . MinTreeSize , settings . MaxTreeSize , settings . TreeCoverage , settings . TreeClumpiness , seedProvider . Next ())
}); ok {
treeMask = out
2026-03-23 10:03:22 -05:00
if wallMask != nil {
drawWallMask ( treeBase , wallMask )
}
if turretMask != nil {
drawTurretMask ( treeBase , turretMask )
}
2026-03-12 12:22:09 -05:00
finalImage = treeBase
} else {
log . Println ( "GenerateTrees timed out after 1 minute; continuing." )
addTimeout ( "Trees" )
treeMask = NewPixelMask ( settings . Width , settings . Height )
}
// Step 13: Generating Bump Map
fyne . Do ( func () {
progressLabel . SetText ( "Step 13 of 13: Generating Bump Map" )
progressBar . SetValue ( 1.0 )
})
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
// Step 11: Finalizing Images
fyne . Do ( func () {
heightmapImg . Image = compositeImg
heightmapImg . Refresh ()
bumpmapImg . Image = bumpMap
bumpmapImg . Refresh ()
canvasImg . Image = finalImage
canvasImg . Refresh ()
if len ( timedOutSteps ) > 0 {
errorLabel . SetText ( "Generation timed out after 1 minute for: " + strings . Join ( timedOutSteps , ", " ) + ". Partial results were used." )
errorLabel . Show ()
} else {
errorLabel . Hide ()
}
})
}()
})
// Create UI elements for image dimensions and seed
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 ))
})
// Create tabs for organizing settings
terrainTab := container . NewTabItem ( "Terrain" , container . NewVBox (
detailSlider ,
roughnessSlider ,
widget . NewLabel ( "" ), // Spacer
minTreeSizeSlider ,
maxTreeSizeSlider ,
treeCoverageSlider ,
treeClumpinessSlider ,
))
waterTab := container . NewTabItem ( "Water" , container . NewVBox (
lakesSlider ,
lakeSizeLowerSlider ,
lakeSizeUpperSlider ,
lakeEdgeRoughnessSlider ,
lakeShapeLabel ,
lakeShapeSelect ,
widget . NewLabel ( "" ), // Spacer
riversSlider ,
minRiverWidthSlider ,
maxRiverWidthSlider ,
riverCurvynessSlider ,
riverWidthVariabilitySlider ,
riverEdgeRoughnessSlider ,
))
roadsTab := container . NewTabItem ( "Roads" , container . NewVBox (
minRoadWidthSlider ,
maxRoadWidthSlider ,
buildingsPerRoadSlider ,
roadExitsSlider ,
minRoadAngleSlider ,
roadCurvynessSlider ,
roadDistributionSlider ,
))
fortificationsTab := container . NewTabItem ( "Fortifications" , container . NewVBox (
minWallWidthSlider ,
maxWallWidthSlider ,
numWallsSlider ,
cityCoverageSlider ,
wallCurvynessSlider ,
2026-03-23 10:03:22 -05:00
gateCountSlider ,
2026-03-12 12:22:09 -05:00
showTurretsCheck ,
turretControls ,
))
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 ()
settings . NumBuildings = int ( val )
markPresetDirty ()
}))
minBuildingSizeSlider := newNumericInputSliderWithStep ( minBuildingSizePercent , maxBuildingSizePercent , settings . MinBuildingSize , buildingSizePercentStep , "%.1f%%" , "Min Building Size" )
minBuildingSizeSlider . entry . OnChanged = func ( s string ) {
minBuildingSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minBuildingSize" ] = hasError
updateGenerateBtnState ()
})
}
minBuildingSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minBuildingSizeSlider . value . Get ()
settings . MinBuildingSize = val
markPresetDirty ()
}))
maxBuildingSizeSlider := newNumericInputSliderWithStep ( minBuildingSizePercent , maxBuildingSizePercent , settings . MaxBuildingSize , buildingSizePercentStep , "%.1f%%" , "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 ()
settings . MaxBuildingSize = val
markPresetDirty ()
}))
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 ()
})
}
buildingDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingDistributionSlider . value . Get ()
settings . BuildingDistribution = val
markPresetDirty ()
}))
buildingShapeLabel := widget . NewLabel ( "Building Shape:" )
buildingShapeSelect := widget . NewSelect ([] string { "squares" , "circles" , "rectangles" , "mixed" , "procedural" }, func ( s string ) {
settings . BuildingShape = s
markPresetDirty ()
})
buildingShapeSelect . SetSelected ( settings . BuildingShape )
// 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 ,
)
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 ()
settings . MinBuildingComplexity = int ( val )
markPresetDirty ()
}))
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 ()
settings . MaxBuildingComplexity = int ( val )
markPresetDirty ()
}))
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 ()
})
}
buildingComplexityRatioSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingComplexityRatioSlider . value . Get ()
settings . BuildingComplexityRatio = val
markPresetDirty ()
}))
proceduralContainer := container . NewVBox (
minBuildingComplexitySlider ,
maxBuildingComplexitySlider ,
buildingComplexityRatioSlider ,
)
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 ) {
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
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 ()
markPresetDirty ()
}
circleRatioSlider . OnChanged = func ( val float64 ) {
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
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 ()
markPresetDirty ()
}
rectangleRatioSlider . OnChanged = func ( val float64 ) {
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
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 ()
markPresetDirty ()
}
buildingShapeSelect . OnChanged = func ( s string ) {
settings . BuildingShape = s
if s == "mixed" {
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if s == "procedural" {
proceduralContainer . Show ()
ratioContainer . Show ()
} else {
proceduralContainer . Hide ()
ratioContainer . Hide ()
}
markPresetDirty ()
}
// Initial visibility check
if settings . BuildingShape == "mixed" {
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if settings . BuildingShape == "procedural" {
proceduralContainer . Show ()
ratioContainer . Show ()
} else {
proceduralContainer . Hide ()
ratioContainer . Hide ()
}
applySettingsToUI = func ( imported * Settings ) {
lastExportPath := settings . LastExportPath
imageViewState := settings . ImageViewState
* settings = * imported
settings . LastExportPath = lastExportPath
settings . ImageViewState = imageViewState
if settings . BuildingShapeRatios == nil {
settings . BuildingShapeRatios = map [ string ] float64 {
"squares" : 40 ,
"circles" : 30 ,
"rectangles" : 30 ,
}
}
widthEntry . SetText ( strconv . Itoa ( settings . Width ))
heightEntry . SetText ( strconv . Itoa ( settings . Height ))
seedEntry . SetText ( strconv . FormatInt ( settings . Seed , 10 ))
detailSlider . value . Set ( settings . Detail )
roughnessSlider . value . Set ( settings . Roughness )
lakesSlider . value . Set ( float64 ( settings . Lakes ))
lakeSizeLowerSlider . value . Set ( settings . LakeSizeLower )
lakeSizeUpperSlider . value . Set ( settings . LakeSizeUpper )
lakeEdgeRoughnessSlider . value . Set ( settings . LakeEdgeRoughness )
lakeShapeSelect . SetSelected ( settings . LakeShape )
riversSlider . value . Set ( float64 ( settings . Rivers ))
minRiverWidthSlider . value . Set ( settings . MinRiverWidth )
maxRiverWidthSlider . value . Set ( settings . MaxRiverWidth )
riverCurvynessSlider . value . Set ( settings . RiverCurvyness )
riverWidthVariabilitySlider . value . Set ( settings . RiverWidthVariability )
riverEdgeRoughnessSlider . value . Set ( settings . RiverEdgeRoughness )
minTreeSizeSlider . value . Set ( settings . MinTreeSize )
maxTreeSizeSlider . value . Set ( settings . MaxTreeSize )
treeCoverageSlider . value . Set ( settings . TreeCoverage )
treeClumpinessSlider . value . Set ( settings . TreeClumpiness )
minRoadWidthSlider . value . Set ( settings . MinRoadWidth )
maxRoadWidthSlider . value . Set ( settings . MaxRoadWidth )
buildingsPerRoadSlider . value . Set ( float64 ( settings . BuildingsPerRoad ))
roadExitsSlider . value . Set ( float64 ( settings . RoadExits ))
roadCurvynessSlider . value . Set ( settings . RoadCurvyness )
roadDistributionSlider . value . Set ( settings . RoadDistribution )
minRoadAngleSlider . value . Set ( settings . MinRoadAngle )
minWallWidthSlider . value . Set ( settings . MinWallWidth )
maxWallWidthSlider . value . Set ( settings . MaxWallWidth )
numWallsSlider . value . Set ( float64 ( settings . NumWalls ))
cityCoverageSlider . value . Set ( settings . CityCoverage )
wallCurvynessSlider . value . Set ( settings . WallCurvyness )
2026-03-23 10:03:22 -05:00
gateCountSlider . value . Set ( float64 ( settings . GateCount ))
2026-03-12 12:22:09 -05:00
showTurretsCheck . SetChecked ( settings . ShowTurrets )
turretSizeSlider . value . Set ( settings . TurretSize )
turretShapeSelect . SetSelected ( settings . TurretShape )
turretSpacingSlider . value . Set ( settings . TurretSpacing )
numBuildingsSlider . value . Set ( float64 ( settings . NumBuildings ))
minBuildingSizeSlider . value . Set ( settings . MinBuildingSize )
maxBuildingSizeSlider . value . Set ( settings . MaxBuildingSize )
buildingDistributionSlider . value . Set ( settings . BuildingDistribution )
buildingShapeSelect . SetSelected ( settings . BuildingShape )
updateRatioSliders ()
minBuildingComplexitySlider . value . Set ( float64 ( settings . MinBuildingComplexity ))
maxBuildingComplexitySlider . value . Set ( float64 ( settings . MaxBuildingComplexity ))
buildingComplexityRatioSlider . value . Set ( settings . BuildingComplexityRatio )
for key := range errorStates {
errorStates [ key ] = false
}
errorLabel . Hide ()
updateGenerateBtnState ()
}
presetDir := filepath . Join ( appConfigDir , "presets" )
var presetInfoByName map [ string ] presetInfo
var suppressPresetApply bool
var removePresetBtn * widget . Button
presetSelect := widget . NewSelect ([] string { "No Preset" }, func ( name string ) {
if suppressPresetApply {
return
}
if name == "" || name == "No Preset" {
if removePresetBtn != nil {
removePresetBtn . Disable ()
}
return
}
info , ok := presetInfoByName [ name ]
if ! ok {
return
}
applyingPreset = true
var imported * Settings
var err error
if info . Source == "embedded" {
imported , err = loadEmbeddedPreset ( info . Path )
} else {
raw , readErr := os . ReadFile ( info . Path )
if readErr != nil {
err = readErr
} else {
imported , err = decodeSettingsJSON ( raw )
}
}
if err != nil {
applyingPreset = false
dialog . ShowError ( err , w )
return
}
imported . Seed = settings . Seed
applySettingsToUI ( imported )
time . AfterFunc ( 50 * time . Millisecond , func () {
fyne . Do ( func () {
applyingPreset = false
})
})
if removePresetBtn != nil {
removePresetBtn . Enable ()
}
})
presetSelect . SetSelected ( "No Preset" )
clearPresetSelection = func () {
if presetSelect . Selected != "No Preset" {
presetSelect . SetSelected ( "No Preset" )
}
}
refreshPresetOptions := func ( selectName string ) {
names , info := buildPresetOptions ( settings , presetDir )
presetInfoByName = info
suppressPresetApply = true
presetSelect . Options = names
presetSelect . Refresh ()
if selectName == "" {
selectName = presetSelect . Selected
}
if _ , ok := presetInfoByName [ selectName ]; ! ok {
selectName = "No Preset"
}
presetSelect . SetSelected ( selectName )
if removePresetBtn != nil {
if selectName == "" || selectName == "No Preset" {
removePresetBtn . Disable ()
} else {
removePresetBtn . Enable ()
}
}
suppressPresetApply = false
}
addPresetBtn := widget . NewButton ( "Add Preset" , func () {
nameEntry := widget . NewEntry ()
nameEntry . SetPlaceHolder ( "Preset Name" )
content := container . NewVBox (
widget . NewLabel ( "Preset Name:" ),
nameEntry ,
)
var addDialog * dialog . CustomDialog
saveBtn := widget . NewButton ( "Save" , func () {
name := strings . TrimSpace ( nameEntry . Text )
if name == "" || name == "No Preset" {
return
}
if strings . ContainsAny ( name , "/\\" ) {
dialog . ShowError ( fmt . Errorf ( "preset name cannot contain path separators" ), w )
return
}
if _ , exists := presetInfoByName [ name ]; exists {
dialog . ShowError ( fmt . Errorf ( "preset name already exists" ), w )
return
}
if err := os . MkdirAll ( presetDir , 0755 ); err != nil {
dialog . ShowError ( err , w )
return
}
filePath := filepath . Join ( presetDir , name + ".json" )
if err := savePresetToFile ( filePath , settings ); err != nil {
dialog . ShowError ( err , w )
return
}
refreshPresetOptions ( name )
if addDialog != nil {
addDialog . Hide ()
}
})
saveBtn . Disable ()
nameEntry . OnChanged = func ( text string ) {
if strings . TrimSpace ( text ) == "" {
saveBtn . Disable ()
} else {
saveBtn . Enable ()
}
}
addDialog = dialog . NewCustom ( "Add Preset" , "Cancel" , content , w )
addDialog . SetButtons ([] fyne . CanvasObject {
widget . NewButton ( "Cancel" , func () {
addDialog . Hide ()
}),
saveBtn ,
})
addDialog . Show ()
})
removePresetBtn = widget . NewButton ( "Remove Preset" , func () {
name := presetSelect . Selected
if name == "" || name == "No Preset" {
return
}
info , ok := presetInfoByName [ name ]
if ! ok {
return
}
if info . Source == "embedded" {
found := false
for _ , existing := range settings . HiddenPresets {
if existing == name {
found = true
break
}
}
if ! found {
settings . HiddenPresets = append ( settings . HiddenPresets , name )
if err := settings . Save (); err != nil {
log . Println ( "Error saving hidden preset list:" , err )
}
}
} else {
if err := os . Remove ( info . Path ); err != nil {
dialog . ShowError ( err , w )
return
}
}
refreshPresetOptions ( "No Preset" )
})
removePresetBtn . Disable ()
refreshPresetOptions ( "No Preset" )
buildingsTab := container . NewTabItem ( "Buildings" , container . NewVBox (
numBuildingsSlider ,
minBuildingSizeSlider ,
maxBuildingSizeSlider ,
buildingDistributionSlider ,
buildingShapeLabel ,
buildingShapeSelect ,
proceduralContainer ,
ratioContainer ,
))
imageTab := container . NewTabItem ( "Image" , container . NewVBox (
widget . NewLabel ( "Preset:" ),
presetSelect ,
container . NewGridWithColumns ( 2 , addPresetBtn , removePresetBtn ),
widget . NewSeparator (),
widget . NewLabel ( "Width:" ),
widthEntry , widget . NewLabel ( "Height:" ),
heightEntry ,
widget . NewLabel ( "Seed:" ),
seedEntry ,
randomizeBtn ,
generateBtn ,
errorLabel ,
progressContainer ,
widget . NewSeparator (),
exportCanvasBtn ,
exportHeightmapBtn ,
exportBumpmapBtn ,
exportMasksBtn ,
exportSettingsBtn ,
importSettingsBtn ,
))
// Create the main layout using a horizontal split
tabs := container . NewAppTabs (
imageTab ,
terrainTab ,
waterTab ,
roadsTab ,
fortificationsTab ,
buildingsTab ,
)
left := container . NewVBox (
widget . NewLabel ( "RPG City Maker Reborn" ),
tabs ,
)
right := container . NewMax ()
2026-03-24 11:39:14 -05:00
legendBar := makeMapLegendBar ()
rightSplit := container . NewHSplit ( right , legendBar )
rightSplit . Offset = 0.82
2026-03-12 12:22:09 -05:00
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 ()
split := container . NewHSplit (
left ,
2026-03-24 11:39:14 -05:00
rightSplit ,
2026-03-12 12:22:09 -05:00
)
split . SetOffset ( 0.3 )
// Set the window content and start the application
w . SetContent ( split )
w . ShowAndRun ()
}
2026-03-24 11:39:14 -05:00
func makeMapLegendBar () fyne . CanvasObject {
content := container . NewVBox (
widget . NewLabelWithStyle ( "Map Legend" , fyne . TextAlignLeading , fyne . TextStyle { Bold : true }),
widget . NewSeparator (),
makeLegendItem ( color . RGBA { R : 34 , G : 139 , B : 34 , A : 255 }, "Trees" ),
makeLegendItem ( color . RGBA { R : 128 , G : 128 , B : 128 , A : 255 }, "Buildings" ),
makeLegendItem ( color . RGBA { R : 139 , G : 69 , B : 19 , A : 255 }, "Roads" ),
makeLegendItem ( color . RGBA { R : 60 , G : 42 , B : 33 , A : 255 }, "Bridges" ),
makeLegendItem ( color . RGBA { R : 0 , G : 0 , B : 0 , A : 255 }, "Walls" ),
makeLegendItem ( color . RGBA { R : 220 , G : 25 , B : 25 , A : 255 }, "Turrets and Gates" ),
makeLegendItem ( color . RGBA { R : 30 , G : 90 , B : 200 , A : 255 }, "Water" ),
)
contentWrapper := container . NewPadded ( content )
contentWrapper . Resize ( fyne . NewSize ( 185 , contentWrapper . MinSize (). Height ))
return contentWrapper
}
func makeLegendItem ( swatch color . Color , label string ) fyne . CanvasObject {
rect := canvas . NewRectangle ( swatch )
rect . SetMinSize ( fyne . NewSize ( 18 , 18 ))
outline := canvas . NewRectangle ( color . Transparent )
outline . StrokeColor = color . RGBA { R : 50 , G : 50 , B : 50 , A : 255 }
outline . StrokeWidth = 1
outline . SetMinSize ( fyne . NewSize ( 18 , 18 ))
return container . NewHBox ( container . NewStack ( rect , outline ), widget . NewLabel ( label ))
}
2026-03-12 12:22:09 -05:00
func encodeImageToWriter ( w io . Writer , img image . Image , format string ) error {
switch format {
case "PNG" :
return png . Encode ( w , img )
case "JPG" :
return jpeg . Encode ( w , img , nil )
case "WEBP" :
return webp . Encode ( w , img , & webp . Options { Lossless : true })
}
return fmt . Errorf ( "unsupported format: %s" , format )
}
// showMasksSaveDialog displays a dialog for saving the generated masks.
func showMasksSaveDialog ( win fyne . Window , canvasImg , heightmapImg , bumpmapImg image . Image , settings * Settings , lakes [][] image . Point , riverMask , treeMask , roadMask , bridgeMask , wallMask , turretMask , buildingMask * PixelMask ) {
// Create UI elements for the save dialog
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 ()
maskToGray := func ( mask * PixelMask ) image . Image {
out := image . NewGray ( bounds )
if mask == nil {
return out
}
for y := 0 ; y < mask . Height ; y ++ {
row := y * mask . Width
for x := 0 ; x < mask . Width ; x ++ {
if mask . Data [ row + x ] != 0 {
out . SetGray ( x , y , color . Gray { Y : 255 })
}
}
}
return out
}
buildLakeMask := func () image . Image {
lakeMask := image . NewGray ( bounds )
for _ , lake := range lakes {
for _ , p := range lake {
lakeMask . SetGray ( p . X , p . Y , color . Gray { Y : 255 })
}
}
return lakeMask
}
type exportItem struct {
name string
make func () image . Image
}
items := [] exportItem {
{ name : "canvas." + imgFormat , make : func () image . Image { return canvasImg }},
{ name : "heightmap." + imgFormat , make : func () image . Image { return heightmapImg }},
{ name : "bump_map." + imgFormat , make : func () image . Image { return bumpmapImg }},
{ name : "lakes_mask." + imgFormat , make : buildLakeMask },
{ name : "rivers_mask." + imgFormat , make : func () image . Image { return maskToGray ( riverMask ) }},
{ name : "trees_mask." + imgFormat , make : func () image . Image { return maskToGray ( treeMask ) }},
{ name : "roads_mask." + imgFormat , make : func () image . Image { return maskToGray ( roadMask ) }},
{ name : "bridges_mask." + imgFormat , make : func () image . Image { return maskToGray ( bridgeMask ) }},
{ name : "walls_mask." + imgFormat , make : func () image . Image { return maskToGray ( wallMask ) }},
{ name : "turrets_mask." + imgFormat , make : func () image . Image { return maskToGray ( turretMask ) }},
{ name : "buildings_mask." + imgFormat , make : func () image . Image { return maskToGray ( buildingMask ) }},
}
// Save the images based on the selected packaging option
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 _ , item := range items {
saveImage ( item . make (), filepath . Join ( exportPath , item . 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 ()
var buf bytes . Buffer
for _ , item := range items {
buf . Reset ()
if err := encodeImageToWriter ( & buf , item . make (), formatSelect . Selected ); err != nil {
log . Printf ( "Error encoding image %s: %v\n" , item . name , err )
continue
}
hdr := & tar . Header {
Name : item . name ,
Mode : 0644 ,
Size : int64 ( buf . Len ()),
}
if err := tw . WriteHeader ( hdr ); err != nil {
log . Printf ( "Error writing tar header for %s: %v\n" , item . name , err )
continue
}
if _ , err := tw . Write ( buf . Bytes ()); err != nil {
log . Printf ( "Error writing tar data for %s: %v\n" , item . 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 _ , item := range items {
f , err := zw . Create ( item . name )
if err != nil {
log . Printf ( "Error creating zip entry for %s: %v\n" , item . name , err )
continue
}
if err := encodeImageToWriter ( f , item . make (), formatSelect . Selected ); err != nil {
log . Printf ( "Error writing zip data for %s: %v\n" , item . name , err )
continue
}
}
}
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 ()
}
// saveImage saves an image to the specified path.
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 )
}
}
func showSettingsExportDialog ( win fyne . Window , settings * Settings ) {
fileNameEntry := widget . NewEntry ()
fileNameEntry . SetPlaceHolder ( "settings" )
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 ,
)
saveDialog := dialog . NewCustom ( "Export Settings" , "Cancel" , content , win )
saveBtn := widget . NewButton ( "Save" , func () {
fileName := strings . TrimSpace ( fileNameEntry . Text )
if fileName == "" {
return
}
if ! strings . HasSuffix ( strings . ToLower ( fileName ), ".json" ) {
fileName += ".json"
}
filePath := filepath . Join ( pathLabel . Text , fileName )
if err := saveSettingsToFile ( filePath , settings ); err != nil {
dialog . ShowError ( err , win )
return
}
settings . LastExportPath = filepath . Dir ( filePath )
saveDialog . Hide ()
})
saveBtn . Disable ()
fileNameEntry . OnChanged = func ( text string ) {
if strings . TrimSpace ( text ) == "" {
saveBtn . Disable ()
} else {
saveBtn . Enable ()
}
}
saveDialog . SetButtons ([] fyne . CanvasObject {
widget . NewButton ( "Cancel" , func () {
saveDialog . Hide ()
}),
saveBtn ,
})
saveDialog . Show ()
}
func showSettingsImportDialog ( win fyne . Window , settings * Settings , apply func ( * Settings )) {
dialog . ShowFileOpen ( func ( rc fyne . URIReadCloser , err error ) {
if err != nil {
dialog . ShowError ( err , win )
return
}
if rc == nil {
return
}
defer rc . Close ()
raw , err := io . ReadAll ( rc )
if err != nil {
dialog . ShowError ( err , win )
return
}
imported , err := decodeSettingsJSON ( raw )
if err != nil {
dialog . ShowError ( err , win )
return
}
if uri := rc . URI (); uri != nil {
settings . LastExportPath = filepath . Dir ( uri . Path ())
}
apply ( imported )
}, win )
}
// showSaveDialog displays a dialog for saving an image.
func showSaveDialog ( win fyne . Window , img image . Image , settings * Settings ) {
// Create UI elements for the save dialog
fileNameEntry := widget . NewEntry ()
fileNameEntry . SetPlaceHolder ( "image" )
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 )
saveImage ( img , filePath )
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 ()
}