diff --git a/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt b/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt
new file mode 100644
index 000000000..b42cbfbd1
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.player
+
+import com.lambda.config.ConfigEditor
+import com.lambda.config.ConfigEditor.editSetting
+import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.withEdits
+import com.lambda.context.SafeContext
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafe
+import net.minecraft.util.math.Vec3d
+import java.awt.Color
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.max
+import kotlin.math.roundToInt
+import kotlin.math.sin
+
+@Suppress("unused")
+object LineLock : Module(
+ name = "LineLock",
+ description = "Locks the player's yaw to a line and steers toward a point ahead on the line",
+ tag = ModuleTag.Companion.PLAYER,
+) {
+ private const val YAW_SNAP_INCREMENT = 45.0 // snap the axis to the nearest 45° heading
+ private const val YAW_SNAP_BIAS = 1.0 // nudge before rounding so exact half-steps snap consistently
+ private const val FULL_CIRCLE_DEGREES = 360.0
+ private const val YAW_DEGREES_OFFSET = 90.0 // atan2 (east = 0°) to Minecraft yaw (south = 0°)
+ private const val MIN_LOOKAHEAD = 2.0 // floor for the look-ahead distance so steering stays stable at low speed
+ private const val ON_LINE_THRESHOLD = 0.1 // how close to the line counts as "on it"
+ private const val TOLERANCE = 0.1 // tolerance for classifying a direction as diagonal vs. straight
+ private const val RENDER_LINE_HALF_LENGTH = 500.0
+
+ private val distance by setting("Distance", 5.0, 0.0..10.0, 0.1, "Look-ahead distance along the axis to steer toward")
+ private val renderLine by setting("Render Line", true, "Render the axis line the yaw is snapping to")
+ private val lineColor by setting("Line Color", Color(0, 255, 0, 255)) { renderLine }
+
+ private var startingYaw = 0.0
+ private var lineOrigin: Vec3d = Vec3d.ZERO
+ private var lineDirection: Vec3d = Vec3d.ZERO
+
+ init {
+ setDefaultAutomationConfig()
+ .withEdits {
+ ConfigEditor.hideAllExcept(::rotationConfig)
+ rotationConfig::rotationMode.editSetting { defaultValue(RotationMode.Lock) }
+ }
+
+ // When enabled we lock onto a straight "rail" and steer the camera down it:
+ //
+ // lineOrigin ●─────────●──────────► lineDirection (the way the rail points)
+ // ╱ closestPoint (rail spot nearest you)
+ // you ●──╯ → we aim a bit further down the rail
+ // (lookaheadPoint) so you curve back on
+ onEnable {
+ // Snap our facing to the nearest 45° compass direction (N, NE, E, SE, ...)
+ // and lock the rail in from where we're standing.
+ val nearestNotch = ((player.yaw + YAW_SNAP_BIAS) / YAW_SNAP_INCREMENT).roundToInt() // which 45° notch we're closest to
+ startingYaw = nearestNotch * YAW_SNAP_INCREMENT % FULL_CIRCLE_DEGREES // back to degrees, kept in 0–360
+ lineOrigin = player.pos
+ lineDirection = directionForHeading(startingYaw)
+ adjustYaw()
+ }
+
+ // Every tick: find the rail spot nearest you, pick a point a little ahead of it,
+ // and turn to face that point.
+ listen {
+ if (player.velocity == Vec3d.ZERO) return@listen
+ adjustYaw()
+ }
+
+ immediateRenderer("HighwayFollower Renderer") {
+ if (!renderLine) return@immediateRenderer
+ runSafe {
+ val playerPos = Vec3d(player.x, lineOrigin.y, player.z)
+ val closestPoint = closestPointOnLine(playerPos, lineOrigin, lineDirection)
+ val lineStart = closestPoint.subtract(lineDirection.multiply(RENDER_LINE_HALF_LENGTH))
+ val lineEnd = closestPoint.add(lineDirection.multiply(RENDER_LINE_HALF_LENGTH))
+ line(lineStart, lineEnd, lineColor)
+ }
+ }
+ }
+
+ private fun SafeContext.adjustYaw() {
+ val playerPos = Vec3d(player.x, lineOrigin.y, player.z)
+ val closestPoint = closestPointOnLine(playerPos, lineOrigin, lineDirection)
+ val distanceToLine = Vec3d(closestPoint.x, playerPos.y, closestPoint.z).distanceTo(playerPos)
+ val targetYaw = if (distanceToLine < ON_LINE_THRESHOLD) {
+ // Already on the line - hold the snapped axis yaw.
+ startingYaw
+ } else {
+ // Off the line - steer toward a point ahead so we converge back onto it.
+ yawTowardsLine(closestPoint)
+ }
+ rotationRequest { yaw(targetYaw) }.submit()
+ }
+
+ private fun SafeContext.yawTowardsLine(closestPoint: Vec3d): Double {
+ // Aim a little further down the rail than the nearest spot, so we curve back onto it.
+ val lookaheadPoint = closestPoint.add(lineDirection.multiply(max(MIN_LOOKAHEAD, player.velocity.length() * distance)))
+ return headingToward(lookaheadPoint)
+ }
+
+ // What compass heading (Minecraft yaw) points from the player toward this spot on the map?
+ private fun SafeContext.headingToward(point: Vec3d): Double {
+ val eastOffset = point.x - player.pos.x
+ val southOffset = point.z - player.pos.z
+ return Math.toDegrees(atan2(southOffset, eastOffset)) - YAW_DEGREES_OFFSET
+ }
+
+ // Turn a compass heading (degrees) into a unit arrow pointing that way on the map.
+ private fun directionForHeading(heading: Double): Vec3d {
+ val radians = Math.toRadians(heading)
+ return Vec3d(-sin(radians), 0.0, cos(radians)).normalize()
+ }
+
+ private fun closestPointOnLine(point: Vec3d, lineOrigin: Vec3d, lineDirection: Vec3d): Vec3d {
+ val originToPoint = point.subtract(lineOrigin)
+ val projection = originToPoint.dotProduct(lineDirection.normalize())
+ return snapToAxis(lineOrigin.add(lineDirection.multiply(projection)), lineOrigin, lineDirection)
+ }
+
+ private fun snapToAxis(point: Vec3d, lineOrigin: Vec3d, lineDirection: Vec3d): Vec3d {
+ var offset = point.subtract(lineOrigin)
+
+ if (abs(abs(lineDirection.x) - abs(lineDirection.z)) < TOLERANCE) { // diagonal
+ val magnitude = (abs(offset.x) + abs(offset.z)) / 2
+ val x = if (lineDirection.x > 0) magnitude else -magnitude
+ val z = if (lineDirection.z > 0) magnitude else -magnitude
+ offset = Vec3d(x, 0.0, z)
+ } else { // straight
+ val x = if (abs(lineDirection.x) < TOLERANCE) 0.0 else offset.x
+ val z = if (abs(lineDirection.z) < TOLERANCE) 0.0 else offset.z
+ if (x == 0.0) {
+ offset = Vec3d(0.0, 0.0, z)
+ } else if (z == 0.0) {
+ offset = Vec3d(x, 0.0, 0.0)
+ }
+ }
+ return lineOrigin.add(offset)
+ }
+}
\ No newline at end of file