Files
RPG_City_Maker_Reborn/main.go
T

894 lines
27 KiB
Go
Raw Normal View History

package main
import (
2026-02-02 13:26:28 -06:00
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"image"
"image/color"
2026-02-02 13:26:28 -06:00
"image/jpeg"
"image/png"
2026-02-02 13:26:28 -06:00
"log"
"os"
"path/filepath"
2026-02-02 13:26:28 -06:00
"strconv"
2026-02-02 12:24:14 -06:00
"strings"
2026-02-02 13:26:28 -06:00
"time"
2026-02-02 12:24:14 -06:00
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
2026-02-02 12:24:14 -06:00
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
2026-02-02 12:24:14 -06:00
"github.com/chai2010/webp"
)
func main() {
2026-02-02 12:24:14 -06:00
a := app.NewWithID("com.example.rpgcitymaker")
2026-01-28 12:35:10 -06:00
a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()})
w := a.NewWindow("RPG City Maker")
w.Resize(fyne.NewSize(800, 600))
settings, err := LoadSettings()
if err != nil {
log.Println("Error loading settings:", err)
// Use default settings if loading fails
2026-01-27 12:00:19 -06:00
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
}
canvasImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
heightmapImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
2026-02-02 13:26:28 -06:00
var lakes [][]image.Point
var riverPixels []image.Point
var treePixels []image.Point
2026-02-04 14:19:18 -06:00
var roadPixels []image.Point
2026-02-02 13:26:28 -06:00
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Failed to get user config dir:", err)
}
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
canvasPath := filepath.Join(appConfigDir, "canvas.png")
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
w.SetOnClosed(func() {
if err := settings.Save(); err != nil {
log.Println("Error saving settings:", err)
}
if canvasImg.Image != nil {
canvasFile, err := os.Create(canvasPath)
if err != nil {
log.Println("Failed to create canvas file:", err)
} else {
defer canvasFile.Close()
if err := png.Encode(canvasFile, canvasImg.Image); err != nil {
log.Println("Failed to encode canvas image:", err)
}
}
}
if heightmapImg.Image != nil {
heightmapFile, err := os.Create(heightmapPath)
if err != nil {
log.Println("Failed to create heightmap file:", err)
} else {
defer heightmapFile.Close()
if err := png.Encode(heightmapFile, heightmapImg.Image); err != nil {
log.Println("Failed to encode heightmap image:", err)
}
}
}
})
canvasFile, err := os.Open(canvasPath)
if err == nil {
defer canvasFile.Close()
img, err := png.Decode(canvasFile)
if err == nil {
canvasImg.Image = img
}
}
heightmapFile, err := os.Open(heightmapPath)
if err == nil {
defer heightmapFile.Close()
img, err := png.Decode(heightmapFile)
if err == nil {
heightmapImg.Image = img
}
}
if canvasImg.Image == nil || heightmapImg.Image == nil {
2026-02-04 14:19:18 -06:00
// Initial image generation
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider(settings.Seed)
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-02-02 13:26:28 -06:00
var lakeImage image.Image
2026-02-04 14:19:18 -06:00
2026-02-04 15:15:47 -06:00
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-02-02 13:26:28 -06:00
var riverImage image.Image
2026-02-04 14:19:18 -06:00
2026-02-04 15:15:47 -06:00
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
2026-02-04 14:19:18 -06:00
2026-01-30 10:38:00 -06:00
finalImage := riverImage.(*image.RGBA)
2026-01-30 10:55:15 -06:00
2026-02-04 14:19:18 -06:00
var roadImage *image.RGBA
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
2026-01-30 10:55:15 -06:00
allWaterPixels := append(flatLakePixels, riverPixels...)
2026-02-04 15:15:47 -06:00
roadPixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next())
2026-02-04 14:50:36 -06:00
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
2026-01-30 10:55:15 -06:00
2026-02-04 15:15:47 -06:00
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
2026-02-04 14:19:18 -06:00
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
2026-02-04 14:19:18 -06:00
2026-02-04 16:13:06 -06:00
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
2026-02-04 14:19:18 -06:00
heightmapImg.Image = compositeImg
2026-02-04 14:19:18 -06:00
canvasImg.Image = finalImage
2026-02-04 14:19:18 -06:00
}
detailLabel := widget.NewLabel(fmt.Sprintf("Detail: %.0f", settings.Detail))
detailSlider := widget.NewSlider(1, 16)
detailSlider.OnChanged = func(val float64) {
settings.Detail = val
detailLabel.SetText(fmt.Sprintf("Detail: %.0f", settings.Detail))
}
detailSlider.SetValue(settings.Detail)
roughnessLabel := widget.NewLabel(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
roughnessSlider := widget.NewSlider(0, 100)
roughnessSlider.OnChanged = func(val float64) {
settings.Roughness = val
roughnessLabel.SetText(fmt.Sprintf("Roughness: %.0f%%", settings.Roughness))
}
roughnessSlider.SetValue(settings.Roughness)
lakesLabel := widget.NewLabel(fmt.Sprintf("Lakes: %d", settings.Lakes))
lakesSlider := widget.NewSlider(0, 15)
lakesSlider.OnChanged = func(val float64) {
settings.Lakes = int(val)
lakesLabel.SetText(fmt.Sprintf("Lakes: %d", settings.Lakes))
}
lakesSlider.SetValue(float64(settings.Lakes))
2026-01-27 12:00:19 -06:00
lakeSizeLowerLabel := widget.NewLabel(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower))
lakeSizeLowerSlider := widget.NewSlider(1, 100)
lakeSizeUpperLabel := widget.NewLabel(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
lakeSizeUpperSlider := widget.NewSlider(1, 100)
lakeSizeLowerSlider.OnChanged = func(val float64) {
settings.LakeSizeLower = val
if settings.LakeSizeLower > settings.LakeSizeUpper {
settings.LakeSizeUpper = settings.LakeSizeLower
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
}
lakeSizeLowerLabel.SetText(fmt.Sprintf("Min Lake Size: %.0f%%", settings.LakeSizeLower))
}
2026-01-27 12:00:19 -06:00
lakeSizeLowerSlider.SetValue(settings.LakeSizeLower)
lakeSizeUpperSlider.OnChanged = func(val float64) {
settings.LakeSizeUpper = val
if settings.LakeSizeUpper < settings.LakeSizeLower {
settings.LakeSizeLower = settings.LakeSizeUpper
lakeSizeLowerSlider.SetValue(settings.LakeSizeLower)
}
lakeSizeUpperLabel.SetText(fmt.Sprintf("Max Lake Size: %.0f%%", settings.LakeSizeUpper))
}
lakeSizeUpperSlider.SetValue(settings.LakeSizeUpper)
2026-01-30 10:38:00 -06:00
riversLabel := widget.NewLabel(fmt.Sprintf("Rivers: %d", settings.Rivers))
riversSlider := widget.NewSlider(0, 5)
riversSlider.OnChanged = func(val float64) {
settings.Rivers = int(val)
riversLabel.SetText(fmt.Sprintf("Rivers: %d", settings.Rivers))
}
riversSlider.SetValue(float64(settings.Rivers))
minRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth))
minRiverWidthSlider := widget.NewSlider(1, 100)
maxRiverWidthLabel := widget.NewLabel(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth))
maxRiverWidthSlider := widget.NewSlider(1, 100)
minRiverWidthSlider.OnChanged = func(val float64) {
settings.MinRiverWidth = val
if settings.MinRiverWidth > settings.MaxRiverWidth {
settings.MaxRiverWidth = settings.MinRiverWidth
maxRiverWidthSlider.SetValue(settings.MaxRiverWidth)
}
minRiverWidthLabel.SetText(fmt.Sprintf("Min River Width: %.0f%%", settings.MinRiverWidth))
}
minRiverWidthSlider.SetValue(settings.MinRiverWidth)
maxRiverWidthSlider.OnChanged = func(val float64) {
settings.MaxRiverWidth = val
if settings.MaxRiverWidth < settings.MinRiverWidth {
settings.MinRiverWidth = settings.MaxRiverWidth
minRiverWidthSlider.SetValue(settings.MinRiverWidth)
}
maxRiverWidthLabel.SetText(fmt.Sprintf("Max River Width: %.0f%%", settings.MaxRiverWidth))
}
maxRiverWidthSlider.SetValue(settings.MaxRiverWidth)
riverCurvynessLabel := widget.NewLabel(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
riverCurvynessSlider := widget.NewSlider(0, 100)
riverCurvynessSlider.OnChanged = func(val float64) {
settings.RiverCurvyness = val
riverCurvynessLabel.SetText(fmt.Sprintf("River Curvyness: %.0f%%", settings.RiverCurvyness))
}
riverCurvynessSlider.SetValue(settings.RiverCurvyness)
minTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
minTreeSizeSlider := widget.NewSlider(1, 150)
maxTreeSizeLabel := widget.NewLabel(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
maxTreeSizeSlider := widget.NewSlider(1, 150)
minTreeSizeSlider.OnChanged = func(val float64) {
settings.MinTreeSize = val
if settings.MinTreeSize > settings.MaxTreeSize {
settings.MaxTreeSize = settings.MinTreeSize
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
}
minTreeSizeLabel.SetText(fmt.Sprintf("Min Tree Size: %.0fpx", settings.MinTreeSize))
}
minTreeSizeSlider.SetValue(settings.MinTreeSize)
maxTreeSizeSlider.OnChanged = func(val float64) {
settings.MaxTreeSize = val
if settings.MaxTreeSize < settings.MinTreeSize {
settings.MinTreeSize = settings.MaxTreeSize
minTreeSizeSlider.SetValue(settings.MinTreeSize)
}
maxTreeSizeLabel.SetText(fmt.Sprintf("Max Tree Size: %.0fpx", settings.MaxTreeSize))
}
maxTreeSizeSlider.SetValue(settings.MaxTreeSize)
treeCoverageLabel := widget.NewLabel(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
treeCoverageSlider := widget.NewSlider(1, 100)
treeCoverageSlider.OnChanged = func(val float64) {
settings.TreeCoverage = val
treeCoverageLabel.SetText(fmt.Sprintf("Tree Coverage: %.0f%%", settings.TreeCoverage))
}
treeCoverageSlider.SetValue(settings.TreeCoverage)
treeClumpinessLabel := widget.NewLabel(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
treeClumpinessSlider := widget.NewSlider(0, 100)
treeClumpinessSlider.OnChanged = func(val float64) {
settings.TreeClumpiness = val
treeClumpinessLabel.SetText(fmt.Sprintf("Tree Clumpiness: %.0f%%", settings.TreeClumpiness))
}
treeClumpinessSlider.SetValue(settings.TreeClumpiness)
2026-02-04 14:19:18 -06:00
numRoadsLabel := widget.NewLabel(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
2026-02-04 14:50:36 -06:00
numRoadsSlider := widget.NewSlider(0, 1000)
2026-02-04 14:19:18 -06:00
numRoadsSlider.OnChanged = func(val float64) {
settings.NumRoads = int(val)
numRoadsLabel.SetText(fmt.Sprintf("Number of Roads: %d", settings.NumRoads))
}
numRoadsSlider.SetValue(float64(settings.NumRoads))
minRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth))
minRoadWidthSlider := widget.NewSlider(1, 150)
maxRoadWidthLabel := widget.NewLabel(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth))
maxRoadWidthSlider := widget.NewSlider(1, 150)
minRoadWidthSlider.OnChanged = func(val float64) {
settings.MinRoadWidth = val
if settings.MinRoadWidth > settings.MaxRoadWidth {
settings.MaxRoadWidth = settings.MinRoadWidth
maxRoadWidthSlider.SetValue(settings.MaxRoadWidth)
}
minRoadWidthLabel.SetText(fmt.Sprintf("Min Road Width: %.0fpx", settings.MinRoadWidth))
}
minRoadWidthSlider.SetValue(settings.MinRoadWidth)
maxRoadWidthSlider.OnChanged = func(val float64) {
settings.MaxRoadWidth = val
if settings.MaxRoadWidth < settings.MinRoadWidth {
settings.MinRoadWidth = settings.MaxRoadWidth
minRoadWidthSlider.SetValue(settings.MinRoadWidth)
}
maxRoadWidthLabel.SetText(fmt.Sprintf("Max Road Width: %.0fpx", settings.MaxRoadWidth))
}
maxRoadWidthSlider.SetValue(settings.MaxRoadWidth)
roadExitsLabel := widget.NewLabel(fmt.Sprintf("Road Exits: %d", settings.RoadExits))
2026-02-04 14:50:36 -06:00
roadExitsSlider := widget.NewSlider(0, 100)
2026-02-04 14:19:18 -06:00
roadExitsSlider.OnChanged = func(val float64) {
if val > float64(settings.NumRoads) {
val = float64(settings.NumRoads)
roadExitsSlider.SetValue(val)
}
settings.RoadExits = int(val)
roadExitsLabel.SetText(fmt.Sprintf("Road Exits: %d", settings.RoadExits))
}
roadExitsSlider.SetValue(float64(settings.RoadExits))
roadCurvynessLabel := widget.NewLabel(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness))
roadCurvynessSlider := widget.NewSlider(0, 100)
roadCurvynessSlider.OnChanged = func(val float64) {
settings.RoadCurvyness = val
roadCurvynessLabel.SetText(fmt.Sprintf("Road Curvyness: %.0f%%", settings.RoadCurvyness))
}
roadCurvynessSlider.SetValue(settings.RoadCurvyness)
roadDistributionLabel := widget.NewLabel(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution))
roadDistributionSlider := widget.NewSlider(0, 100)
roadDistributionSlider.OnChanged = func(val float64) {
settings.RoadDistribution = val
roadDistributionLabel.SetText(fmt.Sprintf("Distribution: %.0f%%", settings.RoadDistribution))
}
roadDistributionSlider.SetValue(settings.RoadDistribution)
errorLabel := canvas.NewText("", color.RGBA{R: 255, A: 255})
errorLabel.TextSize = 12
errorLabel.Hide()
2026-01-28 12:35:10 -06:00
var generateBtn *widget.Button
progressBar := NewTextOverlayProgressBar()
2026-02-02 12:40:25 -06:00
exportCanvasBtn := widget.NewButton("Export Canvas", func() {
2026-02-02 12:24:14 -06:00
showSaveDialog(w, canvasImg.Image, settings)
})
2026-02-02 12:40:25 -06:00
exportHeightmapBtn := widget.NewButton("Export Heightmap", func() {
showSaveDialog(w, heightmapImg.Image, settings)
})
2026-02-02 13:26:28 -06:00
exportMasksBtn := widget.NewButton("Export Masks", func() {
2026-02-04 14:19:18 -06:00
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels)
2026-02-02 13:26:28 -06:00
})
2026-01-28 12:35:10 -06:00
generateBtn = widget.NewButton("Generate", func() {
go func() {
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
generateBtn.Disable()
})
defer func() {
fyne.Do(func() {
generateBtn.Enable()
})
}()
2026-01-28 12:35:10 -06:00
2026-02-04 14:19:18 -06:00
steps := 8
2026-01-28 12:35:10 -06:00
currentStep := 0
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider(settings.Seed)
2026-01-28 12:35:10 -06:00
// Step 1: Generating Heightmap
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Heightmap", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-04 15:15:47 -06:00
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
2026-01-28 12:35:10 -06:00
// Step 2: Generating Lakes
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Lakes", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-02 13:26:28 -06:00
var lakeImage image.Image
2026-02-04 15:15:47 -06:00
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, seedProvider.Next())
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
// Step 3: Generating Rivers
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Rivers", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-02 13:26:28 -06:00
var riverImage image.Image
2026-02-04 15:15:47 -06:00
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg)
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
finalImage := riverImage.(*image.RGBA)
2026-02-04 14:19:18 -06:00
2026-02-04 14:50:36 -06:00
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
allWaterPixels := append(flatLakePixels, riverPixels...)
2026-02-04 14:19:18 -06:00
// Step 4: Generating Roads
currentStep++
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Roads", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
var roadImage *image.RGBA
2026-02-04 15:15:47 -06:00
roadPixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // Draw roadImage over finalImage
2026-02-04 14:19:18 -06:00
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
// Step 5: Darkening Water Areas
2026-01-30 10:38:00 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Darkening Water Areas", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-01-30 10:38:00 -06:00
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
2026-02-04 16:13:06 -06:00
flattenedHeightmap := FlattenRoadAreas(darkenedHeightmap, roadPixels)
2026-01-30 10:38:00 -06:00
2026-02-04 14:19:18 -06:00
// Step 6: Applying Roughness
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Applying Roughness", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
2026-01-28 12:35:10 -06:00
2026-02-04 14:19:18 -06:00
// Step 7: Generating Trees
2026-01-28 13:06:01 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
})
2026-02-04 15:15:47 -06:00
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
2026-01-28 13:06:01 -06:00
2026-02-04 14:19:18 -06:00
// Step 8: Finalizing Images
2026-01-28 12:35:10 -06:00
currentStep++
2026-02-02 10:54:09 -06:00
fyne.Do(func() {
progressBar.SetText(fmt.Sprintf("Step %d/%d: Finalizing Images", currentStep, steps))
progressBar.SetValue(float64(currentStep) / float64(steps))
heightmapImg.Image = compositeImg
heightmapImg.Refresh()
canvasImg.Image = finalImage
canvasImg.Refresh()
2026-01-28 12:35:10 -06:00
2026-02-02 10:54:09 -06:00
progressBar.SetText("Generation Complete!")
})
2026-01-28 12:35:10 -06:00
}()
})
widthEntry := widget.NewEntry()
widthEntry.SetText(strconv.Itoa(settings.Width))
widthEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Width must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Width = val
}
heightEntry := widget.NewEntry()
heightEntry.SetText(strconv.Itoa(settings.Height))
heightEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Height must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Height = val
}
seedEntry := widget.NewEntry()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
seedEntry.OnChanged = func(s string) {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
errorLabel.Text = "Seed must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Seed = val
}
randomizeBtn := widget.NewButton("Randomize", func() {
settings.Seed = time.Now().UnixNano()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
})
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
detailLabel,
detailSlider,
roughnessLabel,
roughnessSlider,
2026-02-02 10:15:26 -06:00
widget.NewLabel(""), // Spacer
minTreeSizeLabel,
minTreeSizeSlider,
maxTreeSizeLabel,
maxTreeSizeSlider,
treeCoverageLabel,
treeCoverageSlider,
treeClumpinessLabel,
treeClumpinessSlider,
))
waterTab := container.NewTabItem("Water", container.NewVBox(
lakesLabel,
lakesSlider,
2026-01-27 12:00:19 -06:00
lakeSizeLowerLabel,
lakeSizeLowerSlider,
lakeSizeUpperLabel,
lakeSizeUpperSlider,
2026-01-30 10:38:00 -06:00
widget.NewLabel(""), // Spacer
riversLabel,
riversSlider,
minRiverWidthLabel,
minRiverWidthSlider,
maxRiverWidthLabel,
maxRiverWidthSlider,
riverCurvynessLabel,
riverCurvynessSlider,
))
2026-02-04 14:19:18 -06:00
roadDistributionSlider.SetValue(settings.RoadDistribution)
roadsTab := container.NewTabItem("Roads", container.NewVBox(
numRoadsLabel,
numRoadsSlider,
minRoadWidthLabel,
minRoadWidthSlider,
maxRoadWidthLabel,
maxRoadWidthSlider,
roadExitsLabel,
roadExitsSlider,
roadCurvynessLabel,
roadCurvynessSlider,
roadDistributionLabel,
roadDistributionSlider,
))
imageTab := container.NewTabItem("Image", container.NewVBox(
widget.NewLabel("Width:"),
2026-02-04 14:19:18 -06:00
widthEntry, widget.NewLabel("Height:"),
heightEntry,
widget.NewLabel("Seed:"),
seedEntry,
randomizeBtn,
generateBtn,
errorLabel,
2026-01-28 12:35:10 -06:00
progressBar,
2026-02-02 12:24:14 -06:00
widget.NewSeparator(),
2026-02-02 12:40:25 -06:00
exportCanvasBtn,
exportHeightmapBtn,
2026-02-02 13:26:28 -06:00
exportMasksBtn,
))
tabs := container.NewAppTabs(
imageTab,
terrainTab,
2026-02-02 10:15:26 -06:00
waterTab,
2026-02-04 14:19:18 -06:00
roadsTab,
)
left := container.NewVBox(
2026-02-02 10:15:26 -06:00
widget.NewLabel("RPG City Maker Reborn"),
tabs,
)
right := container.NewGridWithRows(2,
canvasImg,
heightmapImg,
)
split := container.NewHSplit(
left,
right,
)
split.SetOffset(0.3)
w.SetContent(split)
w.ShowAndRun()
}
2026-02-02 13:26:28 -06:00
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
var err error
switch format {
case "PNG":
err = png.Encode(buf, img)
case "JPG":
err = jpeg.Encode(buf, img, nil)
case "WEBP":
err = webp.Encode(buf, img, &webp.Options{Lossless: true})
}
return buf, err
}
2026-02-04 14:19:18 -06:00
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels []image.Point) {
2026-02-02 13:26:28 -06:00
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("masks_folder")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
packageSelect := widget.NewSelect([]string{"Folder", "tar.gz", "zip"}, nil)
packageSelect.SetSelected("Folder")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Folder Name:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
widget.NewLabel("Packaging:"),
packageSelect,
)
saveDialog := dialog.NewCustom("Export Masks", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
folderName := fileNameEntry.Text
if folderName == "" {
return
}
imgFormat := strings.ToLower(formatSelect.Selected)
bounds := canvasImg.Bounds()
// Create mask images
lakeMask := image.NewGray(bounds)
for _, lake := range lakes {
for _, p := range lake {
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
riverMask := image.NewGray(bounds)
for _, p := range riverPixels {
riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
treeMask := image.NewGray(bounds)
for _, p := range treePixels {
treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-04 14:19:18 -06:00
roadMask := image.NewGray(bounds)
for _, p := range roadPixels {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
2026-02-02 13:26:28 -06:00
imagesToSave := map[string]image.Image{
"canvas." + imgFormat: canvasImg,
"heightmap." + imgFormat: heightmapImg,
"lakes_mask." + imgFormat: lakeMask,
"rivers_mask." + imgFormat: riverMask,
"trees_mask." + imgFormat: treeMask,
2026-02-04 14:19:18 -06:00
"roads_mask." + imgFormat: roadMask,
2026-02-02 13:26:28 -06:00
}
switch packageSelect.Selected {
case "Folder":
exportPath := filepath.Join(pathLabel.Text, folderName)
if err := os.MkdirAll(exportPath, 0755); err != nil {
log.Println("Error creating directory:", err)
return
}
settings.LastExportPath = exportPath
for name, img := range imagesToSave {
saveImage(img, filepath.Join(exportPath, name))
}
case "tar.gz":
filePath := filepath.Join(pathLabel.Text, folderName+".tar.gz")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
gw := gzip.NewWriter(file)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
hdr := &tar.Header{
Name: name,
Mode: 0644,
Size: int64(buf.Len()),
}
if err := tw.WriteHeader(hdr); err != nil {
log.Printf("Error writing tar header for %s: %v\n", name, err)
continue
}
if _, err := tw.Write(buf.Bytes()); err != nil {
log.Printf("Error writing tar data for %s: %v\n", name, err)
}
}
case "zip":
filePath := filepath.Join(pathLabel.Text, folderName+".zip")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
zw := zip.NewWriter(file)
defer zw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
f, err := zw.Create(name)
if err != nil {
log.Printf("Error creating zip entry for %s: %v\n", name, err)
continue
}
if _, err := f.Write(buf.Bytes()); err != nil {
log.Printf("Error writing zip data for %s: %v\n", name, err)
}
}
}
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}
func saveImage(img image.Image, path string) {
file, err := os.Create(path)
if err != nil {
log.Println("Error creating file:", err)
return
}
defer file.Close()
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".png":
err = png.Encode(file, img)
case ".jpg":
err = jpeg.Encode(file, img, nil)
case ".webp":
err = webp.Encode(file, img, &webp.Options{Lossless: true})
}
if err != nil {
log.Println("Failed to encode image:", err)
}
}
2026-02-02 12:24:14 -06:00
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("image")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Filename:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
)
saveDialog := dialog.NewCustom("Export Image", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
fileName := fileNameEntry.Text
if fileName == "" {
return
}
filePath := filepath.Join(pathLabel.Text, fileName+"."+strings.ToLower(formatSelect.Selected))
settings.LastExportPath = filepath.Dir(filePath)
2026-02-02 13:26:28 -06:00
saveImage(img, filePath)
2026-02-02 12:24:14 -06:00
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}