1+ /*
2+ * Copyright 2026 Lambda
3+ *
4+ * This program is free software: you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation, either version 3 of the License, or
7+ * (at your option) any later version.
8+ *
9+ * This program is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU General Public License
15+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16+ */
17+
18+ package com.lambda.module.modules.player
19+
20+ import com.lambda.config.ConfigEditor.editSetting
21+ import com.lambda.config.ConfigEditor.hideAllExcept
22+ import com.lambda.config.Group
23+ import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
24+ import com.lambda.config.withEdits
25+ import com.lambda.context.SafeContext
26+ import com.lambda.event.events.TickEvent
27+ import com.lambda.event.listener.SafeListener.Companion.listen
28+ import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
29+ import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
30+ import com.lambda.interaction.managers.rotating.RotationMode
31+ import com.lambda.module.Module
32+ import com.lambda.module.tag.ModuleTag
33+ import com.lambda.threading.runSafe
34+ import net.minecraft.util.math.Vec3d
35+ import java.awt.Color
36+ import kotlin.math.abs
37+ import kotlin.math.atan2
38+ import kotlin.math.cos
39+ import kotlin.math.max
40+ import kotlin.math.roundToInt
41+ import kotlin.math.sin
42+
43+ @Suppress(" unused" )
44+ object LineLock : Module(
45+ name = " LineLock" ,
46+ description = " Locks the player's yaw to a line and steers toward a point ahead on the line" ,
47+ tag = ModuleTag .PLAYER ,
48+ ) {
49+ private const val YAW_SNAP_BIAS = 1.0 // nudge before rounding so exact half-steps snap consistently
50+ private const val FULL_CIRCLE_DEGREES = 360.0
51+ private const val YAW_DEGREES_OFFSET = 90.0 // atan2 (east = 0°) to Minecraft yaw (south = 0°)
52+ private const val TOLERANCE = 0.1 // tolerance for classifying a direction as diagonal vs. straight
53+ private const val RENDER_LINE_HALF_LENGTH = 500.0
54+
55+ private const val ADVANCED_GROUP = " Advanced"
56+
57+ private val renderLine by setting(" Render Line" , true , " Render the axis line the yaw is snapping to" )
58+ private val lineColor by setting(" Line Color" , Color (0 , 255 , 0 , 255 )) { renderLine }
59+
60+ @Group(ADVANCED_GROUP ) private val correctionSmoothness by setting(" Correction Smoothness" , 5.0 , 0.0 .. 10.0 , 0.1 , " Scales how far ahead down the axis to aim by your speed; higher curves back onto the line more gently" )
61+ @Group(ADVANCED_GROUP ) private val yawSnapIncrement by setting(" Yaw Snap Increment" , 45.0 , 1.0 .. 90.0 , 1.0 , " Snap the locked axis to the nearest multiple of this heading, in degrees" )
62+ @Group(ADVANCED_GROUP ) private val onLineThreshold by setting(" On Line Threshold" , 0.1 , 0.0 .. 1.0 , 0.01 , " How close to the line counts as \" on it\" before holding the snapped yaw" )
63+ @Group(ADVANCED_GROUP ) private val minLookahead by setting(" Min Lookahead" , 2.0 , 0.0 .. 10.0 , 0.1 , " Floor for the look-ahead distance so steering stays stable at low speed" )
64+
65+ private var startingYaw = 0.0
66+ private var lineOrigin: Vec3d = Vec3d .ZERO
67+ private var lineDirection: Vec3d = Vec3d .ZERO
68+
69+ init {
70+ setDefaultAutomationConfig()
71+ .withEdits {
72+ hideAllExcept(::rotationConfig)
73+ rotationConfig::rotationMode.editSetting { defaultValue(RotationMode .Lock ) }
74+ }
75+
76+ // When enabled we lock onto a straight "rail" and steer the camera down it:
77+ //
78+ // lineOrigin ●─────────●──────────► lineDirection (the way the rail points)
79+ // ╱ closestPoint (rail spot nearest you)
80+ // you ●──╯ → we aim a bit further down the rail
81+ // (lookaheadPoint) so you curve back on
82+ onEnable {
83+ // Snap our facing to the nearest Yaw Snap Increment (e.g. 45° -> N, NE, E, SE, ...)
84+ // and lock the rail in from where we're standing.
85+ val nearestNotch = ((player.yaw + YAW_SNAP_BIAS ) / yawSnapIncrement).roundToInt() // which notch we're closest to
86+ startingYaw = nearestNotch * yawSnapIncrement % FULL_CIRCLE_DEGREES // back to degrees, kept in 0–360
87+ lineOrigin = player.pos
88+ lineDirection = directionForHeading(startingYaw)
89+ adjustYaw()
90+ }
91+
92+ // Every tick: find the rail spot nearest you, pick a point a little ahead of it,
93+ // and turn to face that point.
94+ listen<TickEvent .Pre > {
95+ if (player.velocity == Vec3d .ZERO ) return @listen
96+ adjustYaw()
97+ }
98+
99+ immediateRenderer(" LineLock Renderer" ) {
100+ if (! renderLine) return @immediateRenderer
101+ runSafe {
102+ val playerPos = Vec3d (player.x, lineOrigin.y, player.z)
103+ val closestPoint = closestPointOnLine(playerPos, lineOrigin, lineDirection)
104+ val lineStart = closestPoint.subtract(lineDirection.multiply(RENDER_LINE_HALF_LENGTH ))
105+ val lineEnd = closestPoint.add(lineDirection.multiply(RENDER_LINE_HALF_LENGTH ))
106+ line(lineStart, lineEnd, lineColor)
107+ }
108+ }
109+ }
110+
111+ private fun SafeContext.adjustYaw () {
112+ val playerPos = Vec3d (player.x, lineOrigin.y, player.z)
113+ val closestPoint = closestPointOnLine(playerPos, lineOrigin, lineDirection)
114+ val distanceToLine = Vec3d (closestPoint.x, playerPos.y, closestPoint.z).distanceTo(playerPos)
115+ val targetYaw = if (distanceToLine < onLineThreshold) {
116+ // Already on the line - hold the snapped axis yaw.
117+ startingYaw
118+ } else {
119+ // Off the line - steer toward a point ahead so we converge back onto it.
120+ yawTowardsLine(closestPoint)
121+ }
122+ rotationRequest { yaw(targetYaw) }.submit()
123+ }
124+
125+ private fun SafeContext.yawTowardsLine (closestPoint : Vec3d ): Double {
126+ // Aim further down the rail than the nearest spot, so we curve back onto it. How far ahead
127+ // scales with our speed (times Correction Smoothness), but never less than Min Lookahead.
128+ val lookaheadPoint = closestPoint.add(lineDirection.multiply(max(minLookahead, player.velocity.length() * correctionSmoothness)))
129+ return headingToward(lookaheadPoint)
130+ }
131+
132+ // What compass heading (Minecraft yaw) points from the player toward this spot on the map?
133+ private fun SafeContext.headingToward (point : Vec3d ): Double {
134+ val eastOffset = point.x - player.pos.x
135+ val southOffset = point.z - player.pos.z
136+ return Math .toDegrees(atan2(southOffset, eastOffset)) - YAW_DEGREES_OFFSET
137+ }
138+
139+ // Turn a compass heading (degrees) into a unit arrow pointing that way on the map.
140+ private fun directionForHeading (heading : Double ): Vec3d {
141+ val radians = Math .toRadians(heading)
142+ return Vec3d (- sin(radians), 0.0 , cos(radians)).normalize()
143+ }
144+
145+ private fun closestPointOnLine (point : Vec3d , lineOrigin : Vec3d , lineDirection : Vec3d ): Vec3d {
146+ val originToPoint = point.subtract(lineOrigin)
147+ val projection = originToPoint.dotProduct(lineDirection.normalize())
148+ return snapToAxis(lineOrigin.add(lineDirection.multiply(projection)), lineOrigin, lineDirection)
149+ }
150+
151+ private fun snapToAxis (point : Vec3d , lineOrigin : Vec3d , lineDirection : Vec3d ): Vec3d {
152+ var offset = point.subtract(lineOrigin)
153+
154+ if (abs(abs(lineDirection.x) - abs(lineDirection.z)) < TOLERANCE ) { // diagonal
155+ val magnitude = (abs(offset.x) + abs(offset.z)) / 2
156+ val x = if (lineDirection.x > 0 ) magnitude else - magnitude
157+ val z = if (lineDirection.z > 0 ) magnitude else - magnitude
158+ offset = Vec3d (x, 0.0 , z)
159+ } else { // straight
160+ val x = if (abs(lineDirection.x) < TOLERANCE ) 0.0 else offset.x
161+ val z = if (abs(lineDirection.z) < TOLERANCE ) 0.0 else offset.z
162+ if (x == 0.0 ) {
163+ offset = Vec3d (0.0 , 0.0 , z)
164+ } else if (z == 0.0 ) {
165+ offset = Vec3d (x, 0.0 , 0.0 )
166+ }
167+ }
168+ return lineOrigin.add(offset)
169+ }
170+ }
0 commit comments