Polished wall generation, though work is needed good enough for main now

This commit is contained in:
grimsace
2026-03-11 12:43:41 -05:00
parent 580d7e9d4c
commit 8dc7546e09
6 changed files with 605 additions and 22 deletions
+214 -16
View File
@@ -173,6 +173,14 @@ func GenerateRoadsWithPOIs(
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 {
gateRoads := generateGateRoads(wallLayout, settings, waterMask, width, height, randSrc)
roads = append(roads, gateRoads...)
roads = ensureGateRoadConnections(gateRoads, roads, wallLayout, settings, waterMask, width, height, randSrc)
}
roads = reduceRepeatedBridges(roads, waterMask, width, height, randSrc)
if len(roads) == 0 {
return NewPixelMask(width, height), NewPixelMask(width, height), NewPixelMask(width, height), nil, nil
@@ -201,23 +209,20 @@ func nudgePOIsOutsideWalls(pois []*PointOfInterest, wallMask, waterMask *PixelMa
if waterMask == nil {
waterMask = NewPixelMask(width, height)
}
minWallPx, maxWallPx := getWallWidthRangePixels(settings, width, height)
// Build exclusion zone: wall pixels dilated by one road width.
// POIs must be outside this zone so roads have room to run parallel to walls.
fakeLayout := &FortificationLayout{Mask: wallMask}
exclusion := buildWallExclusionMask(fakeLayout, settings, width, height)
centerX := float64(width-1) * 0.5
centerY := float64(height-1) * 0.5
for _, p := range pois {
if p == nil || !wallMask.GetXY(p.X, p.Y) {
if p == nil {
continue
}
wallWidthPx := minWallPx
if maxWallPx > minWallPx {
wallWidthPx = minWallPx + randSrc.Float64()*(maxWallPx-minWallPx)
}
nudgeFactor := 0.02 + randSrc.Float64()*0.03
nudgeDist := int(math.Round(wallWidthPx * nudgeFactor))
if nudgeDist < 1 {
nudgeDist = 1
if !exclusion.GetXY(p.X, p.Y) {
continue
}
vx := float64(p.X) - centerX
@@ -233,13 +238,14 @@ func nudgePOIsOutsideWalls(pois []*PointOfInterest, wallMask, waterMask *PixelMa
dy := vy / vlen
moved := false
for step := 1; step <= nudgeDist+32; step++ {
maxSteps := exclusion.Width + exclusion.Height
for step := 1; step <= maxSteps; step++ {
nx := int(math.Round(float64(p.X) + float64(step)*dx))
ny := int(math.Round(float64(p.Y) + float64(step)*dy))
if nx < 0 || ny < 0 || nx >= width || ny >= height {
break
}
if wallMask.GetXY(nx, ny) || waterMask.GetXY(nx, ny) {
if exclusion.GetXY(nx, ny) || waterMask.GetXY(nx, ny) {
continue
}
p.X = nx
@@ -251,19 +257,18 @@ func nudgePOIsOutsideWalls(pois []*PointOfInterest, wallMask, waterMask *PixelMa
continue
}
// Fallback: small radial sweep if direct outward ray was blocked.
baseAngle := math.Atan2(dy, dx)
for a := -6; a <= 6; a++ {
ang := baseAngle + float64(a)*math.Pi/18.0
adx := math.Cos(ang)
ady := math.Sin(ang)
for step := 1; step <= nudgeDist+32; step++ {
for step := 1; step <= exclusion.Width+exclusion.Height; step++ {
nx := int(math.Round(float64(p.X) + float64(step)*adx))
ny := int(math.Round(float64(p.Y) + float64(step)*ady))
if nx < 0 || ny < 0 || nx >= width || ny >= height {
break
}
if wallMask.GetXY(nx, ny) || waterMask.GetXY(nx, ny) {
if exclusion.GetXY(nx, ny) || waterMask.GetXY(nx, ny) {
continue
}
p.X = nx
@@ -1761,6 +1766,199 @@ func bridgedRegionIDs(points []PathPoint, regionByPixel []int, width, height int
return out
}
// buildWallExclusionMask creates a mask of wall pixels dilated by one road width.
// Roads will avoid pixels set in this mask (except at gates).
func buildWallExclusionMask(wallLayout *FortificationLayout, settings *Settings, width, height int) *PixelMask {
if wallLayout == nil || wallLayout.Mask == nil {
return NewPixelMask(width, height)
}
_, maxRoadPx := getRoadWidthRangePixels(settings, width, height)
margin := int(math.Ceil(maxRoadPx))
if margin < 1 {
margin = 1
}
out := NewPixelMask(width, height)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if !wallLayout.Mask.GetXY(x, y) {
continue
}
for dy := -margin; dy <= margin; dy++ {
for dx := -margin; dx <= margin; dx++ {
if dx*dx+dy*dy <= margin*margin {
out.SetXY(x+dx, y+dy)
}
}
}
}
}
return out
}
// generateGateRoads creates one straight perpendicular road per gate.
// Each road runs from the outer end to the inner end of the gate, crossing the wall gap.
// It also creates POIs at inner/outer ends so the road network can connect to them.
func generateGateRoads(wallLayout *FortificationLayout, settings *Settings, waterMask *PixelMask, width, height int, randSrc *rand.Rand) []*Road {
if wallLayout == nil || len(wallLayout.Gates) == 0 {
return nil
}
_, maxRoadPx := getRoadWidthRangePixels(settings, width, height)
roadWidth := int(math.Round(maxRoadPx + 0.5*(maxRoadPx)))
if roadWidth < 1 {
roadWidth = 1
}
roads := make([]*Road, 0, len(wallLayout.Gates))
for _, gate := range wallLayout.Gates {
// Straight line from outerEnd to innerEnd — do NOT route through gateCenter
// (which is a wall boundary pixel and causes a kink in the road).
outer := &PointOfInterest{X: gate.OuterEnd.X, Y: gate.OuterEnd.Y, IsExit: false}
inner := &PointOfInterest{X: gate.InnerEnd.X, Y: gate.InnerEnd.Y, IsExit: false}
outer.Connections = 1
inner.Connections = 1
pts := bresenhamRoad([]image.Point{gate.OuterEnd, gate.InnerEnd})
path := toPathPoints(pts, waterMask)
roads = append(roads, &Road{
Start: outer,
End: inner,
Points: path,
Width: roadWidth,
Importance: 10, // high importance so gate roads get wide treatment
})
}
return roads
}
// ensureGateRoadConnections adds short connector roads from each gate's inner/outer
// endpoints to the nearest existing road POI, so the gate road is part of the network.
func ensureGateRoadConnections(gateRoads []*Road, allRoads []*Road, wallLayout *FortificationLayout, settings *Settings, waterMask *PixelMask, width, height int, randSrc *rand.Rand) []*Road {
if len(gateRoads) == 0 || wallLayout == nil {
return allRoads
}
// Collect non-gate POIs.
poiSet := make(map[*PointOfInterest]bool)
for _, r := range allRoads {
if r.Start != nil {
poiSet[r.Start] = true
}
if r.End != nil {
poiSet[r.End] = true
}
}
// Remove gate road endpoints from the non-gate set.
for _, r := range gateRoads {
delete(poiSet, r.Start)
delete(poiSet, r.End)
}
pois := make([]*PointOfInterest, 0, len(poiSet))
for p := range poiSet {
pois = append(pois, p)
}
connectors := make([]*Road, 0, len(gateRoads)*2)
_, maxRoadPx := getRoadWidthRangePixels(settings, width, height)
connW := int(math.Round(maxRoadPx))
if connW < 1 {
connW = 1
}
// pathCrossesWall returns true if a straight Bresenham line from a to b touches any wall pixel.
pathCrossesWall := func(a, b image.Point) bool {
dx := abs(b.X - a.X)
dy := abs(b.Y - a.Y)
sx := -1
if a.X < b.X {
sx = 1
}
sy := -1
if a.Y < b.Y {
sy = 1
}
err := dx - dy
x, y := a.X, a.Y
for {
if wallLayout.Mask.GetXY(x, y) {
return true
}
if x == b.X && y == b.Y {
break
}
e2 := 2 * err
if e2 > -dy {
err -= dy
x += sx
}
if e2 < dx {
err += dx
y += sy
}
}
return false
}
for _, gr := range gateRoads {
for _, ep := range []*PointOfInterest{gr.Start, gr.End} {
if len(pois) == 0 {
break
}
epPt := image.Point{X: ep.X, Y: ep.Y}
// Find nearest POI reachable without crossing any wall.
var best *PointOfInterest
bestD2 := math.MaxFloat64
for _, p := range pois {
if wallLayout.Mask.GetXY(p.X, p.Y) {
continue
}
pPt := image.Point{X: p.X, Y: p.Y}
if pathCrossesWall(epPt, pPt) {
continue
}
dx := float64(p.X - ep.X)
dy := float64(p.Y - ep.Y)
d2 := dx*dx + dy*dy
if d2 < bestD2 {
bestD2 = d2
best = p
}
}
// Fallback: if no wall-safe POI found, take the nearest regardless.
if best == nil {
for _, p := range pois {
if wallLayout.Mask.GetXY(p.X, p.Y) {
continue
}
dx := float64(p.X - ep.X)
dy := float64(p.Y - ep.Y)
d2 := dx*dx + dy*dy
if d2 < bestD2 {
bestD2 = d2
best = p
}
}
}
if best == nil {
continue
}
pts := bresenhamRoad([]image.Point{epPt, {X: best.X, Y: best.Y}})
path := toPathPoints(pts, waterMask)
ep.Connections++
best.Connections++
connectors = append(connectors, &Road{
Start: ep,
End: best,
Points: path,
Width: connW,
Importance: 6,
})
}
}
return append(allRoads, connectors...)
}
func clamp(v, lo, hi float64) float64 {
if v < lo {
return lo