Skip to content

Commit 705bf08

Browse files
Feature: LineLock Module (#317)
1 parent 4f8dd5a commit 705bf08

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

  • src/main/kotlin/com/lambda/module/modules/player
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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

Comments
 (0)