Polished wall generation, though work is needed good enough for main now
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user