Files
RPG_City_Maker_Reborn/roads.go
T

461 lines
10 KiB
Go
Raw Normal View History

2026-02-04 14:19:18 -06:00
package main
import (
"fmt"
"image"
"image/color"
"math"
"math/rand"
"sort"
2026-02-04 16:20:31 -06:00
"sync"
2026-02-04 14:19:18 -06:00
"unsafe"
)
type PointOfInterest struct {
X, Y int
Connections int
2026-02-04 15:21:45 -06:00
IsExit bool
2026-02-04 14:19:18 -06:00
}
2026-02-04 16:03:55 -06:00
type PathPoint struct {
Point image.Point
IsBridge bool
}
2026-02-04 14:19:18 -06:00
type Road struct {
2026-02-04 16:03:55 -06:00
Start, End *PointOfInterest
Width int
Points []PathPoint
Importance int
2026-02-04 14:19:18 -06:00
}
2026-02-04 16:37:50 -06:00
func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) {
2026-02-04 14:19:18 -06:00
img := image.NewRGBA(image.Rect(0, 0, width, height))
// Transparent background
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.Transparent)
}
}
2026-02-04 15:15:47 -06:00
randSrc := rand.New(rand.NewSource(seed))
2026-02-04 14:19:18 -06:00
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
2026-02-04 16:03:55 -06:00
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
2026-02-04 14:19:18 -06:00
2026-02-04 15:15:47 -06:00
pois := generatePOIs(width, height, settings, allWaterPixels, randSrc)
2026-02-04 14:19:18 -06:00
if len(pois) == 0 {
2026-02-04 16:37:50 -06:00
return nil, nil, img
2026-02-04 14:19:18 -06:00
}
2026-02-04 16:03:55 -06:00
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
2026-02-04 14:19:18 -06:00
assignRoadWidths(roads, settings)
var allRoadPixels []image.Point
2026-02-04 16:37:50 -06:00
var allBridgePixels []image.Point
2026-02-04 14:19:18 -06:00
for _, road := range roads {
2026-02-04 16:37:50 -06:00
roadPixels, bridgePixels := drawRoad(img, road.Points, roadColor, bridgeColor, road.Width)
2026-02-04 14:19:18 -06:00
allRoadPixels = append(allRoadPixels, roadPixels...)
2026-02-04 16:37:50 -06:00
allBridgePixels = append(allBridgePixels, bridgePixels...)
2026-02-04 14:19:18 -06:00
}
2026-02-04 16:37:50 -06:00
return allRoadPixels, allBridgePixels, img
2026-02-04 14:19:18 -06:00
}
2026-02-04 15:15:47 -06:00
func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest {
2026-02-04 14:19:18 -06:00
numPOIs := settings.NumRoads / 2
if numPOIs == 0 {
return nil
}
2026-02-04 14:50:36 -06:00
waterMap := make(map[image.Point]bool)
for _, p := range allWaterPixels {
waterMap[p] = true
}
2026-02-04 14:19:18 -06:00
numExits := settings.RoadExits
if numExits > settings.NumRoads {
numExits = settings.NumRoads
}
2026-02-04 14:50:36 -06:00
pois := make([]*PointOfInterest, 0, numPOIs)
2026-02-04 14:19:18 -06:00
centerX := width / 2
centerY := height / 2
// Distribution affects the radius
maxRadius := math.Min(float64(width)/2, float64(height)/2)
radius := maxRadius * (settings.RoadDistribution / 100.0)
for i := 0; i < numPOIs; i++ {
2026-02-04 14:50:36 -06:00
var x, y int
found := false
for j := 0; j < 100; j++ { // 100 retries to find a land spot
if i < numExits {
2026-02-04 15:15:47 -06:00
side := randSrc.Intn(4)
2026-02-04 14:50:36 -06:00
switch side {
case 0: // Top
2026-02-04 15:15:47 -06:00
x = randSrc.Intn(width)
2026-02-04 14:50:36 -06:00
y = 0
case 1: // Bottom
2026-02-04 15:15:47 -06:00
x = randSrc.Intn(width)
2026-02-04 14:50:36 -06:00
y = height - 1
case 2: // Left
x = 0
2026-02-04 15:15:47 -06:00
y = randSrc.Intn(height)
2026-02-04 14:50:36 -06:00
case 3: // Right
x = width - 1
2026-02-04 15:15:47 -06:00
y = randSrc.Intn(height)
2026-02-04 14:50:36 -06:00
}
} else {
2026-02-04 15:15:47 -06:00
angle := randSrc.Float64() * 2 * math.Pi
r := randSrc.Float64() * radius
2026-02-04 14:50:36 -06:00
x = int(float64(centerX) + r*math.Cos(angle))
y = int(float64(centerY) + r*math.Sin(angle))
2026-02-04 14:19:18 -06:00
}
2026-02-04 14:50:36 -06:00
if !waterMap[image.Point{X: x, Y: y}] {
found = true
break
}
}
if found {
2026-02-04 15:21:45 -06:00
isExit := i < numExits
pois = append(pois, &PointOfInterest{X: x, Y: y, IsExit: isExit})
2026-02-04 14:50:36 -06:00
}
2026-02-04 14:19:18 -06:00
}
return pois
}
2026-02-04 16:03:55 -06:00
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road {
2026-02-04 14:19:18 -06:00
if len(pois) < 2 {
return nil
}
var roads []*Road
2026-02-04 16:20:31 -06:00
var roadChan = make(chan *Road)
var wg sync.WaitGroup
2026-02-04 14:19:18 -06:00
visited := make(map[*PointOfInterest]bool)
existingRoads := make(map[string]bool)
// Find the center-most POI
centerX := width / 2
centerY := height / 2
var startNode *PointOfInterest
minDist := -1.0
for _, poi := range pois {
2026-02-04 14:50:36 -06:00
if poi == nil {
continue
}
2026-02-04 14:19:18 -06:00
dist := math.Sqrt(math.Pow(float64(poi.X-centerX), 2) + math.Pow(float64(poi.Y-centerY), 2))
if startNode == nil || dist < minDist {
minDist = dist
startNode = poi
}
}
2026-02-04 14:50:36 -06:00
if startNode == nil {
return nil
}
2026-02-04 14:19:18 -06:00
visited[startNode] = true
2026-02-04 15:39:09 -06:00
avgDim := float64(width+height) / 2.0
numControlPoints := max(int(avgDim*0.03), 60)
2026-02-04 14:19:18 -06:00
for len(visited) < len(pois) {
var closest *PointOfInterest
var fromNode *PointOfInterest
minDist := -1.0
for poi := range visited {
for _, other := range pois {
2026-02-04 14:50:36 -06:00
if poi == nil || other == nil {
continue
}
2026-02-04 14:19:18 -06:00
if !visited[other] {
dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2))
// Check if road exists
key := fmt.Sprintf("%p-%p", poi, other)
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
key = fmt.Sprintf("%p-%p", other, poi)
}
if existingRoads[key] {
continue
}
2026-02-04 15:21:45 -06:00
// Don't connect two exit points
if poi.IsExit && other.IsExit {
continue
}
2026-02-04 14:19:18 -06:00
if closest == nil || dist < minDist {
minDist = dist
closest = other
fromNode = poi
}
}
}
}
if closest != nil {
visited[closest] = true
fromNode.Connections++
closest.Connections++
// Add road to existing roads map
key := fmt.Sprintf("%p-%p", fromNode, closest)
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
key = fmt.Sprintf("%p-%p", closest, fromNode)
}
existingRoads[key] = true
2026-02-04 16:20:31 -06:00
wg.Add(1)
go func(fromNode, closest *PointOfInterest) {
defer wg.Done()
localRand := rand.New(rand.NewSource(randSrc.Int63()))
path := calculateRoadPath(fromNode, closest, settings.RoadCurvyness/100.0, avgDim, localRand, numControlPoints, allWaterPixels)
roadChan <- &Road{
Start: fromNode,
End: closest,
Points: path,
}
}(fromNode, closest)
2026-02-04 14:50:36 -06:00
} else {
// No more reachable POIs
break
2026-02-04 14:19:18 -06:00
}
}
2026-02-04 16:20:31 -06:00
go func() {
wg.Wait()
close(roadChan)
}()
for road := range roadChan {
roads = append(roads, road)
}
2026-02-04 14:19:18 -06:00
for _, road := range roads {
road.Importance = road.Start.Connections + road.End.Connections
}
return roads
}
func assignRoadWidths(roads []*Road, settings *Settings) {
if len(roads) == 0 {
return
}
sort.Slice(roads, func(i, j int) bool {
return roads[i].Importance > roads[j].Importance
})
minWidth := settings.MinRoadWidth
maxWidth := settings.MaxRoadWidth
widthStep := 0.0
if len(roads) > 1 {
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
}
for i, road := range roads {
road.Width = int(maxWidth - float64(i)*widthStep)
}
}
2026-02-04 16:37:50 -06:00
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
2026-02-04 15:39:09 -06:00
var roadPixels []image.Point
2026-02-04 16:37:50 -06:00
var bridgePixels []image.Point
2026-02-04 15:39:09 -06:00
for i := 0; i < len(points)-1; i++ {
2026-02-04 16:03:55 -06:00
p1 := points[i]
p2 := points[i+1]
c := roadColor
2026-02-04 16:37:50 -06:00
isBridge := p1.IsBridge && p2.IsBridge
if isBridge {
2026-02-04 16:03:55 -06:00
c = bridgeColor
}
linePoints := drawLine(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, c, width)
2026-02-04 16:37:50 -06:00
if isBridge {
bridgePixels = append(bridgePixels, linePoints...)
} else {
roadPixels = append(roadPixels, linePoints...)
}
2026-02-04 14:19:18 -06:00
}
2026-02-04 16:37:50 -06:00
return roadPixels, bridgePixels
2026-02-04 15:39:09 -06:00
}
func bresenhamRoad(path []image.Point) []image.Point {
if len(path) < 2 {
return path
}
var fullPath []image.Point
for i := 0; i < len(path)-1; i++ {
p1, p2 := path[i], path[i+1]
dx, dy := p2.X-p1.X, p2.Y-p1.Y
absDx, absDy := int(math.Abs(float64(dx))), int(math.Abs(float64(dy)))
sx, sy := 1, 1
if dx < 0 {
sx = -1
}
if dy < 0 {
sy = -1
}
err := absDx - absDy
x, y := p1.X, p1.Y
for {
fullPath = append(fullPath, image.Point{X: x, Y: y})
if x == p2.X && y == p2.Y {
break
}
e2 := 2 * err
if e2 > -absDy {
err -= absDy
x += sx
}
if e2 < absDx {
err += absDx
y += sy
}
}
}
return fullPath
}
2026-02-04 16:03:55 -06:00
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint {
2026-02-04 15:39:09 -06:00
dx := end.X - start.X
dy := end.Y - start.Y
dist := math.Sqrt(float64(dx*dx + dy*dy))
2026-02-04 16:03:55 -06:00
waterMap := make(map[image.Point]bool)
for _, p := range allWaterPixels {
waterMap[p] = true
}
2026-02-04 15:39:09 -06:00
if dist == 0 {
2026-02-04 16:03:55 -06:00
return []PathPoint{{Point: image.Point{X: start.X, Y: start.Y}, IsBridge: waterMap[image.Point{X: start.X, Y: start.Y}]}}
2026-02-04 15:39:09 -06:00
}
2026-02-04 15:50:53 -06:00
// Adjust curviness based on distance
distanceFactor := math.Min(1.0, dist/(avgDim*0.5))
adjustedCurvyness := curvyness * distanceFactor
if adjustedCurvyness == 0 {
2026-02-04 16:03:55 -06:00
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
pathPoints := make([]PathPoint, len(points))
for i, p := range points {
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]}
}
return pathPoints
2026-02-04 15:39:09 -06:00
}
type wave struct {
amplitude float64
numWaves float64
phase float64
}
2026-02-04 15:50:53 -06:00
waves := make([]wave, 2)
amp := (avgDim / 10.0) * adjustedCurvyness
2026-02-04 15:39:09 -06:00
mainWavelength := avgDim / 4.0
if mainWavelength < 1 {
mainWavelength = 1
}
2026-02-04 15:50:53 -06:00
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
2026-02-04 15:39:09 -06:00
2026-02-04 15:50:53 -06:00
// Main wave
waves[0] = wave{
amplitude: amp,
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
phase: randSrc.Float64() * 2 * math.Pi,
}
// Smaller wave for detours
waves[1] = wave{
amplitude: amp / 4,
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
phase: randSrc.Float64() * 2 * math.Pi,
2026-02-04 15:39:09 -06:00
}
controlPoints := make([]image.Point, numControlPoints+1)
for i := 0; i <= numControlPoints; i++ {
t := float64(i) / float64(numControlPoints)
x := float64(start.X) + t*float64(dx)
y := float64(start.Y) + t*float64(dy)
2026-02-04 16:03:55 -06:00
p := image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
if !waterMap[p] {
perpX, perpY := -float64(dy)/dist, float64(dx)/dist
totalOffset := 0.0
for _, w := range waves {
totalOffset += math.Sin(t*w.numWaves*2*math.Pi+w.phase) * w.amplitude
}
totalOffset *= math.Sin(t * math.Pi)
x += totalOffset * perpX
y += totalOffset * perpY
2026-02-04 15:39:09 -06:00
}
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
}
2026-02-04 16:03:55 -06:00
points := bresenhamRoad(controlPoints)
pathPoints := make([]PathPoint, len(points))
for i, p := range points {
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]}
}
return pathPoints
2026-02-04 14:19:18 -06:00
}
// Bresenham's line algorithm for drawing segments of the curve
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
var points []image.Point
dx := abs(x1 - x0)
dy := -abs(y1 - y0)
sx := -1
if x0 < x1 {
sx = 1
}
sy := -1
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
for i := -width / 2; i <= width/2; i++ {
for j := -width / 2; j <= width/2; j++ {
px := x0 + i
py := y0 + j
if img.Bounds().Min.X <= px && px < img.Bounds().Max.X && img.Bounds().Min.Y <= py && py < img.Bounds().Max.Y {
img.Set(px, py, col)
points = append(points, image.Point{X: px, Y: py})
}
}
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
return points
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}