All files / web/src/app/arcade/complement-race/lib RailroadTrackGenerator.ts

0% Statements 0/246
0% Branches 0/1
0% Functions 0/1
0% Lines 0/246

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
/**
 * Railroad Track Generator
 *
 * Generates dynamic curved railroad tracks with proper ballast, ties, and rails.
 * Based on the original Python implementation with SVG path generation.
 */

export interface Waypoint {
  x: number
  y: number
}

export interface TrackElements {
  ballastPath: string
  referencePath: string
  ties: Array<{ x1: number; y1: number; x2: number; y2: number }>
  leftRailPath: string
  rightRailPath: string
}

export class RailroadTrackGenerator {
  private viewWidth: number
  private viewHeight: number

  constructor(viewWidth = 800, viewHeight = 600) {
    this.viewWidth = viewWidth
    this.viewHeight = viewHeight
  }

  /**
   * Generate complete track elements for rendering
   */
  generateTrack(routeNumber: number = 1): TrackElements {
    const waypoints = this.generateTrackWaypoints(routeNumber)
    const pathData = this.generateSmoothPath(waypoints)

    return {
      ballastPath: pathData,
      referencePath: pathData,
      ties: [],
      leftRailPath: '',
      rightRailPath: '',
    }
  }

  /**
   * Seeded random number generator for deterministic randomness
   */
  private seededRandom(seed: number): number {
    const x = Math.sin(seed) * 10000
    return x - Math.floor(x)
  }

  /**
   * Generate waypoints for track with controlled randomness
   * Based on route number for variety across different routes
   */
  private generateTrackWaypoints(routeNumber: number): Waypoint[] {
    // Base waypoints - tracks span from left tunnel (x=20) to right tunnel (x=780)
    // viewBox is "-50 -50 900 700", so x ranges from -50 to 850
    const baseWaypoints: Waypoint[] = [
      { x: 20, y: 300 }, // Start at left tunnel center
      { x: 120, y: 260 }, // Emerging from left tunnel
      { x: 240, y: 200 }, // Climb into hills
      { x: 380, y: 170 }, // Mountain pass
      { x: 520, y: 220 }, // Descent to valley
      { x: 660, y: 160 }, // Bridge over canyon
      { x: 780, y: 300 }, // Enter right tunnel center
    ]

    // Add deterministic randomness based on route number (but keep start/end fixed)
    return baseWaypoints.map((point, index) => {
      if (index === 0 || index === baseWaypoints.length - 1) {
        return point // Keep start/end points fixed
      }

      // Use seeded randomness for consistent track per route
      const seed1 = routeNumber * 12.9898 + index * 78.233
      const seed2 = routeNumber * 43.789 + index * 67.123
      const randomX = (this.seededRandom(seed1) - 0.5) * 60 // ±30 pixels
      const randomY = (this.seededRandom(seed2) - 0.5) * 80 // ±40 pixels

      return {
        x: point.x + randomX,
        y: point.y + randomY,
      }
    })
  }

  /**
   * Generate smooth cubic bezier curves through waypoints
   */
  private generateSmoothPath(waypoints: Waypoint[]): string {
    if (waypoints.length < 2) return ''

    let pathData = `M ${waypoints[0].x} ${waypoints[0].y}`

    for (let i = 1; i < waypoints.length; i++) {
      const current = waypoints[i]
      const previous = waypoints[i - 1]

      // Calculate control points for smooth curves
      const dx = current.x - previous.x
      const dy = current.y - previous.y

      const cp1x = previous.x + dx * 0.3
      const cp1y = previous.y + dy * 0.2
      const cp2x = current.x - dx * 0.3
      const cp2y = current.y - dy * 0.2

      pathData += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${current.x} ${current.y}`
    }

    return pathData
  }

  /**
   * Generate gentle curves through densely sampled waypoints
   * Uses very gentle control points to avoid wobbles in straight sections
   */
  private generateGentlePath(waypoints: Waypoint[]): string {
    if (waypoints.length < 2) return ''

    let pathData = `M ${waypoints[0].x} ${waypoints[0].y}`

    for (let i = 1; i < waypoints.length; i++) {
      const current = waypoints[i]
      const previous = waypoints[i - 1]

      // Use extremely gentle control points for very dense sampling
      const dx = current.x - previous.x
      const dy = current.y - previous.y

      const cp1x = previous.x + dx * 0.33
      const cp1y = previous.y + dy * 0.33
      const cp2x = current.x - dx * 0.33
      const cp2y = current.y - dy * 0.33

      pathData += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${current.x} ${current.y}`
    }

    return pathData
  }

  /**
   * Generate railroad ties and rails along the path
   * This requires an SVG path element to measure
   */
  generateTiesAndRails(pathElement: SVGPathElement): {
    ties: Array<{ x1: number; y1: number; x2: number; y2: number }>
    leftRailPath: string
    rightRailPath: string
  } {
    const pathLength = pathElement.getTotalLength()
    const tieSpacing = 12 // Distance between ties in pixels
    const gaugeWidth = 15 // Standard gauge (tie extends 15px each side)
    const tieCount = Math.floor(pathLength / tieSpacing)

    const ties: Array<{ x1: number; y1: number; x2: number; y2: number }> = []

    // Generate ties at normal spacing
    for (let i = 0; i < tieCount; i++) {
      const distance = i * tieSpacing
      const point = pathElement.getPointAtLength(distance)

      // Calculate perpendicular angle for tie orientation
      const nextDistance = Math.min(distance + 2, pathLength)
      const nextPoint = pathElement.getPointAtLength(nextDistance)
      const angle = Math.atan2(nextPoint.y - point.y, nextPoint.x - point.x)
      const perpAngle = angle + Math.PI / 2

      // Calculate tie end points
      const leftX = point.x + Math.cos(perpAngle) * gaugeWidth
      const leftY = point.y + Math.sin(perpAngle) * gaugeWidth
      const rightX = point.x - Math.cos(perpAngle) * gaugeWidth
      const rightY = point.y - Math.sin(perpAngle) * gaugeWidth

      // Store tie
      ties.push({ x1: leftX, y1: leftY, x2: rightX, y2: rightY })
    }

    // Generate rail paths as smooth curves (not polylines)
    // Sample points along the path and create offset waypoints
    const railSampling = 2 // Sample every 2 pixels for waypoints (very dense sampling for smooth curves)
    const sampleCount = Math.floor(pathLength / railSampling)

    const leftRailWaypoints: Waypoint[] = []
    const rightRailWaypoints: Waypoint[] = []

    for (let i = 0; i <= sampleCount; i++) {
      const distance = Math.min(i * railSampling, pathLength)
      const point = pathElement.getPointAtLength(distance)

      // Calculate perpendicular angle with longer lookahead for smoother curves
      const nextDistance = Math.min(distance + 8, pathLength)
      const nextPoint = pathElement.getPointAtLength(nextDistance)
      const angle = Math.atan2(nextPoint.y - point.y, nextPoint.x - point.x)
      const perpAngle = angle + Math.PI / 2

      // Calculate offset positions for rails
      const leftX = point.x + Math.cos(perpAngle) * gaugeWidth
      const leftY = point.y + Math.sin(perpAngle) * gaugeWidth
      const rightX = point.x - Math.cos(perpAngle) * gaugeWidth
      const rightY = point.y - Math.sin(perpAngle) * gaugeWidth

      leftRailWaypoints.push({ x: leftX, y: leftY })
      rightRailWaypoints.push({ x: rightX, y: rightY })
    }

    // Generate smooth curved paths through the rail waypoints with gentle control points
    const leftRailPath = this.generateGentlePath(leftRailWaypoints)
    const rightRailPath = this.generateGentlePath(rightRailWaypoints)

    return { ties, leftRailPath, rightRailPath }
  }

  /**
   * Calculate train position and rotation along path
   */
  getTrainTransform(
    pathElement: SVGPathElement,
    progress: number // 0-100%
  ): { x: number; y: number; rotation: number } {
    const pathLength = pathElement.getTotalLength()
    const targetLength = (progress / 100) * pathLength

    // Get exact point on curved path
    const point = pathElement.getPointAtLength(targetLength)

    // Calculate rotation based on path direction
    const lookAheadDistance = Math.min(5, pathLength - targetLength)
    const nextPoint = pathElement.getPointAtLength(targetLength + lookAheadDistance)

    // Calculate angle between current and next point
    const deltaX = nextPoint.x - point.x
    const deltaY = nextPoint.y - point.y
    const angleRadians = Math.atan2(deltaY, deltaX)
    const angleDegrees = angleRadians * (180 / Math.PI)

    return {
      x: point.x,
      y: point.y,
      rotation: angleDegrees,
    }
  }
}