redid wall generation completely.

This commit is contained in:
grimsace
2026-03-23 10:03:22 -05:00
parent 302a9aaf74
commit bae89fa586
9 changed files with 606 additions and 797 deletions
+2 -2
View File
@@ -34,7 +34,7 @@
"turret_size": 2.5000000000000004, "turret_size": 2.5000000000000004,
"turret_shape": "circular", "turret_shape": "circular",
"turret_spacing": 20, "turret_spacing": 20,
"gate_spacing": 34, "gate_count": 3,
"num_buildings": 1500, "num_buildings": 1500,
"min_building_size": 1, "min_building_size": 1,
"max_building_size": 2.5, "max_building_size": 2.5,
@@ -51,4 +51,4 @@
"seed": 1773254459149268515, "seed": 1773254459149268515,
"last_export_path": "", "last_export_path": "",
"image_view_state": 0 "image_view_state": 0
} }
+2 -2
View File
@@ -34,7 +34,7 @@
"turret_size": 2.5000000000000004, "turret_size": 2.5000000000000004,
"turret_shape": "circular", "turret_shape": "circular",
"turret_spacing": 20, "turret_spacing": 20,
"gate_spacing": 34, "gate_count": 3,
"num_buildings": 50, "num_buildings": 50,
"min_building_size": 2.5, "min_building_size": 2.5,
"max_building_size": 5, "max_building_size": 5,
@@ -51,4 +51,4 @@
"seed": 1773254246832144883, "seed": 1773254246832144883,
"last_export_path": "", "last_export_path": "",
"image_view_state": 0 "image_view_state": 0
} }
+2 -2
View File
@@ -34,7 +34,7 @@
"turret_size": 2.5000000000000004, "turret_size": 2.5000000000000004,
"turret_shape": "circular", "turret_shape": "circular",
"turret_spacing": 20, "turret_spacing": 20,
"gate_spacing": 34, "gate_count": 3,
"num_buildings": 7000, "num_buildings": 7000,
"min_building_size": 0.5, "min_building_size": 0.5,
"max_building_size": 1, "max_building_size": 1,
@@ -51,4 +51,4 @@
"seed": 1773254695515253606, "seed": 1773254695515253606,
"last_export_path": "", "last_export_path": "",
"image_view_state": 0 "image_view_state": 0
} }
+2 -2
View File
@@ -34,7 +34,7 @@
"turret_size": 2.5000000000000004, "turret_size": 2.5000000000000004,
"turret_shape": "circular", "turret_shape": "circular",
"turret_spacing": 20, "turret_spacing": 20,
"gate_spacing": 34, "gate_count": 3,
"num_buildings": 500, "num_buildings": 500,
"min_building_size": 1.5, "min_building_size": 1.5,
"max_building_size": 3.5, "max_building_size": 3.5,
@@ -51,4 +51,4 @@
"seed": 1773254407932680563, "seed": 1773254407932680563,
"last_export_path": "", "last_export_path": "",
"image_view_state": 0 "image_view_state": 0
} }
+2 -2
View File
@@ -34,7 +34,7 @@
"turret_size": 2.5000000000000004, "turret_size": 2.5000000000000004,
"turret_shape": "circular", "turret_shape": "circular",
"turret_spacing": 20, "turret_spacing": 20,
"gate_spacing": 34, "gate_count": 3,
"num_buildings": 10, "num_buildings": 10,
"min_building_size": 2.5, "min_building_size": 2.5,
"max_building_size": 5, "max_building_size": 5,
@@ -51,4 +51,4 @@
"seed": 1773254133615935858, "seed": 1773254133615935858,
"last_export_path": "", "last_export_path": "",
"image_view_state": 0 "image_view_state": 0
} }
+515 -658
View File
File diff suppressed because it is too large Load Diff
+36 -116
View File
@@ -169,23 +169,23 @@ func GenerateRoadsWithPOIs(
if len(roads) == 0 { if len(roads) == 0 {
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
} }
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
if len(roads) == 0 {
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
}
// Add gate roads after wall-crossing rules (so they are never filtered out).
if wallLayout != nil && len(wallLayout.Gates) > 0 { if wallLayout != nil && len(wallLayout.Gates) > 0 {
gateRoads := generateGateRoads(wallLayout, settings, waterMask, width, height, randSrc) gateRoads := generateGateRoads(wallLayout, settings, waterMask, width, height, randSrc)
roads = append(roads, gateRoads...) roads = append(roads, gateRoads...)
roads = ensureGateRoadConnections(gateRoads, roads, wallLayout, settings, waterMask, width, height, randSrc) roads = ensureGateRoadConnections(gateRoads, roads, wallLayout, settings, waterMask, width, height, randSrc)
} }
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
if len(roads) == 0 {
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
}
roads = reduceRepeatedBridges(roads, waterMask, width, height, randSrc) roads = reduceRepeatedBridges(roads, waterMask, width, height, randSrc)
if len(roads) == 0 { if len(roads) == 0 {
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
} }
roads = ensureRoadNetworkConnected(roads, settings, randSrc, waterMask, wallLayout, width, height) roads = ensureRoadNetworkConnected(roads, settings, randSrc, waterMask, wallLayout, width, height)
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
assignRoadWidths(roads, settings, randSrc, width, height, wallLayout) assignRoadWidths(roads, settings, randSrc, width, height, wallLayout)
roadMask := NewPixelMask(width, height) roadMask := NewPixelMask(width, height)
@@ -655,32 +655,6 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
} }
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout) path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
if wallLayout != nil && wallLayout.Mask != nil && len(crossedWallIDs(path, wallLayout)) == 0 {
bestScore := -1.0
bestAnchor := anchor
bestPath := path
for _, cand := range pois {
testPath := calculateRoadPath(cand, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
if len(crossedWallIDs(testPath, wallLayout)) == 0 {
continue
}
d := math.Hypot(float64(cand.X-edgeNode.X), float64(cand.Y-edgeNode.Y))
score := cand.ArterialWeight*2.0 + clamp(1.0-d/2000.0, 0, 1)
if score > bestScore {
bestScore = score
bestAnchor = cand
bestPath = testPath
}
}
anchor = bestAnchor
path = bestPath
}
if wallLayout != nil && wallLayout.Mask != nil && len(wallLayout.Coverages) > 0 {
// Exit roads always use the gate-cheat when walls exist so they are always placeable.
if forced, ok := forcePathThroughWallGate(anchor, edgeNode, wallLayout, waterMask); ok {
path = forced
}
}
anchor.Connections++ anchor.Connections++
edgeNode.IsExit = true edgeNode.IsExit = true
@@ -1081,14 +1055,14 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
curve := clamp(curvyness, 0, 1) curve := clamp(curvyness, 0, 1)
if curve <= 0 { if curve <= 0 {
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, waterMask) return toPathPoints(points, waterMask)
} }
// Non-linear scaling: low values stay fairly straight, high values become very winding. // Non-linear scaling: low values stay fairly straight, high values become very winding.
strength := math.Pow(curve, 1.35) strength := math.Pow(curve, 1.35)
if strength < 0.001 { if strength < 0.001 {
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}}) points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, waterMask) return toPathPoints(points, waterMask)
} }
baseControls := int(math.Max(12, dist/(22.0-14.0*strength))) baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
@@ -1149,7 +1123,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
} }
points := bresenhamRoad(controlPoints) points := bresenhamRoad(controlPoints)
return straightenPathAcrossWalls(toPathPoints(points, waterMask), wallLayout, waterMask) return toPathPoints(points, waterMask)
} }
func toPathPoints(points []image.Point, waterMask *PixelMask) []PathPoint { func toPathPoints(points []image.Point, waterMask *PixelMask) []PathPoint {
@@ -1352,95 +1326,41 @@ func containsWallID(ids []int, wallID int) bool {
} }
func applyWallCrossingRules(roads []*Road, wallLayout *FortificationLayout, waterMask *PixelMask, randSrc *rand.Rand) []*Road { func applyWallCrossingRules(roads []*Road, wallLayout *FortificationLayout, waterMask *PixelMask, randSrc *rand.Rand) []*Road {
if len(roads) == 0 || wallLayout == nil || wallLayout.Mask == nil || len(wallLayout.Coverages) == 0 { if len(roads) == 0 || wallLayout == nil || wallLayout.Mask == nil {
return roads return roads
} }
_ = waterMask
// Straighten each wall crossing segment first. _ = randSrc
for _, road := range roads {
road.Points = straightenPathAcrossWalls(road.Points, wallLayout, waterMask)
}
type roadInfo struct {
road *Road
ids []int
}
infos := make([]roadInfo, 0, len(roads))
for _, road := range roads {
infos = append(infos, roadInfo{road: road, ids: crossedWallIDs(road.Points, wallLayout)})
}
const repeatWallFactor = 0.55
wallCrossCount := make(map[int]int)
requiredWalls := make(map[int]bool)
for i, cov := range wallLayout.Coverages {
if cov < 95 {
requiredWalls[i+1] = true
}
}
keep := make([]bool, len(infos))
for i, info := range infos {
if len(info.ids) == 0 {
keep[i] = true
continue
}
if info.road.Start.IsExit || info.road.End.IsExit {
keep[i] = true
for _, wid := range info.ids {
wallCrossCount[wid]++
}
continue
}
if crossesSameWallMultipleTimes(info.road.Points, wallLayout) {
keep[i] = false
continue
}
keepProb := 1.0
for _, wid := range info.ids {
c := wallCrossCount[wid]
if c > 0 {
keepProb *= math.Pow(repeatWallFactor, float64(c))
}
}
if randSrc.Float64() <= keepProb {
keep[i] = true
for _, wid := range info.ids {
wallCrossCount[wid]++
}
}
}
// Ensure at least one crossing on each wall unless its configured coverage is >= 95%.
for wallID := range requiredWalls {
if wallCrossCount[wallID] > 0 {
continue
}
for i, info := range infos {
if keep[i] {
continue
}
if !containsWallID(info.ids, wallID) {
continue
}
keep[i] = true
for _, wid := range info.ids {
wallCrossCount[wid]++
}
break
}
}
filtered := make([]*Road, 0, len(roads)) filtered := make([]*Road, 0, len(roads))
for i, info := range infos { for _, road := range roads {
if keep[i] { if pathRespectsWallPassages(road.Points, wallLayout.Mask, wallLayout.GateMask) {
filtered = append(filtered, info.road) filtered = append(filtered, road)
} }
} }
return filtered return filtered
} }
func pathRespectsWallPassages(points []PathPoint, exclusionMask, gateMask *PixelMask) bool {
if len(points) == 0 || exclusionMask == nil {
return true
}
for _, pp := range points {
x := pp.Point.X
y := pp.Point.Y
if !exclusionMask.InBounds(x, y) {
continue
}
if !exclusionMask.GetXY(x, y) {
continue
}
if gateMask != nil && gateMask.GetXY(x, y) {
continue
}
return false
}
return true
}
func crossesSameWallMultipleTimes(points []PathPoint, wallLayout *FortificationLayout) bool { func crossesSameWallMultipleTimes(points []PathPoint, wallLayout *FortificationLayout) bool {
if wallLayout == nil || wallLayout.Mask == nil || len(points) < 2 { if wallLayout == nil || wallLayout.Mask == nil || len(points) < 2 {
return false return false
+23 -4
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"math"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -54,7 +55,7 @@ type Settings struct {
TurretSize float64 `json:"turret_size"` TurretSize float64 `json:"turret_size"`
TurretShape string `json:"turret_shape"` TurretShape string `json:"turret_shape"`
TurretSpacing float64 `json:"turret_spacing"` TurretSpacing float64 `json:"turret_spacing"`
GateSpacing float64 `json:"gate_spacing"` // percent of wall circumference between gates (0=no gates) GateCount int `json:"gate_count"` // number of gates on the outer wall before inner-wall halving
// Building settings // Building settings
NumBuildings int `json:"num_buildings"` NumBuildings int `json:"num_buildings"`
@@ -161,7 +162,7 @@ func LoadSettings() (*Settings, error) {
TurretSize: 0.6, TurretSize: 0.6,
TurretShape: "circular", TurretShape: "circular",
TurretSpacing: 55, TurretSpacing: 55,
GateSpacing: 25, GateCount: 3,
NumBuildings: 200, NumBuildings: 200,
MinBuildingSize: 3.5, MinBuildingSize: 3.5,
MaxBuildingSize: 10.0, MaxBuildingSize: 10.0,
@@ -264,8 +265,20 @@ func normalizeSettings(settings *Settings, rawKeys map[string]json.RawMessage) {
if _, ok := rawKeys["turret_spacing"]; !ok { if _, ok := rawKeys["turret_spacing"]; !ok {
settings.TurretSpacing = 55 settings.TurretSpacing = 55
} }
if _, ok := rawKeys["gate_spacing"]; !ok { if _, ok := rawKeys["gate_count"]; !ok {
settings.GateSpacing = 25 if legacyRaw, legacy := rawKeys["gate_spacing"]; legacy && settings.GateCount == 0 {
var legacySpacing float64
if err := json.Unmarshal(legacyRaw, &legacySpacing); err == nil {
legacyCount := int(math.Round(100.0 / clamp(legacySpacing, 1, 100)))
if legacyCount < 1 {
legacyCount = 1
}
settings.GateCount = legacyCount
}
}
if settings.GateCount == 0 {
settings.GateCount = 3
}
} }
// Wall widths are percentages of average image dimension. // Wall widths are percentages of average image dimension.
@@ -289,6 +302,12 @@ func normalizeSettings(settings *Settings, rawKeys map[string]json.RawMessage) {
settings.WallCurvyness = clamp(settings.WallCurvyness, 0, 100) settings.WallCurvyness = clamp(settings.WallCurvyness, 0, 100)
settings.TurretSize = snapTurretSizePercent(settings.TurretSize) settings.TurretSize = snapTurretSizePercent(settings.TurretSize)
settings.TurretSpacing = clamp(settings.TurretSpacing, 0, 100) settings.TurretSpacing = clamp(settings.TurretSpacing, 0, 100)
if settings.GateCount < 1 {
settings.GateCount = 1
}
if settings.GateCount > 20 {
settings.GateCount = 20
}
// Tree sizes are percentages of average image dimension. // Tree sizes are percentages of average image dimension.
// Migrate older pixel-based values when they exceed the valid percentage range. // Migrate older pixel-based values when they exceed the valid percentage range.
+22 -9
View File
@@ -12,6 +12,7 @@ import (
"image/png" "image/png"
"io" "io"
"log" "log"
"math"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -322,6 +323,12 @@ func main() {
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
}); ok { }); ok {
treeMask = out treeMask = out
if wallMask != nil {
drawWallMask(treeBase, wallMask)
}
if turretMask != nil {
drawTurretMask(treeBase, turretMask)
}
finalImage = treeBase finalImage = treeBase
} else { } else {
log.Println("GenerateTrees timed out after 1 minute; continuing.") log.Println("GenerateTrees timed out after 1 minute; continuing.")
@@ -789,16 +796,16 @@ func main() {
markPresetDirty() markPresetDirty()
})) }))
gateSpacingSlider := newNumericInputSlider(0, 100, settings.GateSpacing, "%.0f%%", "Gate Spacing") gateCountSlider := newNumericInputSliderWithStep(1, 20, float64(settings.GateCount), 1, "%.0f", "Gate Count")
gateSpacingSlider.entry.OnChanged = func(s string) { gateCountSlider.entry.OnChanged = func(s string) {
gateSpacingSlider.validate(s, func(hasError bool) { gateCountSlider.validate(s, func(hasError bool) {
errorStates["gateSpacing"] = hasError errorStates["gateCount"] = hasError
updateGenerateBtnState() updateGenerateBtnState()
}) })
} }
gateSpacingSlider.value.AddListener(binding.NewDataListener(func() { gateCountSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := gateSpacingSlider.value.Get() val, _ := gateCountSlider.value.Get()
settings.GateSpacing = val settings.GateCount = int(math.Round(val))
markPresetDirty() markPresetDirty()
})) }))
@@ -1111,6 +1118,12 @@ func main() {
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next()) return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
}); ok { }); ok {
treeMask = out treeMask = out
if wallMask != nil {
drawWallMask(treeBase, wallMask)
}
if turretMask != nil {
drawTurretMask(treeBase, turretMask)
}
finalImage = treeBase finalImage = treeBase
} else { } else {
log.Println("GenerateTrees timed out after 1 minute; continuing.") log.Println("GenerateTrees timed out after 1 minute; continuing.")
@@ -1240,7 +1253,7 @@ func main() {
numWallsSlider, numWallsSlider,
cityCoverageSlider, cityCoverageSlider,
wallCurvynessSlider, wallCurvynessSlider,
gateSpacingSlider, gateCountSlider,
showTurretsCheck, showTurretsCheck,
turretControls, turretControls,
)) ))
@@ -1553,7 +1566,7 @@ func main() {
numWallsSlider.value.Set(float64(settings.NumWalls)) numWallsSlider.value.Set(float64(settings.NumWalls))
cityCoverageSlider.value.Set(settings.CityCoverage) cityCoverageSlider.value.Set(settings.CityCoverage)
wallCurvynessSlider.value.Set(settings.WallCurvyness) wallCurvynessSlider.value.Set(settings.WallCurvyness)
gateSpacingSlider.value.Set(settings.GateSpacing) gateCountSlider.value.Set(float64(settings.GateCount))
showTurretsCheck.SetChecked(settings.ShowTurrets) showTurretsCheck.SetChecked(settings.ShowTurrets)
turretSizeSlider.value.Set(settings.TurretSize) turretSizeSlider.value.Set(settings.TurretSize)
turretShapeSelect.SetSelected(settings.TurretShape) turretShapeSelect.SetSelected(settings.TurretShape)