package main import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "fmt" "image" "image/color" "image/jpeg" "image/png" "log" "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/dialog" "fyne.io/fyne/v2/widget" "github.com/chai2010/webp" ) func main() { // Initialize the Fyne application and window a := app.NewWithID("com.example.rpgcitymaker") a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()}) w := a.NewWindow("RPG City Maker") w.Resize(fyne.NewSize(800, 600)) // Load settings from file, or use defaults if loading fails settings, err := LoadSettings() if err != nil { log.Println("Error loading settings:", err) // 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, } // Initialize slices to store generated map features var lakes [][]image.Point var riverPixels []image.Point var treePixels []image.Point var buildingPixels []image.Point var buildings [][]image.Point var roadPixels []image.Point var bridgePixels []image.Point // Set up application configuration directory configDir, err := os.UserConfigDir() if err != nil { log.Fatal("Failed to get user config dir:", err) } appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn") canvasPath := filepath.Join(appConfigDir, "canvas.png") heightmapPath := filepath.Join(appConfigDir, "heightmap.png") // Save settings and images on window close w.SetOnClosed(func() { if err := settings.Save(); err != nil { log.Println("Error saving settings:", err) } 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) } } } }) // 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 } } // 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()) // Step 2: Generating Lakes var lakeImage image.Image lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next()) // Step 3: Generating Rivers var riverImage image.Image riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg) finalImage := riverImage.(*image.RGBA) var roadImage *image.RGBA var flatLakePixels []image.Point for _, lake := range lakes { flatLakePixels = append(flatLakePixels, lake...) } allWaterPixels := append(flatLakePixels, riverPixels...) // Step 4: Generating Roads roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) for y := 0; y < roadImage.Bounds().Max.Y; y++ { for x := 0; x < roadImage.Bounds().Max.X; x++ { 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)}) } } } // Step 5: Generating Buildings buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) // Step 6: Generating Trees treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) // Step 7: Darkening Water Areas darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) // Step 8: Flattening Building Areas flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height) // Step 9: Flattening Road Areas flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) // Step 10: Applying Roughness compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness) heightmapImg.Image = compositeImg canvasImg.Image = finalImage } // Create UI elements for controlling terrain generation settings detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail)) detailSlider := widget.NewSlider(1, 16) detailSlider.OnChanged = func(val float64) { settings.Detail = val detailLabel.SetText(fmt.Sprintf("Detail: %.0f", settings.Detail)) } detailSlider.SetValue(settings.Detail) roughnessLabel := widget.NewLabel(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness)) roughnessSlider := widget.NewSlider(0, 100) roughnessSlider.OnChanged = func(val float64) { settings.Roughness = val roughnessLabel.SetText(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness)) } roughnessSlider.SetValue(settings.Roughness) // Create UI elements for controlling lake generation settings lakesLabel := widget.NewLabel(fmt.Sprintf("Lakes: %d", settings.Lakes)) lakesSlider := widget.NewSlider(0, 15) lakesSlider.OnChanged = func(val float64) { settings.Lakes = int(val) lakesLabel.SetText(fmt.Sprintf("Lakes: %d", settings.Lakes)) } lakesSlider.SetValue(float64(settings.Lakes)) lakeSizeLowerLabel := widget.NewLabel(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower)) lakeSizeLowerSlider := widget.NewSlider(1, 100) lakeSizeUpperLabel := widget.NewLabel(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper)) lakeSizeUpperSlider := widget.NewSlider(1, 100) lakeSizeLowerSlider.OnChanged = func(val float64) { settings.LakeSizeLower = val if settings.LakeSizeLower > settings.LakeSizeUpper { settings.LakeSizeUpper = settings.LakeSizeLower lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper) } lakeSizeLowerLabel.SetText(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower)) } lakeSizeLowerSlider.SetValue(settings.LakeSizeLower) lakeSizeUpperSlider.OnChanged = func(val float64) { settings.LakeSizeUpper = val if settings.LakeSizeUpper < settings.LakeSizeLower { settings.LakeSizeLower = settings.LakeSizeUpper lakeSizeLowerSlider.SetValue(settings.LakeSizeLower) } lakeSizeUpperLabel.SetText(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper)) } lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper) // Create UI elements for controlling river generation settings riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers)) riversSlider := widget.NewSlider(0, 5) riversSlider.OnChanged = func(val float64) { settings.Rivers = int(val) riversLabel.SetText(fmt.Sprintf("Rivers: %d", settings.Rivers)) } riversSlider.SetValue(float64(settings.Rivers)) minRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth)) minRiverWidthSlider := widget.NewSlider(1, 100) maxRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth)) maxRiverWidthSlider := widget.NewSlider(1, 100) minRiverWidthSlider.OnChanged = func(val float64) { settings.MinRiverWidth = val if settings.MinRiverWidth > settings.MaxRiverWidth { settings.MaxRiverWidth = settings.MinRiverWidth maxRiverWidthSlider.SetValue(settings.MaxRiverWidth) } minRiverWidthLabel.SetText(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth)) } minRiverWidthSlider.SetValue(settings.MinRiverWidth) maxRiverWidthSlider.OnChanged = func(val float64) { settings.MaxRiverWidth = val if settings.MaxRiverWidth < settings.MinRiverWidth { settings.MinRiverWidth = settings.MaxRiverWidth minRiverWidthSlider.SetValue(settings.MinRiverWidth) } maxRiverWidthLabel.SetText(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth)) } maxRiverWidthSlider.SetValue(settings.MaxRiverWidth) riverCurvynessLabel := widget.NewLabel(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness)) riverCurvynessSlider := widget.NewSlider(0, 100) riverCurvynessSlider.OnChanged = func(val float64) { settings.RiverCurvyness = val riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness)) } riverCurvynessSlider.SetValue(settings.RiverCurvyness) // Create UI elements for controlling tree generation settings minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize)) minTreeSizeSlider := widget.NewSlider(1, 150) maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize)) maxTreeSizeSlider := widget.NewSlider(1, 150) minTreeSizeSlider.OnChanged = func(val float64) { settings.MinTreeSize = val if settings.MinTreeSize > settings.MaxTreeSize { settings.MaxTreeSize = settings.MinTreeSize maxTreeSizeSlider.SetValue(settings.MaxTreeSize) } minTreeSizeLabel.SetText(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize)) } minTreeSizeSlider.SetValue(settings.MinTreeSize) maxTreeSizeSlider.OnChanged = func(val float64) { settings.MaxTreeSize = val if settings.MaxTreeSize < settings.MinTreeSize { settings.MinTreeSize = settings.MaxTreeSize minTreeSizeSlider.SetValue(settings.MinTreeSize) } maxTreeSizeLabel.SetText(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize)) } maxTreeSizeSlider.SetValue(settings.MaxTreeSize) treeCoverageLabel := widget.NewLabel(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage)) treeCoverageSlider := widget.NewSlider(1, 100) treeCoverageSlider.OnChanged = func(val float64) { settings.TreeCoverage = val treeCoverageLabel.SetText(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage)) } treeCoverageSlider.SetValue(settings.TreeCoverage) treeClumpinessLabel := widget.NewLabel(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness)) treeClumpinessSlider := widget.NewSlider(0, 100) treeClumpinessSlider.OnChanged = func(val float64) { settings.TreeClumpiness = val treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness)) } treeClumpinessSlider.SetValue(settings.TreeClumpiness) // Create UI elements for controlling road generation settings numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads)) numRoadsSlider := widget.NewSlider(0, 2000) numRoadsSlider.OnChanged = func(val float64) { settings.NumRoads = int(val) numRoadsLabel.SetText(fmt.Sprintf("Number of Roads: %d", settings.NumRoads)) } numRoadsSlider.SetValue(float64(settings.NumRoads)) minRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth)) minRoadWidthSlider := widget.NewSlider(1, 150) maxRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth)) maxRoadWidthSlider := widget.NewSlider(1, 150) minRoadWidthSlider.OnChanged = func(val float64) { settings.MinRoadWidth = val if settings.MinRoadWidth > settings.MaxRoadWidth { settings.MaxRoadWidth = settings.MinRoadWidth maxRoadWidthSlider.SetValue(settings.MaxRoadWidth) } minRoadWidthLabel.SetText(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth)) } minRoadWidthSlider.SetValue(settings.MinRoadWidth) maxRoadWidthSlider.OnChanged = func(val float64) { settings.MaxRoadWidth = val if settings.MaxRoadWidth < settings.MinRoadWidth { settings.MinRoadWidth = settings.MaxRoadWidth minRoadWidthSlider.SetValue(settings.MinRoadWidth) } maxRoadWidthLabel.SetText(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth)) } maxRoadWidthSlider.SetValue(settings.MaxRoadWidth) roadExitsLabel := widget.NewLabel(fmt.Sprintf("Road Exits: %d", settings.RoadExits)) roadExitsSlider := widget.NewSlider(0, 100) roadExitsSlider.OnChanged = func(val float64) { if val > float64(settings.NumRoads) { val = float64(settings.NumRoads) roadExitsSlider.SetValue(val) } settings.RoadExits = int(val) roadExitsLabel.SetText(fmt.Sprintf("Road Exits: %d", settings.RoadExits)) } roadExitsSlider.SetValue(float64(settings.RoadExits)) roadCurvynessLabel := widget.NewLabel(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness)) roadCurvynessSlider := widget.NewSlider(0, 100) roadCurvynessSlider.OnChanged = func(val float64) { settings.RoadCurvyness = val roadCurvynessLabel.SetText(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness)) } roadCurvynessSlider.SetValue(settings.RoadCurvyness) roadDistributionLabel := widget.NewLabel(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution)) roadDistributionSlider := widget.NewSlider(0, 100) roadDistributionSlider.OnChanged = func(val float64) { settings.RoadDistribution = val roadDistributionLabel.SetText(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution)) } roadDistributionSlider.SetValue(settings.RoadDistribution) // Create UI elements for error display and action buttons errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255}) errorLabel.TextSize = 12 errorLabel.Hide() var generateBtn *widget.Button progressBar := NewTextOverlayProgressBar() exportCanvasBtn := widget.NewButton("Export Canvas", func() { showSaveDialog(w, canvasImg.Image, settings) }) exportHeightmapBtn := widget.NewButton("Export Heightmap", func() { showSaveDialog(w, heightmapImg.Image, settings) }) exportMasksBtn := widget.NewButton("Export Masks", func() { showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels, buildings) }) // Main generation button and logic generateBtn = widget.NewButton("Generate", func() { go func() { // Disable button during generation fyne.Do(func() { generateBtn.Disable() }) defer func() { // Re-enable button after generation fyne.Do(func() { generateBtn.Enable() }) }() // Set up progress bar steps := 10 currentStep := 0 seedProvider := NewSeedProvider(settings.Seed) // Step 1: Generating Heightmap currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Heightmap", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next()) // Step 2: Generating Lakes currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Lakes", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) var lakeImage image.Image lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next()) // Step 3: Generating Rivers currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Rivers", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) var riverImage image.Image riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg) finalImage := riverImage.(*image.RGBA) var flatLakePixels []image.Point for _, lake := range lakes { flatLakePixels = append(flatLakePixels, lake...) } allWaterPixels := append(flatLakePixels, riverPixels...) // Step 4: Generating Roads currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Roads", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) var roadImage *image.RGBA roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // Draw roadImage over finalImage 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)}) } } } // Step 5: Generating Buildings currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Buildings", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next()) // Step 6: Darkening Water Areas currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Water Areas", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) // Step 7: Flattening Building Areas currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Flattening Building Areas", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height) flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels) // Step 8: Applying Roughness currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Applying Roughness", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness) // Step 9: Generating Trees currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) // Step 10: Finalizing Images currentStep++ fyne.Do(func() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) heightmapImg.Image = compositeImg heightmapImg.Refresh() canvasImg.Image = finalImage canvasImg.Refresh() progressBar.SetText("Generation Complete!") }) }() }) // 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( detailLabel, detailSlider, roughnessLabel, roughnessSlider, widget.NewLabel(""), // Spacer minTreeSizeLabel, minTreeSizeSlider, maxTreeSizeLabel, maxTreeSizeSlider, treeCoverageLabel, treeCoverageSlider, treeClumpinessLabel, treeClumpinessSlider, )) waterTab := container.NewTabItem("Water", container.NewVBox( lakesLabel, lakesSlider, lakeSizeLowerLabel, lakeSizeLowerSlider, lakeSizeUpperLabel, lakeSizeUpperSlider, widget.NewLabel(""), // Spacer riversLabel, riversSlider, minRiverWidthLabel, minRiverWidthSlider, maxRiverWidthLabel, maxRiverWidthSlider, riverCurvynessLabel, riverCurvynessSlider, )) roadDistributionSlider.SetValue(settings.RoadDistribution) roadsTab := container.NewTabItem("Roads", container.NewVBox( numRoadsLabel, numRoadsSlider, minRoadWidthLabel, minRoadWidthSlider, maxRoadWidthLabel, maxRoadWidthSlider, roadExitsLabel, roadExitsSlider, roadCurvynessLabel, roadCurvynessSlider, roadDistributionLabel, roadDistributionSlider, )) // Create UI elements for building generation settings numBuildingsLabel := widget.NewLabel(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings)) numBuildingsSlider := widget.NewSlider(0, 10000) numBuildingsSlider.OnChanged = func(val float64) { settings.NumBuildings = int(val) numBuildingsLabel.SetText(fmt.Sprintf("Number of Buildings: %d", settings.NumBuildings)) } numBuildingsSlider.SetValue(float64(settings.NumBuildings)) minBuildingSizeLabel := widget.NewLabel(fmt.Sprintf("Min Building Size: %.0fpx", settings.MinBuildingSize)) minBuildingSizeSlider := widget.NewSlider(1, 150) maxBuildingSizeLabel := widget.NewLabel(fmt.Sprintf("Max Building Size: %.0fpx", settings.MaxBuildingSize)) maxBuildingSizeSlider := widget.NewSlider(1, 150) minBuildingSizeSlider.OnChanged = func(val float64) { settings.MinBuildingSize = val if settings.MinBuildingSize > settings.MaxBuildingSize { settings.MaxBuildingSize = settings.MinBuildingSize maxBuildingSizeSlider.SetValue(settings.MaxBuildingSize) } minBuildingSizeLabel.SetText(fmt.Sprintf("Min Building Size: %.0fpx", settings.MinBuildingSize)) } minBuildingSizeSlider.SetValue(settings.MinBuildingSize) maxBuildingSizeSlider.OnChanged = func(val float64) { settings.MaxBuildingSize = val if settings.MaxBuildingSize < settings.MinBuildingSize { settings.MinBuildingSize = settings.MaxBuildingSize minBuildingSizeSlider.SetValue(settings.MinBuildingSize) } maxBuildingSizeLabel.SetText(fmt.Sprintf("Max Building Size: %.0fpx", settings.MaxBuildingSize)) } maxBuildingSizeSlider.SetValue(settings.MaxBuildingSize) buildingDistributionLabel := widget.NewLabel(fmt.Sprintf("Building Distribution: %.0f%%", settings.BuildingDistribution)) buildingDistributionSlider := widget.NewSlider(0, 100) buildingDistributionSlider.OnChanged = func(val float64) { settings.BuildingDistribution = val buildingDistributionLabel.SetText(fmt.Sprintf("Building Distribution: %.0f%%", settings.BuildingDistribution)) } buildingDistributionSlider.SetValue(settings.BuildingDistribution) buildingShapeLabel := widget.NewLabel("Building Shape:") buildingShapeSelect := widget.NewSelect([]string{"squares", "circles", "rectangles", "mixed", "procedural"}, func(s string) { settings.BuildingShape = s }) 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, ) // Create sliders for procedural building complexity minBuildingComplexityLabel := widget.NewLabel(fmt.Sprintf("Min Building Complexity: %d", settings.MinBuildingComplexity)) minBuildingComplexitySlider := widget.NewSlider(1, 6) minBuildingComplexitySlider.OnChanged = func(val float64) { settings.MinBuildingComplexity = int(val) if float64(settings.MinBuildingComplexity) > float64(settings.MaxBuildingComplexity) { settings.MaxBuildingComplexity = settings.MinBuildingComplexity } minBuildingComplexityLabel.SetText(fmt.Sprintf("Min Building Complexity: %d", settings.MinBuildingComplexity)) } minBuildingComplexitySlider.SetValue(float64(settings.MinBuildingComplexity)) maxBuildingComplexityLabel := widget.NewLabel(fmt.Sprintf("Max Building Complexity: %d", settings.MaxBuildingComplexity)) maxBuildingComplexitySlider := widget.NewSlider(1, 6) maxBuildingComplexitySlider.OnChanged = func(val float64) { settings.MaxBuildingComplexity = int(val) if float64(settings.MaxBuildingComplexity) < float64(settings.MinBuildingComplexity) { settings.MinBuildingComplexity = settings.MaxBuildingComplexity } maxBuildingComplexityLabel.SetText(fmt.Sprintf("Max Building Complexity: %d", settings.MaxBuildingComplexity)) } maxBuildingComplexitySlider.SetValue(float64(settings.MaxBuildingComplexity)) buildingComplexityRatioLabel := widget.NewLabel(fmt.Sprintf("Building Complexity Ratio: %.0f%%", settings.BuildingComplexityRatio)) buildingComplexityRatioSlider := widget.NewSlider(0, 100) buildingComplexityRatioSlider.OnChanged = func(val float64) { settings.BuildingComplexityRatio = val buildingComplexityRatioLabel.SetText(fmt.Sprintf("Building Complexity Ratio: %.0f%%", settings.BuildingComplexityRatio)) } buildingComplexityRatioSlider.SetValue(settings.BuildingComplexityRatio) proceduralContainer := container.NewVBox( minBuildingComplexityLabel, minBuildingComplexitySlider, maxBuildingComplexityLabel, maxBuildingComplexitySlider, buildingComplexityRatioLabel, 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() } 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() } 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() } 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() } } // 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() } buildingsTab := container.NewTabItem("Buildings", container.NewVBox( numBuildingsLabel, numBuildingsSlider, minBuildingSizeLabel, minBuildingSizeSlider, maxBuildingSizeLabel, maxBuildingSizeSlider, buildingDistributionLabel, buildingDistributionSlider, buildingShapeLabel, buildingShapeSelect, proceduralContainer, ratioContainer, )) imageTab := container.NewTabItem("Image", container.NewVBox( widget.NewLabel("Width:"), widthEntry, widget.NewLabel("Height:"), heightEntry, widget.NewLabel("Seed:"), seedEntry, randomizeBtn, generateBtn, errorLabel, progressBar, widget.NewSeparator(), exportCanvasBtn, exportHeightmapBtn, exportMasksBtn, )) // Create the main layout using a horizontal split tabs := container.NewAppTabs( imageTab, terrainTab, waterTab, roadsTab, buildingsTab, ) left := container.NewVBox( widget.NewLabel("RPG City Maker Reborn"), tabs, ) right := container.NewGridWithRows(2, canvasImg, heightmapImg, ) split := container.NewHSplit( left, right, ) split.SetOffset(0.3) // Set the window content and start the application w.SetContent(split) w.ShowAndRun() } // getImageData encodes an image to the specified format and returns the data as a byte buffer. func getImageData(img image.Image, format string) (*bytes.Buffer, error) { buf := new(bytes.Buffer) var err error 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 } // showMasksSaveDialog displays a dialog for saving the generated masks. func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point, buildings [][]image.Point) { // 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() // Create mask images from the generated data 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}) } roadMask := image.NewGray(bounds) for _, p := range roadPixels { roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } bridgeMask := image.NewGray(bounds) for _, p := range bridgePixels { bridgeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } buildingMask := image.NewGray(bounds) for _, building := range buildings { for _, p := range building { buildingMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) } } imagesToSave := map[string]image.Image{ "canvas." + imgFormat: canvasImg, "heightmap." + imgFormat: heightmapImg, "lakes_mask." + imgFormat: lakeMask, "rivers_mask." + imgFormat: riverMask, "trees_mask." + imgFormat: treeMask, "roads_mask." + imgFormat: roadMask, "bridges_mask." + imgFormat: bridgeMask, "buildings_mask." + imgFormat: buildingMask, } // 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 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() } // 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) } } // 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() }