changed added basic turret support
This commit is contained in:
@@ -12,6 +12,10 @@ const (
|
|||||||
minWallWidthPercent = minBuildingSizePercent
|
minWallWidthPercent = minBuildingSizePercent
|
||||||
maxWallWidthPercent = maxBuildingSizePercent
|
maxWallWidthPercent = maxBuildingSizePercent
|
||||||
wallWidthPercentStep = buildingSizePercentStep
|
wallWidthPercentStep = buildingSizePercentStep
|
||||||
|
|
||||||
|
minTurretSizePercent = 0.2
|
||||||
|
maxTurretSizePercent = maxWallWidthPercent
|
||||||
|
turretSizePercentStep = 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
func clampWallWidthPercent(v float64) float64 {
|
func clampWallWidthPercent(v float64) float64 {
|
||||||
@@ -56,6 +60,35 @@ func getWallWidthRangePixels(settings *Settings, width, height int) (float64, fl
|
|||||||
return minPx, maxPx
|
return minPx, maxPx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clampTurretSizePercent(v float64) float64 {
|
||||||
|
if v < minTurretSizePercent {
|
||||||
|
return minTurretSizePercent
|
||||||
|
}
|
||||||
|
if v > maxTurretSizePercent {
|
||||||
|
return maxTurretSizePercent
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapTurretSizePercent(v float64) float64 {
|
||||||
|
v = clampTurretSizePercent(v)
|
||||||
|
steps := math.Round((v - minTurretSizePercent) / turretSizePercentStep)
|
||||||
|
return clampTurretSizePercent(minTurretSizePercent + steps*turretSizePercentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTurretSizePixels(settings *Settings, width, height int) float64 {
|
||||||
|
sizePercent := snapTurretSizePercent(settings.TurretSize)
|
||||||
|
avgDim := averageImageDimension(width, height)
|
||||||
|
if avgDim < 1 {
|
||||||
|
avgDim = 1
|
||||||
|
}
|
||||||
|
sizePx := (sizePercent / 100.0) * avgDim
|
||||||
|
if sizePx < 1 {
|
||||||
|
sizePx = 1
|
||||||
|
}
|
||||||
|
return sizePx
|
||||||
|
}
|
||||||
|
|
||||||
type FortificationLayout struct {
|
type FortificationLayout struct {
|
||||||
Mask *PixelMask
|
Mask *PixelMask
|
||||||
WallIDByPixel []int
|
WallIDByPixel []int
|
||||||
@@ -360,3 +393,324 @@ func drawWallMask(img *image.RGBA, wallMask *PixelMask) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateTurrets(
|
||||||
|
img *image.RGBA,
|
||||||
|
width, height int,
|
||||||
|
settings *Settings,
|
||||||
|
layout *FortificationLayout,
|
||||||
|
waterMask, roadMask *PixelMask,
|
||||||
|
roads []*Road,
|
||||||
|
) *PixelMask {
|
||||||
|
mask := NewPixelMask(width, height)
|
||||||
|
if !settings.ShowTurrets || layout == nil || layout.Mask == nil || len(layout.WallIDByPixel) != width*height {
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
if img == nil {
|
||||||
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
}
|
||||||
|
if waterMask == nil {
|
||||||
|
waterMask = NewPixelMask(width, height)
|
||||||
|
}
|
||||||
|
if roadMask == nil {
|
||||||
|
roadMask = NewPixelMask(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
sizePx := getTurretSizePixels(settings, width, height)
|
||||||
|
radius := int(math.Round(sizePx / 2.0))
|
||||||
|
if radius < 1 {
|
||||||
|
radius = 1
|
||||||
|
}
|
||||||
|
shape := settings.TurretShape
|
||||||
|
if shape != "square" {
|
||||||
|
shape = "circular"
|
||||||
|
}
|
||||||
|
colorRed := color.RGBA{R: 220, G: 25, B: 25, A: 255}
|
||||||
|
|
||||||
|
wallPoints := make(map[int][]image.Point)
|
||||||
|
waterMeetPoints := make(map[int][]image.Point)
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
row := y * width
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
wid := layout.WallIDByPixel[row+x]
|
||||||
|
if wid <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isBoundaryWallPixel(x, y, layout.Mask) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := image.Point{X: x, Y: y}
|
||||||
|
wallPoints[wid] = append(wallPoints[wid], p)
|
||||||
|
if touchesWater(x, y, waterMask) {
|
||||||
|
waterMeetPoints[wid] = append(waterMeetPoints[wid], p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occupied := make(map[int]bool)
|
||||||
|
addTurret := func(center image.Point) {
|
||||||
|
snapped, ok := snapPointToWall(center, layout.Mask, max(3, radius*4))
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if nearbyTurretExists(mask, snapped, max(2, radius)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := snapped.Y*width + snapped.X
|
||||||
|
if occupied[key] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
occupied[key] = true
|
||||||
|
drawTurret(img, mask, snapped, radius, shape, colorRed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base spacing turrets along each wall ring.
|
||||||
|
for wid, pts := range wallPoints {
|
||||||
|
if len(pts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
centroid := averagePoint(pts)
|
||||||
|
sort.Slice(pts, func(i, j int) bool {
|
||||||
|
ai := math.Atan2(float64(pts[i].Y-centroid.Y), float64(pts[i].X-centroid.X))
|
||||||
|
aj := math.Atan2(float64(pts[j].Y-centroid.Y), float64(pts[j].X-centroid.X))
|
||||||
|
return ai < aj
|
||||||
|
})
|
||||||
|
// Spacing is "distance along wall as % of wall circumference", independent of turret size.
|
||||||
|
spacingPct := clamp(settings.TurretSpacing, 0, 100)
|
||||||
|
step := int(math.Round((spacingPct / 100.0) * float64(len(pts))))
|
||||||
|
if step < 1 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
if step > len(pts) {
|
||||||
|
step = len(pts)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pts); i += step {
|
||||||
|
addTurret(pts[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always place turrets where wall meets water.
|
||||||
|
for _, p := range waterMeetPoints[wid] {
|
||||||
|
addTurret(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gate turrets: one on each side of each road crossing, spacing = 3x road width.
|
||||||
|
for _, r := range roads {
|
||||||
|
if r == nil || len(r.Points) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gates := roadGateCentersForRoad(r, layout)
|
||||||
|
if len(gates) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, g := range gates {
|
||||||
|
tx, ty, ok := fortEstimateWallTangent(g, layout.Mask)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
offset := 1.5 * float64(max(1, r.Width))
|
||||||
|
left := image.Point{
|
||||||
|
X: int(math.Round(float64(g.X) + tx*offset)),
|
||||||
|
Y: int(math.Round(float64(g.Y) + ty*offset)),
|
||||||
|
}
|
||||||
|
right := image.Point{
|
||||||
|
X: int(math.Round(float64(g.X) - tx*offset)),
|
||||||
|
Y: int(math.Round(float64(g.Y) - ty*offset)),
|
||||||
|
}
|
||||||
|
addTurret(left)
|
||||||
|
addTurret(right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBoundaryWallPixel(x, y int, wallMask *PixelMask) bool {
|
||||||
|
if wallMask == nil || !wallMask.GetXY(x, y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for dy := -1; dy <= 1; dy++ {
|
||||||
|
for dx := -1; dx <= 1; dx++ {
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nx, ny := x+dx, y+dy
|
||||||
|
if !wallMask.InBounds(nx, ny) || !wallMask.GetXY(nx, ny) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchesWater(x, y int, waterMask *PixelMask) bool {
|
||||||
|
if waterMask == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for dy := -1; dy <= 1; dy++ {
|
||||||
|
for dx := -1; dx <= 1; dx++ {
|
||||||
|
nx, ny := x+dx, y+dy
|
||||||
|
if waterMask.GetXY(nx, ny) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func averagePoint(points []image.Point) image.Point {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return image.Point{}
|
||||||
|
}
|
||||||
|
var sx, sy int
|
||||||
|
for _, p := range points {
|
||||||
|
sx += p.X
|
||||||
|
sy += p.Y
|
||||||
|
}
|
||||||
|
return image.Point{X: sx / len(points), Y: sy / len(points)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawTurret(img *image.RGBA, mask *PixelMask, center image.Point, radius int, shape string, col color.RGBA) {
|
||||||
|
for dy := -radius; dy <= radius; dy++ {
|
||||||
|
for dx := -radius; dx <= radius; dx++ {
|
||||||
|
if shape == "circular" && dx*dx+dy*dy > radius*radius {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x, y := center.X+dx, center.Y+dy
|
||||||
|
if !mask.InBounds(x, y) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mask.SetXY(x, y)
|
||||||
|
img.Set(x, y, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nearbyTurretExists(mask *PixelMask, center image.Point, radius int) bool {
|
||||||
|
if mask == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for dy := -radius; dy <= radius; dy++ {
|
||||||
|
for dx := -radius; dx <= radius; dx++ {
|
||||||
|
if dx*dx+dy*dy > radius*radius {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mask.GetXY(center.X+dx, center.Y+dy) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapPointToWall(center image.Point, wallMask *PixelMask, maxRadius int) (image.Point, bool) {
|
||||||
|
if wallMask == nil {
|
||||||
|
return image.Point{}, false
|
||||||
|
}
|
||||||
|
if wallMask.GetXY(center.X, center.Y) {
|
||||||
|
return center, true
|
||||||
|
}
|
||||||
|
if maxRadius < 1 {
|
||||||
|
maxRadius = 1
|
||||||
|
}
|
||||||
|
best := image.Point{}
|
||||||
|
bestD2 := math.MaxInt
|
||||||
|
found := false
|
||||||
|
for r := 1; r <= maxRadius; r++ {
|
||||||
|
minX := center.X - r
|
||||||
|
maxX := center.X + r
|
||||||
|
minY := center.Y - r
|
||||||
|
maxY := center.Y + r
|
||||||
|
for y := minY; y <= maxY; y++ {
|
||||||
|
for x := minX; x <= maxX; x++ {
|
||||||
|
if x != minX && x != maxX && y != minY && y != maxY {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !wallMask.GetXY(x, y) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dx := x - center.X
|
||||||
|
dy := y - center.Y
|
||||||
|
d2 := dx*dx + dy*dy
|
||||||
|
if d2 < bestD2 {
|
||||||
|
bestD2 = d2
|
||||||
|
best = image.Point{X: x, Y: y}
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return best, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image.Point{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func roadGateCentersForRoad(r *Road, layout *FortificationLayout) []image.Point {
|
||||||
|
out := make([]image.Point, 0, 2)
|
||||||
|
if r == nil || layout == nil || layout.Mask == nil || len(r.Points) < 2 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
prevID := 0
|
||||||
|
if layout.Mask.InBounds(r.Points[0].Point.X, r.Points[0].Point.Y) {
|
||||||
|
prevID = layout.WallIDByPixel[r.Points[0].Point.Y*layout.Mask.Width+r.Points[0].Point.X]
|
||||||
|
}
|
||||||
|
for i := 1; i < len(r.Points); i++ {
|
||||||
|
p := r.Points[i].Point
|
||||||
|
currID := 0
|
||||||
|
if layout.Mask.InBounds(p.X, p.Y) {
|
||||||
|
currID = layout.WallIDByPixel[p.Y*layout.Mask.Width+p.X]
|
||||||
|
}
|
||||||
|
if (prevID == 0 && currID > 0) || (prevID > 0 && currID == 0) {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
prevID = currID
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func fortEstimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64, bool) {
|
||||||
|
if wallMask == nil {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
const r = 4
|
||||||
|
var pts [][2]float64
|
||||||
|
for dy := -r; dy <= r; dy++ {
|
||||||
|
y := mid.Y + dy
|
||||||
|
if y < 0 || y >= wallMask.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for dx := -r; dx <= r; dx++ {
|
||||||
|
x := mid.X + dx
|
||||||
|
if x < 0 || x >= wallMask.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if wallMask.GetXY(x, y) {
|
||||||
|
pts = append(pts, [2]float64{float64(x), float64(y)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(pts) < 3 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
var mx, my float64
|
||||||
|
for _, p := range pts {
|
||||||
|
mx += p[0]
|
||||||
|
my += p[1]
|
||||||
|
}
|
||||||
|
mx /= float64(len(pts))
|
||||||
|
my /= float64(len(pts))
|
||||||
|
var sxx, syy, sxy float64
|
||||||
|
for _, p := range pts {
|
||||||
|
dx := p[0] - mx
|
||||||
|
dy := p[1] - my
|
||||||
|
sxx += dx * dx
|
||||||
|
syy += dy * dy
|
||||||
|
sxy += dx * dy
|
||||||
|
}
|
||||||
|
if sxx+syy < 0.001 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
theta := 0.5 * math.Atan2(2*sxy, sxx-syy)
|
||||||
|
return math.Cos(theta), math.Sin(theta), true
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func main() {
|
|||||||
var bridgeMask *PixelMask
|
var bridgeMask *PixelMask
|
||||||
var exitRoadMask *PixelMask
|
var exitRoadMask *PixelMask
|
||||||
var wallMask *PixelMask
|
var wallMask *PixelMask
|
||||||
|
var turretMask *PixelMask
|
||||||
|
|
||||||
// Set up application configuration directory
|
// Set up application configuration directory
|
||||||
configDir, err := os.UserConfigDir()
|
configDir, err := os.UserConfigDir()
|
||||||
@@ -282,29 +283,34 @@ func main() {
|
|||||||
|
|
||||||
// Step 6: Generating Roads
|
// Step 6: Generating Roads
|
||||||
var roadAnchors []image.Point
|
var roadAnchors []image.Point
|
||||||
|
var roadList []*Road
|
||||||
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
|
rl []*Road
|
||||||
} {
|
} {
|
||||||
rd, br, ex, anc := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
rd, br, ex, anc, rl := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
}{rd: rd, br: br, ex: ex, anc: anc}
|
rl []*Road
|
||||||
|
}{rd: rd, br: br, ex: ex, anc: anc, rl: rl}
|
||||||
}); ok {
|
}); ok {
|
||||||
roadMask, bridgeMask, exitRoadMask, roadAnchors = out.rd, out.br, out.ex, out.anc
|
roadMask, bridgeMask, exitRoadMask, roadAnchors, roadList = out.rd, out.br, out.ex, out.anc, out.rl
|
||||||
drawWallMask(roadBase, wallMask)
|
drawWallMask(roadBase, wallMask)
|
||||||
|
turretMask = GenerateTurrets(roadBase, settings.Width, settings.Height, settings, wallLayout, waterMask, roadMask, roadList)
|
||||||
finalImage = roadBase
|
finalImage = roadBase
|
||||||
} else {
|
} else {
|
||||||
log.Println("GenerateRoads timed out after 1 minute; continuing.")
|
log.Println("GenerateRoads timed out after 1 minute; continuing.")
|
||||||
roadMask = NewPixelMask(settings.Width, settings.Height)
|
roadMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
bridgeMask = NewPixelMask(settings.Width, settings.Height)
|
bridgeMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
turretMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
roadAnchors = nil
|
roadAnchors = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,6 +737,59 @@ func main() {
|
|||||||
settings.WallCurvyness = val
|
settings.WallCurvyness = val
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
showTurretsCheck := widget.NewCheck("Show Turrets", func(v bool) {
|
||||||
|
settings.ShowTurrets = v
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
|
||||||
|
turretShapeLabel := widget.NewLabel("Turret Shape:")
|
||||||
|
turretShapeSelect := widget.NewSelect([]string{"circular", "square"}, func(s string) {
|
||||||
|
settings.TurretShape = s
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create UI elements for error display and action buttons
|
// Create UI elements for error display and action buttons
|
||||||
errorLabel := widget.NewLabel("")
|
errorLabel := widget.NewLabel("")
|
||||||
errorLabel.Wrapping = fyne.TextWrapWord
|
errorLabel.Wrapping = fyne.TextWrapWord
|
||||||
@@ -749,7 +808,7 @@ func main() {
|
|||||||
showSaveDialog(w, bumpmapImg.Image, settings)
|
showSaveDialog(w, bumpmapImg.Image, settings)
|
||||||
})
|
})
|
||||||
exportMasksBtn := widget.NewButton("Export Masks", func() {
|
exportMasksBtn := widget.NewButton("Export Masks", func() {
|
||||||
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, buildingMask)
|
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask)
|
||||||
})
|
})
|
||||||
// Main generation button and logic
|
// Main generation button and logic
|
||||||
generateBtn = widget.NewButton("Generate", func() {
|
generateBtn = widget.NewButton("Generate", func() {
|
||||||
@@ -905,23 +964,27 @@ func main() {
|
|||||||
progressBar.SetValue(6.0 / 13.0)
|
progressBar.SetValue(6.0 / 13.0)
|
||||||
})
|
})
|
||||||
var roadAnchors []image.Point
|
var roadAnchors []image.Point
|
||||||
|
var roadList []*Road
|
||||||
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
|
||||||
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
|
rl []*Road
|
||||||
} {
|
} {
|
||||||
rd, br, ex, anc := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
rd, br, ex, anc, rl := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
|
||||||
return struct {
|
return struct {
|
||||||
rd *PixelMask
|
rd *PixelMask
|
||||||
br *PixelMask
|
br *PixelMask
|
||||||
ex *PixelMask
|
ex *PixelMask
|
||||||
anc []image.Point
|
anc []image.Point
|
||||||
}{rd: rd, br: br, ex: ex, anc: anc}
|
rl []*Road
|
||||||
|
}{rd: rd, br: br, ex: ex, anc: anc, rl: rl}
|
||||||
}); ok {
|
}); ok {
|
||||||
roadMask, bridgeMask, exitRoadMask, roadAnchors = out.rd, out.br, out.ex, out.anc
|
roadMask, bridgeMask, exitRoadMask, roadAnchors, roadList = out.rd, out.br, out.ex, out.anc, out.rl
|
||||||
drawWallMask(roadBase, wallMask)
|
drawWallMask(roadBase, wallMask)
|
||||||
|
turretMask = GenerateTurrets(roadBase, settings.Width, settings.Height, settings, wallLayout, waterMask, roadMask, roadList)
|
||||||
finalImage = roadBase
|
finalImage = roadBase
|
||||||
} else {
|
} else {
|
||||||
log.Println("GenerateRoads timed out after 1 minute; continuing.")
|
log.Println("GenerateRoads timed out after 1 minute; continuing.")
|
||||||
@@ -929,6 +992,7 @@ func main() {
|
|||||||
roadMask = NewPixelMask(settings.Width, settings.Height)
|
roadMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
bridgeMask = NewPixelMask(settings.Width, settings.Height)
|
bridgeMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
|
turretMask = NewPixelMask(settings.Width, settings.Height)
|
||||||
roadAnchors = nil
|
roadAnchors = nil
|
||||||
}
|
}
|
||||||
placementMask := cloneMask(roadMask)
|
placementMask := cloneMask(roadMask)
|
||||||
@@ -1135,6 +1199,8 @@ func main() {
|
|||||||
numWallsSlider,
|
numWallsSlider,
|
||||||
cityCoverageSlider,
|
cityCoverageSlider,
|
||||||
wallCurvynessSlider,
|
wallCurvynessSlider,
|
||||||
|
showTurretsCheck,
|
||||||
|
turretControls,
|
||||||
))
|
))
|
||||||
numBuildingsSlider := newNumericInputSlider(0, 10000, float64(settings.NumBuildings), "%.0f", "Number of Buildings")
|
numBuildingsSlider := newNumericInputSlider(0, 10000, float64(settings.NumBuildings), "%.0f", "Number of Buildings")
|
||||||
numBuildingsSlider.entry.OnChanged = func(s string) {
|
numBuildingsSlider.entry.OnChanged = func(s string) {
|
||||||
@@ -1498,7 +1564,7 @@ func encodeImageToWriter(w io.Writer, img image.Image, format string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// showMasksSaveDialog displays a dialog for saving the generated masks.
|
// 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, buildingMask *PixelMask) {
|
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
|
// Create UI elements for the save dialog
|
||||||
fileNameEntry := widget.NewEntry()
|
fileNameEntry := widget.NewEntry()
|
||||||
fileNameEntry.SetPlaceHolder("masks_folder")
|
fileNameEntry.SetPlaceHolder("masks_folder")
|
||||||
@@ -1583,6 +1649,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
|
|||||||
{name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }},
|
{name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }},
|
||||||
{name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }},
|
{name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }},
|
||||||
{name: "walls_mask." + imgFormat, make: func() image.Image { return maskToGray(wallMask) }},
|
{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) }},
|
{name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(buildingMask) }},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ func GenerateRoads(
|
|||||||
waterMask *PixelMask,
|
waterMask *PixelMask,
|
||||||
seed int64,
|
seed int64,
|
||||||
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
||||||
return GenerateRoadsWithPOIs(img, width, height, settings, waterMask, nil, nil, 0, false, seed)
|
roadMask, bridgeMask, exitRoadMask, roadAnchors, _ := GenerateRoadsWithPOIs(img, width, height, settings, waterMask, nil, nil, 0, false, seed)
|
||||||
|
return roadMask, bridgeMask, exitRoadMask, roadAnchors
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrepareRoadNodes(width, height int, settings *Settings, waterMask *PixelMask, seed int64) ([]*PointOfInterest, int, bool) {
|
func PrepareRoadNodes(width, height int, settings *Settings, waterMask *PixelMask, seed int64) ([]*PointOfInterest, int, bool) {
|
||||||
@@ -122,7 +123,7 @@ func GenerateRoadsWithPOIs(
|
|||||||
roadTarget int,
|
roadTarget int,
|
||||||
edgeToEdgeOnly bool,
|
edgeToEdgeOnly bool,
|
||||||
seed int64,
|
seed int64,
|
||||||
) (*PixelMask, *PixelMask, *PixelMask, []image.Point) {
|
) (*PixelMask, *PixelMask, *PixelMask, []image.Point, []*Road) {
|
||||||
if img == nil {
|
if img == nil {
|
||||||
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ func GenerateRoadsWithPOIs(
|
|||||||
internalRoads := int(math.Round(clamp(settings.RoadDistribution, 0, 100)))
|
internalRoads := int(math.Round(clamp(settings.RoadDistribution, 0, 100)))
|
||||||
exitRoads := max(0, settings.RoadExits)
|
exitRoads := max(0, settings.RoadExits)
|
||||||
if internalRoads == 0 && exitRoads == 0 {
|
if internalRoads == 0 && exitRoads == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
|
||||||
}
|
}
|
||||||
if internalRoads > 0 {
|
if internalRoads > 0 {
|
||||||
roadTarget = internalRoads
|
roadTarget = internalRoads
|
||||||
@@ -159,22 +160,22 @@ func GenerateRoadsWithPOIs(
|
|||||||
pois = generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
|
pois = generatePOIs(width, height, settings, waterMask, randSrc, roadTarget)
|
||||||
}
|
}
|
||||||
if len(pois) < 2 {
|
if len(pois) < 2 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
|
||||||
}
|
}
|
||||||
roads = connectPOIs(pois, width, height, settings, randSrc, waterMask, wallLayout, roadTarget)
|
roads = connectPOIs(pois, width, height, settings, randSrc, waterMask, wallLayout, roadTarget)
|
||||||
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask, wallLayout)
|
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMask, wallLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
|
||||||
}
|
}
|
||||||
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
|
roads = applyWallCrossingRules(roads, wallLayout, waterMask, randSrc)
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil
|
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
|
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)
|
||||||
assignRoadWidths(roads, settings, randSrc, width, height, wallLayout)
|
assignRoadWidths(roads, settings, randSrc, width, height, wallLayout)
|
||||||
@@ -190,7 +191,7 @@ func GenerateRoadsWithPOIs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
roadAnchors := roadMask.ToPoints()
|
roadAnchors := roadMask.ToPoints()
|
||||||
return roadMask, bridgeMask, exitRoadMask, roadAnchors
|
return roadMask, bridgeMask, exitRoadMask, roadAnchors, roads
|
||||||
}
|
}
|
||||||
|
|
||||||
func nudgePOIsOutsideWalls(pois []*PointOfInterest, wallMask, waterMask *PixelMask, settings *Settings, width, height int, randSrc *rand.Rand) {
|
func nudgePOIsOutsideWalls(pois []*PointOfInterest, wallMask, waterMask *PixelMask, settings *Settings, width, height int, randSrc *rand.Rand) {
|
||||||
|
|||||||
+22
@@ -50,6 +50,10 @@ type Settings struct {
|
|||||||
NumWalls int `json:"num_walls"`
|
NumWalls int `json:"num_walls"`
|
||||||
CityCoverage float64 `json:"city_coverage"`
|
CityCoverage float64 `json:"city_coverage"`
|
||||||
WallCurvyness float64 `json:"wall_curvyness"`
|
WallCurvyness float64 `json:"wall_curvyness"`
|
||||||
|
ShowTurrets bool `json:"show_turrets"`
|
||||||
|
TurretSize float64 `json:"turret_size"`
|
||||||
|
TurretShape string `json:"turret_shape"`
|
||||||
|
TurretSpacing float64 `json:"turret_spacing"`
|
||||||
|
|
||||||
// Building settings
|
// Building settings
|
||||||
NumBuildings int `json:"num_buildings"`
|
NumBuildings int `json:"num_buildings"`
|
||||||
@@ -151,6 +155,10 @@ func LoadSettings() (*Settings, error) {
|
|||||||
NumWalls: 1,
|
NumWalls: 1,
|
||||||
CityCoverage: 70,
|
CityCoverage: 70,
|
||||||
WallCurvyness: 35,
|
WallCurvyness: 35,
|
||||||
|
ShowTurrets: true,
|
||||||
|
TurretSize: 0.6,
|
||||||
|
TurretShape: "circular",
|
||||||
|
TurretSpacing: 55,
|
||||||
NumBuildings: 200,
|
NumBuildings: 200,
|
||||||
MinBuildingSize: 3.5,
|
MinBuildingSize: 3.5,
|
||||||
MaxBuildingSize: 10.0,
|
MaxBuildingSize: 10.0,
|
||||||
@@ -230,6 +238,18 @@ func LoadSettings() (*Settings, error) {
|
|||||||
if _, ok := rawKeys["wall_curvyness"]; !ok {
|
if _, ok := rawKeys["wall_curvyness"]; !ok {
|
||||||
settings.WallCurvyness = 35
|
settings.WallCurvyness = 35
|
||||||
}
|
}
|
||||||
|
if _, ok := rawKeys["show_turrets"]; !ok {
|
||||||
|
settings.ShowTurrets = true
|
||||||
|
}
|
||||||
|
if _, ok := rawKeys["turret_size"]; !ok {
|
||||||
|
settings.TurretSize = 0.6
|
||||||
|
}
|
||||||
|
if _, ok := rawKeys["turret_shape"]; !ok || (settings.TurretShape != "square" && settings.TurretShape != "circular") {
|
||||||
|
settings.TurretShape = "circular"
|
||||||
|
}
|
||||||
|
if _, ok := rawKeys["turret_spacing"]; !ok {
|
||||||
|
settings.TurretSpacing = 55
|
||||||
|
}
|
||||||
|
|
||||||
// Wall widths are percentages of average image dimension.
|
// Wall widths 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.
|
||||||
@@ -250,6 +270,8 @@ func LoadSettings() (*Settings, error) {
|
|||||||
}
|
}
|
||||||
settings.CityCoverage = clamp(settings.CityCoverage, 1, 100)
|
settings.CityCoverage = clamp(settings.CityCoverage, 1, 100)
|
||||||
settings.WallCurvyness = clamp(settings.WallCurvyness, 0, 100)
|
settings.WallCurvyness = clamp(settings.WallCurvyness, 0, 100)
|
||||||
|
settings.TurretSize = snapTurretSizePercent(settings.TurretSize)
|
||||||
|
settings.TurretSpacing = clamp(settings.TurretSpacing, 0, 100)
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user