reworked road generation with focus on main roads, secondary streets and smaller arterial roads
This commit is contained in:
+53
-4
@@ -173,22 +173,68 @@ func GenerateBuildings(
|
||||
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
||||
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
||||
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||
anchorUsage := make(map[image.Point]int, len(anchorPoints))
|
||||
normalAnchorCap := 0
|
||||
exitAnchorCap := 0
|
||||
if len(normalRoadAnchors) > 0 {
|
||||
normalAnchorCap = max(2, int(math.Ceil((float64(settings.NumBuildings)/float64(len(normalRoadAnchors)))*1.15)))
|
||||
}
|
||||
if len(exitRoadAnchors) > 0 {
|
||||
exitAnchorCap = max(1, int(math.Ceil((float64(settings.NumBuildings)/float64(len(exitRoadAnchors)))*0.20)))
|
||||
}
|
||||
|
||||
pickAnchorWithCapacity := func(candidates []image.Point, capLimit int) (image.Point, bool) {
|
||||
if len(candidates) == 0 {
|
||||
return image.Point{}, false
|
||||
}
|
||||
if capLimit <= 0 {
|
||||
return candidates[randSrc.Intn(len(candidates))], true
|
||||
}
|
||||
best := candidates[randSrc.Intn(len(candidates))]
|
||||
bestCount := anchorUsage[best]
|
||||
for tries := 0; tries < min(16, len(candidates)*2); tries++ {
|
||||
candidate := candidates[randSrc.Intn(len(candidates))]
|
||||
count := anchorUsage[candidate]
|
||||
if count < capLimit {
|
||||
return candidate, true
|
||||
}
|
||||
if count < bestCount {
|
||||
best = candidate
|
||||
bestCount = count
|
||||
}
|
||||
}
|
||||
if bestCount < capLimit {
|
||||
return best, true
|
||||
}
|
||||
return image.Point{}, false
|
||||
}
|
||||
|
||||
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
|
||||
maxPlacementAttempts--
|
||||
|
||||
// Select an anchor point for the new building
|
||||
var anchor image.Point
|
||||
usedRoadAnchor := false
|
||||
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
|
||||
// Buildings should only rarely use exit-road anchors.
|
||||
useExitAnchor := len(exitRoadAnchors) > 0 && randSrc.Float64() < 0.02
|
||||
if useExitAnchor {
|
||||
anchor = exitRoadAnchors[randSrc.Intn(len(exitRoadAnchors))]
|
||||
if a, ok := pickAnchorWithCapacity(exitRoadAnchors, exitAnchorCap); ok {
|
||||
anchor = a
|
||||
usedRoadAnchor = true
|
||||
}
|
||||
} else if len(normalRoadAnchors) > 0 {
|
||||
anchor = normalRoadAnchors[randSrc.Intn(len(normalRoadAnchors))]
|
||||
if a, ok := pickAnchorWithCapacity(normalRoadAnchors, normalAnchorCap); ok {
|
||||
anchor = a
|
||||
usedRoadAnchor = true
|
||||
}
|
||||
} else if len(anchorPoints) > 0 {
|
||||
anchor = anchorPoints[randSrc.Intn(len(anchorPoints))]
|
||||
} else {
|
||||
if a, ok := pickAnchorWithCapacity(anchorPoints, normalAnchorCap); ok {
|
||||
anchor = a
|
||||
usedRoadAnchor = true
|
||||
}
|
||||
}
|
||||
if !usedRoadAnchor {
|
||||
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -247,6 +293,9 @@ func GenerateBuildings(
|
||||
img.Set(p.X, p.Y, buildingColor)
|
||||
buildingMask.SetPoint(p)
|
||||
}
|
||||
if usedRoadAnchor {
|
||||
anchorUsage[anchor]++
|
||||
}
|
||||
buildings = append(buildings, pixels)
|
||||
buildingsPlaced++
|
||||
break // Move to the next building
|
||||
|
||||
@@ -23,12 +23,21 @@ type PathPoint struct {
|
||||
IsBridge bool
|
||||
}
|
||||
|
||||
type RoadTier int
|
||||
|
||||
const (
|
||||
RoadTierLocal RoadTier = iota
|
||||
RoadTierCollector
|
||||
RoadTierArterial
|
||||
)
|
||||
|
||||
// Road represents a connection between two points of interest.
|
||||
type Road struct {
|
||||
Start, End *PointOfInterest
|
||||
Width int
|
||||
Points []PathPoint
|
||||
Importance int
|
||||
Tier RoadTier
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -198,7 +207,7 @@ func GenerateRoadsWithPOIs(
|
||||
}
|
||||
}
|
||||
|
||||
roadAnchors := roadMask.ToPoints()
|
||||
roadAnchors := collectRoadAnchors(roads, settings, waterMask, width, height)
|
||||
return roadMask, bridgeMask, exitRoadMask, roadAnchors, roads
|
||||
}
|
||||
|
||||
@@ -293,12 +302,13 @@ func generateEdgeToEdgeExitRoads(exitRoads, width, height int, settings *Setting
|
||||
start, end := sampleDifferentEdgePair(width, height, randSrc)
|
||||
start.IsExit = true
|
||||
end.IsExit = true
|
||||
path := calculateRoadPath(start, end, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||
path := calculateRoadPath(start, end, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout, RoadTierArterial)
|
||||
roads = append(roads, &Road{
|
||||
Start: start,
|
||||
End: end,
|
||||
Points: path,
|
||||
Importance: 1,
|
||||
Tier: RoadTierArterial,
|
||||
})
|
||||
}
|
||||
return roads
|
||||
@@ -491,10 +501,23 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
if roadTarget < len(pois)-1 {
|
||||
roadTarget = len(pois) - 1
|
||||
}
|
||||
collectorTarget := max(len(pois)-1, max(roadTarget, roadTarget+max(2, roadTarget/8)))
|
||||
totalBudget := max(collectorTarget, roadTarget+max(3, roadTarget/4))
|
||||
|
||||
centerX := float64(width-1) * 0.5
|
||||
centerY := float64(height-1) * 0.5
|
||||
centerRadius := math.Max(math.Min(float64(width), float64(height))*0.28, 1)
|
||||
|
||||
centerCloseness := func(p *PointOfInterest) float64 {
|
||||
d := math.Hypot(float64(p.X)-centerX, float64(p.Y)-centerY)
|
||||
return 1.0 - clamp01(d/centerRadius)
|
||||
}
|
||||
|
||||
type edgeCandidate struct {
|
||||
a, b int
|
||||
score float64
|
||||
a, b int
|
||||
score float64
|
||||
dist float64
|
||||
arterialMean float64
|
||||
}
|
||||
|
||||
candidates := make([]edgeCandidate, 0, len(pois)*6)
|
||||
@@ -518,7 +541,13 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
arterialBias := 1.0 - math.Abs(a.ArterialWeight-b.ArterialWeight)
|
||||
distanceBias := 1.0 - clamp01(d/(edgeDist*1.6))
|
||||
score := arterialBias*0.65 + distanceBias*0.35 + randSrc.Float64()*0.08
|
||||
candidates = append(candidates, edgeCandidate{a: i, b: j, score: score})
|
||||
candidates = append(candidates, edgeCandidate{
|
||||
a: i,
|
||||
b: j,
|
||||
score: score,
|
||||
dist: d,
|
||||
arterialMean: (a.ArterialWeight + b.ArterialWeight) * 0.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
@@ -529,14 +558,31 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
return candidates[i].score > candidates[j].score
|
||||
})
|
||||
|
||||
selected := make(map[uint64]bool, roadTarget)
|
||||
adjAngles := make([][]float64, len(pois))
|
||||
selectedEdges := make([]edgeCandidate, 0, roadTarget)
|
||||
type selectedEdge struct {
|
||||
edge edgeCandidate
|
||||
tier RoadTier
|
||||
}
|
||||
|
||||
addEdge := func(pick edgeCandidate) {
|
||||
selected := make(map[uint64]bool, totalBudget)
|
||||
adjAngles := make([][]float64, len(pois))
|
||||
selectedEdges := make([]selectedEdge, 0, totalBudget)
|
||||
|
||||
nodeCapacity := func(p *PointOfInterest, tier RoadTier) int {
|
||||
base := max(1, p.TargetDegree)
|
||||
switch tier {
|
||||
case RoadTierArterial:
|
||||
return max(base+2, 4)
|
||||
case RoadTierCollector:
|
||||
return base + 1
|
||||
default:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
addEdge := func(pick edgeCandidate, tier RoadTier) {
|
||||
key := edgeKey(pick.a, pick.b)
|
||||
selected[key] = true
|
||||
selectedEdges = append(selectedEdges, pick)
|
||||
selectedEdges = append(selectedEdges, selectedEdge{edge: pick, tier: tier})
|
||||
a := pois[pick.a]
|
||||
b := pois[pick.b]
|
||||
angAB := math.Atan2(float64(b.Y-a.Y), float64(b.X-a.X))
|
||||
@@ -547,14 +593,14 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
adjAngles[pick.b] = append(adjAngles[pick.b], angBA)
|
||||
}
|
||||
|
||||
canUseEdge := func(pick edgeCandidate) bool {
|
||||
canUseEdge := func(pick edgeCandidate, tier RoadTier) bool {
|
||||
key := edgeKey(pick.a, pick.b)
|
||||
if selected[key] {
|
||||
return false
|
||||
}
|
||||
a := pois[pick.a]
|
||||
b := pois[pick.b]
|
||||
if a.Connections >= max(1, a.TargetDegree+1) || b.Connections >= max(1, b.TargetDegree+1) {
|
||||
if a.Connections >= nodeCapacity(a, tier) || b.Connections >= nodeCapacity(b, tier) {
|
||||
return false
|
||||
}
|
||||
angAB := math.Atan2(float64(b.Y-a.Y), float64(b.X-a.X))
|
||||
@@ -565,33 +611,80 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
return pick.score-degreePenalty(a, b) >= -0.4
|
||||
}
|
||||
|
||||
// Phase 1: enforce one connected backbone.
|
||||
start := 0
|
||||
bestWeight := pois[0].ArterialWeight
|
||||
for i := 1; i < len(pois); i++ {
|
||||
if pois[i].ArterialWeight > bestWeight {
|
||||
start = i
|
||||
bestWeight = pois[i].ArterialWeight
|
||||
arterialCount := max(2, min(len(pois), min(10, 2+roadTarget/16)))
|
||||
arterialOrder := make([]int, len(pois))
|
||||
for i := range arterialOrder {
|
||||
arterialOrder[i] = i
|
||||
}
|
||||
sort.Slice(arterialOrder, func(i, j int) bool {
|
||||
pi := pois[arterialOrder[i]]
|
||||
pj := pois[arterialOrder[j]]
|
||||
scoreI := pi.ArterialWeight - centerCloseness(pi)*0.22
|
||||
scoreJ := pj.ArterialWeight - centerCloseness(pj)*0.22
|
||||
if scoreI == scoreJ {
|
||||
return centerCloseness(pi) < centerCloseness(pj)
|
||||
}
|
||||
return scoreI > scoreJ
|
||||
})
|
||||
arterialNodes := make(map[int]bool, arterialCount)
|
||||
arterialMinSpacing := edgeDist * 0.55
|
||||
arterialMinSpacing2 := arterialMinSpacing * arterialMinSpacing
|
||||
for _, idx := range arterialOrder {
|
||||
if len(arterialNodes) >= arterialCount {
|
||||
break
|
||||
}
|
||||
keep := true
|
||||
for chosen := range arterialNodes {
|
||||
dx := float64(pois[chosen].X - pois[idx].X)
|
||||
dy := float64(pois[chosen].Y - pois[idx].Y)
|
||||
if dx*dx+dy*dy < arterialMinSpacing2 {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
arterialNodes[idx] = true
|
||||
}
|
||||
}
|
||||
for _, idx := range arterialOrder {
|
||||
if len(arterialNodes) >= arterialCount {
|
||||
break
|
||||
}
|
||||
arterialNodes[idx] = true
|
||||
}
|
||||
|
||||
start := arterialOrder[0]
|
||||
connected := make([]bool, len(pois))
|
||||
connected[start] = true
|
||||
connectedCount := 1
|
||||
|
||||
for connectedCount < len(pois) && len(selectedEdges) < roadTarget {
|
||||
// Phase 1: connect the major arterial skeleton first.
|
||||
arterialBudget := max(1, min(len(arterialNodes)-1, min(10, 2+roadTarget/20)))
|
||||
for len(selectedEdges) < arterialBudget {
|
||||
bestIdx := -1
|
||||
bestScore := -1.0
|
||||
for idx, c := range candidates {
|
||||
if !arterialNodes[c.a] || !arterialNodes[c.b] {
|
||||
continue
|
||||
}
|
||||
if c.dist < edgeDist*0.35 {
|
||||
continue
|
||||
}
|
||||
aConn := connected[c.a]
|
||||
bConn := connected[c.b]
|
||||
if aConn == bConn {
|
||||
continue
|
||||
}
|
||||
if !canUseEdge(c) {
|
||||
if !canUseEdge(c, RoadTierArterial) {
|
||||
continue
|
||||
}
|
||||
if c.score > bestScore {
|
||||
bestScore = c.score
|
||||
a := pois[c.a]
|
||||
b := pois[c.b]
|
||||
centerPenalty := centerCloseness(a) * centerCloseness(b) * 0.45
|
||||
degreePenalty := clamp01(float64(a.Connections+b.Connections) / 8.0)
|
||||
score := c.arterialMean*0.58 + clamp01(c.dist/edgeDist)*0.27 + c.score*0.15 - centerPenalty - degreePenalty*0.18
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestIdx = idx
|
||||
}
|
||||
}
|
||||
@@ -599,7 +692,7 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
break
|
||||
}
|
||||
pick := candidates[bestIdx]
|
||||
addEdge(pick)
|
||||
addEdge(pick, RoadTierArterial)
|
||||
if !connected[pick.a] {
|
||||
connected[pick.a] = true
|
||||
connectedCount++
|
||||
@@ -610,25 +703,71 @@ func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings,
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: add extra links up to the target.
|
||||
for _, pick := range candidates {
|
||||
if len(selectedEdges) >= roadTarget {
|
||||
// Phase 2: connect remaining nodes with collector roads.
|
||||
for connectedCount < len(pois) && len(selectedEdges) < collectorTarget {
|
||||
bestIdx := -1
|
||||
bestScore := -1.0
|
||||
for idx, c := range candidates {
|
||||
aConn := connected[c.a]
|
||||
bConn := connected[c.b]
|
||||
if aConn == bConn {
|
||||
continue
|
||||
}
|
||||
if !canUseEdge(c, RoadTierCollector) {
|
||||
continue
|
||||
}
|
||||
a := pois[c.a]
|
||||
b := pois[c.b]
|
||||
connectedBonus := 0.0
|
||||
if arterialNodes[c.a] || arterialNodes[c.b] {
|
||||
connectedBonus = 0.20
|
||||
}
|
||||
distScore := 1.0 - clamp01(c.dist/(edgeDist*1.1))
|
||||
centerPenalty := centerCloseness(a) * centerCloseness(b) * 0.35
|
||||
degreePenalty := clamp01(float64(a.Connections+b.Connections) / 7.0)
|
||||
score := c.score*0.28 + c.arterialMean*0.27 + distScore*0.35 + connectedBonus - centerPenalty - degreePenalty*0.14
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestIdx = idx
|
||||
}
|
||||
}
|
||||
if bestIdx == -1 {
|
||||
break
|
||||
}
|
||||
if !canUseEdge(pick) {
|
||||
pick := candidates[bestIdx]
|
||||
addEdge(pick, RoadTierCollector)
|
||||
if !connected[pick.a] {
|
||||
connected[pick.a] = true
|
||||
connectedCount++
|
||||
}
|
||||
if !connected[pick.b] {
|
||||
connected[pick.b] = true
|
||||
connectedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: add shorter local links inside districts.
|
||||
for _, pick := range candidates {
|
||||
if len(selectedEdges) >= totalBudget {
|
||||
break
|
||||
}
|
||||
if pick.dist > edgeDist*0.85 {
|
||||
continue
|
||||
}
|
||||
addEdge(pick)
|
||||
if !canUseEdge(pick, RoadTierLocal) {
|
||||
continue
|
||||
}
|
||||
addEdge(pick, RoadTierLocal)
|
||||
}
|
||||
|
||||
roads := make([]*Road, 0, len(selectedEdges))
|
||||
avgDim := float64(width+height) / 2
|
||||
for _, e := range selectedEdges {
|
||||
a := pois[e.a]
|
||||
b := pois[e.b]
|
||||
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||
a := pois[e.edge.a]
|
||||
b := pois[e.edge.b]
|
||||
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout, e.tier)
|
||||
imp := a.Connections + b.Connections + int(math.Round((a.ArterialWeight+b.ArterialWeight)*4))
|
||||
roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp})
|
||||
roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp, Tier: e.tier})
|
||||
}
|
||||
|
||||
return roads
|
||||
@@ -654,7 +793,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
|
||||
continue
|
||||
}
|
||||
|
||||
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout, RoadTierArterial)
|
||||
|
||||
anchor.Connections++
|
||||
edgeNode.IsExit = true
|
||||
@@ -666,6 +805,7 @@ func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int,
|
||||
End: edgeNode,
|
||||
Points: path,
|
||||
Importance: importance,
|
||||
Tier: RoadTierArterial,
|
||||
})
|
||||
usedEdgePoints = append(usedEdgePoints, image.Point{X: edgeNode.X, Y: edgeNode.Y})
|
||||
exitRoadsAdded++
|
||||
@@ -839,11 +979,33 @@ func estimateRoadTarget(settings *Settings) int {
|
||||
return settings.NumBuildings
|
||||
}
|
||||
divisor := float64(max(settings.BuildingsPerRoad, 1))
|
||||
roads := int(math.Round(float64(max(settings.NumBuildings, 1)) / divisor))
|
||||
if roads < 1 {
|
||||
roads = 1
|
||||
buildings := float64(max(settings.NumBuildings, 1))
|
||||
baseRoads := buildings / divisor
|
||||
scale := 1.0
|
||||
if buildings > 400 {
|
||||
scale *= 0.96
|
||||
}
|
||||
return roads
|
||||
if buildings > 1200 {
|
||||
scale *= 0.92
|
||||
}
|
||||
if buildings > 3000 {
|
||||
scale *= 0.88
|
||||
}
|
||||
roads := baseRoads * scale
|
||||
if buildings > 200 {
|
||||
roads += math.Pow((buildings-200.0)/divisor, 0.72) * 0.35
|
||||
}
|
||||
if buildings > 1200 {
|
||||
roads += math.Pow((buildings-1200.0)/(divisor*1.8), 0.68) * 0.22
|
||||
}
|
||||
if buildings > 1000 {
|
||||
roads *= 0.97
|
||||
}
|
||||
result := int(math.Round(roads))
|
||||
if result < 1 {
|
||||
result = 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func edgeKey(a, b int) uint64 {
|
||||
@@ -1042,7 +1204,7 @@ func bresenhamRoad(path []image.Point) []image.Point {
|
||||
}
|
||||
|
||||
// calculateRoadPath computes the path for a road including curves and bridges.
|
||||
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout) []PathPoint {
|
||||
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMask *PixelMask, wallLayout *FortificationLayout, tier RoadTier) []PathPoint {
|
||||
dx := end.X - start.X
|
||||
dy := end.Y - start.Y
|
||||
dist := math.Hypot(float64(dx), float64(dy))
|
||||
@@ -1052,77 +1214,59 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
||||
return []PathPoint{{Point: p, IsBridge: waterMask.GetPoint(p)}}
|
||||
}
|
||||
|
||||
_ = wallLayout
|
||||
|
||||
curve := clamp(curvyness, 0, 1)
|
||||
if curve <= 0 {
|
||||
if curve <= 0.01 || dist < 10 {
|
||||
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
|
||||
return toPathPoints(points, waterMask)
|
||||
}
|
||||
|
||||
// Non-linear scaling: low values stay fairly straight, high values become very winding.
|
||||
strength := math.Pow(curve, 1.35)
|
||||
if strength < 0.001 {
|
||||
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
|
||||
return toPathPoints(points, waterMask)
|
||||
}
|
||||
|
||||
baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
|
||||
controlPoints := make([]image.Point, baseControls+1)
|
||||
perpX, perpY := -float64(dy)/dist, float64(dx)/dist
|
||||
lengthScale := clamp(dist/(avgDim*0.55), 0.45, 2.4)
|
||||
strength := math.Pow(curve, 1.1)
|
||||
baseAmp := clamp(dist*(0.018+0.055*strength), 1.5, avgDim*0.06)
|
||||
|
||||
ampBase := clamp(dist*(0.01+0.13*strength*strength), 2, avgDim*0.16)
|
||||
amp1 := ampBase * (0.9 + randSrc.Float64()*0.25)
|
||||
amp2 := ampBase * (0.45 + randSrc.Float64()*0.20)
|
||||
amp3 := ampBase * (0.20 + randSrc.Float64()*0.15)
|
||||
|
||||
w1 := clamp(dist*(1.10-0.70*strength), 30, avgDim*0.95)
|
||||
w2 := clamp(dist*(0.55-0.30*strength), 16, avgDim*0.55)
|
||||
w3 := clamp(dist*(0.26-0.12*strength), 8, avgDim*0.30)
|
||||
|
||||
type wave struct {
|
||||
amplitude float64
|
||||
wavelength float64
|
||||
phase float64
|
||||
}
|
||||
|
||||
waves := []wave{
|
||||
{
|
||||
amplitude: amp1,
|
||||
wavelength: w1,
|
||||
phase: randSrc.Float64() * 2 * math.Pi,
|
||||
},
|
||||
{
|
||||
amplitude: amp2,
|
||||
wavelength: w2,
|
||||
phase: randSrc.Float64() * 2 * math.Pi,
|
||||
},
|
||||
{
|
||||
amplitude: amp3,
|
||||
wavelength: w3,
|
||||
phase: randSrc.Float64() * 2 * math.Pi,
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i <= baseControls; i++ {
|
||||
t := float64(i) / float64(baseControls)
|
||||
addControl := func(points []image.Point, t, lateral float64) []image.Point {
|
||||
x := float64(start.X) + t*float64(dx)
|
||||
y := float64(start.Y) + t*float64(dy)
|
||||
|
||||
// Keep endpoints fixed while allowing large mid-segment deflection.
|
||||
envelope := math.Pow(math.Sin(t*math.Pi), 0.78)
|
||||
offset := 0.0
|
||||
for _, w := range waves {
|
||||
angle := (dist*t/w.wavelength)*2*math.Pi + w.phase
|
||||
offset += math.Sin(angle) * w.amplitude
|
||||
}
|
||||
offset *= envelope * lengthScale
|
||||
|
||||
x += offset * perpX
|
||||
y += offset * perpY
|
||||
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
||||
x += lateral * perpX
|
||||
y += lateral * perpY
|
||||
return append(points, image.Point{X: int(math.Round(x)), Y: int(math.Round(y))})
|
||||
}
|
||||
|
||||
points := bresenhamRoad(controlPoints)
|
||||
polyline := []image.Point{{X: start.X, Y: start.Y}}
|
||||
switch tier {
|
||||
case RoadTierArterial:
|
||||
lateral := baseAmp * (0.7 + randSrc.Float64()*0.35)
|
||||
if randSrc.Float64() < 0.5 {
|
||||
lateral = -lateral
|
||||
}
|
||||
polyline = addControl(polyline, 0.33, lateral*0.45)
|
||||
polyline = addControl(polyline, 0.66, lateral)
|
||||
case RoadTierCollector:
|
||||
lateral := baseAmp * (0.9 + randSrc.Float64()*0.45)
|
||||
if randSrc.Float64() < 0.5 {
|
||||
lateral = -lateral
|
||||
}
|
||||
polyline = addControl(polyline, 0.35, lateral*0.65)
|
||||
polyline = addControl(polyline, 0.72, lateral)
|
||||
default:
|
||||
lateralA := baseAmp * (0.65 + randSrc.Float64()*0.30)
|
||||
lateralB := lateralA * (0.35 + randSrc.Float64()*0.25)
|
||||
if randSrc.Float64() < 0.5 {
|
||||
lateralA = -lateralA
|
||||
}
|
||||
if randSrc.Float64() < 0.8 {
|
||||
lateralB = lateralA * (0.35 + randSrc.Float64()*0.20)
|
||||
} else {
|
||||
lateralB = -lateralB
|
||||
}
|
||||
polyline = addControl(polyline, 0.30, lateralA)
|
||||
polyline = addControl(polyline, 0.68, lateralB)
|
||||
}
|
||||
polyline = append(polyline, image.Point{X: end.X, Y: end.Y})
|
||||
|
||||
points := bresenhamRoad(polyline)
|
||||
return toPathPoints(points, waterMask)
|
||||
}
|
||||
|
||||
@@ -1475,12 +1619,13 @@ func ensureRoadNetworkConnected(roads []*Road, settings *Settings, randSrc *rand
|
||||
b := nodes[bestB]
|
||||
a.Connections++
|
||||
b.Connections++
|
||||
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout)
|
||||
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMask, wallLayout, RoadTierCollector)
|
||||
roads = append(roads, &Road{
|
||||
Start: a,
|
||||
End: b,
|
||||
Points: path,
|
||||
Importance: a.Connections + b.Connections + 2,
|
||||
Tier: RoadTierCollector,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1698,6 +1843,7 @@ func generateGateRoads(wallLayout *FortificationLayout, settings *Settings, wate
|
||||
Points: path,
|
||||
Width: roadWidth,
|
||||
Importance: 10, // high importance so gate roads get wide treatment
|
||||
Tier: RoadTierArterial,
|
||||
})
|
||||
}
|
||||
return roads
|
||||
@@ -1825,8 +1971,93 @@ func ensureGateRoadConnections(gateRoads []*Road, allRoads []*Road, wallLayout *
|
||||
Points: path,
|
||||
Width: connW,
|
||||
Importance: 6,
|
||||
Tier: RoadTierCollector,
|
||||
})
|
||||
}
|
||||
}
|
||||
return append(allRoads, connectors...)
|
||||
}
|
||||
|
||||
func collectRoadAnchors(roads []*Road, settings *Settings, waterMask *PixelMask, width, height int) []image.Point {
|
||||
if len(roads) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||
spacing := int(math.Round(clamp((minBuildingSizePx+maxBuildingSizePx)*0.5, 8, 28)))
|
||||
if spacing < 6 {
|
||||
spacing = 6
|
||||
}
|
||||
cellSize := max(4, spacing/2)
|
||||
|
||||
type anchorCell struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
cells := make(map[anchorCell][]image.Point)
|
||||
anchors := make([]image.Point, 0, len(roads)*4)
|
||||
|
||||
addAnchor := func(p image.Point) {
|
||||
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height {
|
||||
return
|
||||
}
|
||||
if waterMask != nil && waterMask.GetPoint(p) {
|
||||
return
|
||||
}
|
||||
|
||||
cx := p.X / cellSize
|
||||
cy := p.Y / cellSize
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
key := anchorCell{x: cx + dx, y: cy + dy}
|
||||
for _, existing := range cells[key] {
|
||||
ddx := existing.X - p.X
|
||||
ddy := existing.Y - p.Y
|
||||
if ddx*ddx+ddy*ddy < spacing*spacing {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := anchorCell{x: cx, y: cy}
|
||||
cells[key] = append(cells[key], p)
|
||||
anchors = append(anchors, p)
|
||||
}
|
||||
|
||||
nodeDegree := make(map[*PointOfInterest]int, len(roads)*2)
|
||||
for _, road := range roads {
|
||||
if road.Start != nil {
|
||||
nodeDegree[road.Start]++
|
||||
}
|
||||
if road.End != nil {
|
||||
nodeDegree[road.End]++
|
||||
}
|
||||
}
|
||||
|
||||
for _, road := range roads {
|
||||
if road.Start != nil && (nodeDegree[road.Start] > 1 || !road.Start.IsExit) {
|
||||
addAnchor(image.Point{X: road.Start.X, Y: road.Start.Y})
|
||||
}
|
||||
if road.End != nil && (nodeDegree[road.End] > 1 || !road.End.IsExit) {
|
||||
addAnchor(image.Point{X: road.End.X, Y: road.End.Y})
|
||||
}
|
||||
|
||||
step := spacing
|
||||
if road.Tier == RoadTierArterial {
|
||||
step = int(math.Round(float64(spacing) * 1.35))
|
||||
}
|
||||
if step < 6 {
|
||||
step = 6
|
||||
}
|
||||
for i := step / 2; i < len(road.Points); i += step {
|
||||
if road.Points[i].IsBridge {
|
||||
continue
|
||||
}
|
||||
addAnchor(road.Points[i].Point)
|
||||
}
|
||||
}
|
||||
|
||||
return anchors
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user