basic road generation added

This commit is contained in:
Grimsace
2026-02-04 14:19:18 -06:00
parent 05887fde26
commit b9173dd1f3
3 changed files with 478 additions and 46 deletions
+280
View File
@@ -0,0 +1,280 @@
package main
import (
"fmt"
"image"
"image/color"
"math"
"math/rand"
"sort"
"unsafe"
)
type PointOfInterest struct {
X, Y int
Connections int
}
type Road struct {
Start, End, Control *PointOfInterest
Width int
Points []image.Point
Importance int
}
func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image) ([]image.Point, *image.RGBA) {
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)
}
}
rand.Seed(settings.Seed)
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
pois := generatePOIs(width, height, settings)
if len(pois) == 0 {
return nil, img
}
roads := connectPOIs(pois, width, height, settings)
assignRoadWidths(roads, settings)
var allRoadPixels []image.Point
for _, road := range roads {
roadPixels := drawCurve(img, road.Start, road.End, road.Control, roadColor, road.Width)
allRoadPixels = append(allRoadPixels, roadPixels...)
}
return allRoadPixels, img
}
func generatePOIs(width, height int, settings *Settings) []*PointOfInterest {
numPOIs := settings.NumRoads / 2
if numPOIs == 0 {
return nil
}
numExits := settings.RoadExits
if numExits > settings.NumRoads {
numExits = settings.NumRoads
}
pois := make([]*PointOfInterest, numPOIs)
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++ {
// Handle exits
if i < numExits {
side := rand.Intn(4)
var x, y int
switch side {
case 0: // Top
x = rand.Intn(width)
y = 0
case 1: // Bottom
x = rand.Intn(width)
y = height - 1
case 2: // Left
x = 0
y = rand.Intn(height)
case 3: // Right
x = width - 1
y = rand.Intn(height)
}
pois[i] = &PointOfInterest{X: x, Y: y}
continue
}
angle := rand.Float64() * 2 * math.Pi
r := rand.Float64() * radius
x := int(float64(centerX) + r*math.Cos(angle))
y := int(float64(centerY) + r*math.Sin(angle))
pois[i] = &PointOfInterest{X: x, Y: y}
}
return pois
}
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings) []*Road {
if len(pois) < 2 {
return nil
}
var roads []*Road
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 {
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
}
}
visited[startNode] = true
for len(visited) < len(pois) {
var closest *PointOfInterest
var fromNode *PointOfInterest
minDist := -1.0
for poi := range visited {
for _, other := range pois {
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
}
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
// Create control point for curve
midX := (fromNode.X + closest.X) / 2
midY := (fromNode.Y + closest.Y) / 2
dist := math.Sqrt(math.Pow(float64(fromNode.X-closest.X), 2) + math.Pow(float64(fromNode.Y-closest.Y), 2))
offset := dist * (settings.RoadCurvyness / 100.0) * (rand.Float64() - 0.5)
controlX := int(float64(midX) + offset)
controlY := int(float64(midY) + offset)
roads = append(roads, &Road{
Start: fromNode,
End: closest,
Control: &PointOfInterest{X: controlX, Y: controlY},
})
}
}
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)
}
}
func drawCurve(img *image.RGBA, p0, p1, p2 *PointOfInterest, col color.Color, width int) []image.Point {
var points []image.Point
var lastX, lastY int = -1, -1
for t := 0.0; t <= 1.0; t += 0.01 {
x := (1-t)*(1-t)*float64(p0.X) + 2*(1-t)*t*float64(p2.X) + t*t*float64(p1.X)
y := (1-t)*(1-t)*float64(p0.Y) + 2*(1-t)*t*float64(p2.Y) + t*t*float64(p1.Y)
if lastX != -1 {
linePoints := drawLine(img, lastX, lastY, int(x), int(y), col, width)
points = append(points, linePoints...)
}
lastX, lastY = int(x), int(y)
}
return points
}
// 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
}