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