diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
index 7a56d7d78..9983cab53 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
@@ -205,6 +205,7 @@ object BetterFirework : Module(
* Use a firework from the hotbar or inventory if possible.
* Return true if a firework has been used
*/
+ @JvmStatic
fun SafeContext.startFirework(silent: Boolean) {
val stack = selectStack(count = 1) { isItem(Items.FIREWORK_ROCKET) }
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/ElytraAttitudeControl.kt b/src/main/kotlin/com/lambda/module/modules/movement/ElytraAttitudeControl.kt
new file mode 100644
index 000000000..9bda31bbe
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/movement/ElytraAttitudeControl.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2025 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.movement
+
+import com.lambda.config.groups.RotationSettings
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.managers.rotating.visibilty.lookAt
+import com.lambda.module.Module
+import com.lambda.module.modules.movement.BetterFirework.startFirework
+import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafe
+import com.lambda.util.Communication.info
+import com.lambda.util.NamedEnum
+import com.lambda.util.SpeedUnit
+import com.lambda.util.Timer
+import com.lambda.util.world.fastEntitySearch
+import net.minecraft.client.network.ClientPlayerEntity
+import net.minecraft.entity.projectile.FireworkRocketEntity
+import net.minecraft.text.Text.literal
+import net.minecraft.util.math.Vec3d
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.TimeSource
+
+object ElytraAttitudeControl : Module(
+ name = "ElytraAttitudeControl",
+ description = "Automatically control attitude or speed while elytra flying",
+ tag = ModuleTag.MOVEMENT,
+) {
+ val controlValue by setting("Control Value", Mode.Altitude)
+
+ val maxPitchAngle by setting("Max Pitch Angle", 45.0, 0.0..90.0, 1.0, unit = "°", description = "Maximum pitch angle")
+ val disableOnFirework by setting("Disable On Firework", false, description = "Disables the module when a firework is used")
+
+ val targetAltitude by setting("Target Altitude", 120, 0..256, 10, unit = " blocks", description = "Adjusts pitch to control altitude") { controlValue == Mode.Altitude }
+ val altitudeControllerP by setting("Altitude Control P", 1.2, 0.0..2.0, 0.05).group(Group.AltitudeControl)
+ val altitudeControllerD by setting("Altitude Control D", 0.85, 0.0..1.0, 0.05).group(Group.AltitudeControl)
+ val altitudeControllerI by setting("Altitude Control I", 0.04, 0.0..1.0, 0.05).group(Group.AltitudeControl)
+ val altitudeControllerConst by setting("Altitude Control Const", 0.0, 0.0..10.0, 0.1).group(Group.AltitudeControl)
+
+ val targetSpeed by setting("Target Speed", 20.0, 0.1..50.0, 0.1, unit = " m/s", description = "Adjusts pitch to control speed") { controlValue == Mode.Speed }
+ val horizontalSpeed by setting("Horizontal Speed", false, description = "Uses horizontal speed instead of total speed for speed control") { controlValue == Mode.Speed }
+ val speedControllerP by setting("Speed Control P", 6.75, 0.0..10.0, 0.05).group(Group.SpeedControl)
+ val speedControllerD by setting("Speed Control D", 4.5, 0.0..5.0, 0.05).group(Group.SpeedControl)
+ val speedControllerI by setting("Speed Control I", 0.3, 0.0..1.0, 0.05).group(Group.SpeedControl)
+
+ val useFireworkOnHeight by setting("Use Firework On Height", false, "Use fireworks when below a certain height")
+ val minHeight by setting("Min Height", 50, 0..256, 10, unit = " blocks", description = "Minimum height to use firework") { useFireworkOnHeight }
+
+ val useFireworkOnSpeed by setting("Use Firework On Speed", false, "Use fireworks based on speed")
+ val minSpeed by setting("Min Speed", 20.0, 0.1..50.0, 0.1, unit = " m/s", description = "Minimum speed to use fireworks") { useFireworkOnSpeed }
+
+ var lastPos: Vec3d = Vec3d.ZERO
+ val speedController: PIController = PIController({ speedControllerP }, { speedControllerD }, { speedControllerI }, { 0.0 })
+ val altitudeController: PIController = PIController({ altitudeControllerP }, { altitudeControllerD }, { altitudeControllerI }, { altitudeControllerConst })
+
+ val usePitch40OnHeight by setting("Use Pitch 40 On Height", false, "Use Pitch 40 to gain height and speed")
+ val logHeightGain by setting("Log Height Gain", false, "Logs the height gained each cycle to the chat") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val minHeightForPitch40 by setting("Min Height For Pitch 40", 120, 0..256, 10, unit = " blocks", description = "Minimum height to use Pitch 40") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40ExitHeight by setting("Exit height", 190, 0..256, 10, unit = " blocks", description = "Height to exit Pitch 40 mode") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40UpStartAngle by setting("Up Start Angle", -49f, -90f..0f, .5f, description = "Start angle when going back up. negative pitch = looking up") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40DownAngle by setting("Down Angle", 33f, 0f..90f, .5f, description = "Angle to dive down at to gain speed") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40AngleChangeRate by setting("Angle Change Rate", 0.5f, 0.1f..5f, 0.01f, description = "Rate at which to increase pitch while in the fly up curve") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40SpeedThreshold by setting("Speed Threshold", 41f, 10f..100f, .5f, description = "Speed at which to start pitching up") { usePitch40OnHeight }.group(Group.Pitch40Control)
+ val pitch40UseFireworkOnUpTrajectory by setting("Use Firework On Up Trajectory", false, "Use fireworks when converting speed to altitude in the Pitch 40 maneuver") { usePitch40OnHeight }.group(Group.Pitch40Control)
+
+ override val rotationConfig = RotationSettings(this, Group.Rotation)
+
+ var controlState = ControlState.AttitudeControl
+ var state = Pitch40State.GainSpeed
+ var lastAngle = pitch40UpStartAngle
+ var lastCycleFinish = TimeSource.Monotonic.markNow()
+ var lastY = 0.0
+
+ val usageDelay = Timer()
+
+ init {
+ listen {
+ if (!player.isGliding) return@listen
+ run {
+ when (controlState) {
+ ControlState.AttitudeControl -> {
+ if (disableOnFirework && player.hasFirework) {
+ return@run
+ }
+ if (usePitch40OnHeight) {
+ if (player.y < minHeightForPitch40) {
+ controlState = ControlState.Pitch40Fly
+ lastY = player.pos.y
+ return@run
+ }
+ }
+ val outputPitch = when (controlValue) {
+ Mode.Speed -> {
+ speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
+ }
+ Mode.Altitude -> {
+ -1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
+ }
+ }.coerceIn(-maxPitchAngle, maxPitchAngle)
+ lookAt(Rotation(player.yaw, outputPitch.toFloat())).requestBy(this@ElytraAttitudeControl)
+
+ if (usageDelay.timePassed(2.seconds) && !player.hasFirework) {
+ if (useFireworkOnHeight && minHeight > player.y) {
+ usageDelay.reset()
+ runSafe {
+ startFirework(true)
+ }
+ }
+ if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
+ usageDelay.reset()
+ runSafe {
+ startFirework(true)
+ }
+ }
+ }
+ }
+ ControlState.Pitch40Fly -> when (state) {
+ Pitch40State.GainSpeed -> {
+ player.pitch = pitch40DownAngle
+ if (player.flySpeed() > pitch40SpeedThreshold) {
+ state = Pitch40State.PitchUp
+ }
+ }
+ Pitch40State.PitchUp -> {
+ lastAngle -= 5f
+ player.pitch = lastAngle
+ if (lastAngle <= pitch40UpStartAngle) {
+ state = Pitch40State.FlyUp
+ if (pitch40UseFireworkOnUpTrajectory) {
+ runSafe {
+ startFirework(true)
+ }
+ }
+ }
+ }
+ Pitch40State.FlyUp -> {
+ lastAngle += pitch40AngleChangeRate
+ player.pitch = lastAngle
+ if (lastAngle >= 0f) {
+ state = Pitch40State.GainSpeed
+ if (logHeightGain) {
+ var timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
+ var heightDelta = player.pos.y - lastY
+ var heightPerMinute = (heightDelta) / (timeDelta / 1000.0) * 60.0
+ info(literal("Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)".format(heightDelta, timeDelta / 1000.0, heightPerMinute)))
+ }
+
+ lastCycleFinish = TimeSource.Monotonic.markNow()
+ lastY = player.pos.y
+ if (pitch40ExitHeight < player.y) {
+ controlState = ControlState.AttitudeControl
+ speedController.reset()
+ altitudeController.reset()
+ }
+ }
+ }
+ }
+ }
+ }
+ lastPos = player.pos
+ }
+
+ onEnable {
+ speedController.reset()
+ altitudeController.reset()
+ lastPos = player.pos
+ state = Pitch40State.GainSpeed
+ controlState = ControlState.AttitudeControl
+ lastAngle = pitch40UpStartAngle
+ }
+ }
+
+ val ClientPlayerEntity.hasFirework: Boolean
+ get() = runSafe { return fastEntitySearch(4.0) { it.shooter == this.player }.any() } ?: false
+
+ class PIController(val valueP: () -> Double, val valueD: () -> Double, val valueI: () -> Double, val constant: () -> Double) {
+ var accumulator = 0.0 // Integral term accumulator
+ var lastDiff = 0.0
+ fun getOutput(target: Double, current: Double): Double {
+ val diff = target - current
+ val diffDt = diff - lastDiff
+ accumulator += diff
+
+ accumulator = accumulator.coerceIn(-100.0, 100.0) // Prevent integral windup
+ lastDiff = diff
+
+ return diffDt * valueD() + diff * valueP() + accumulator * valueI() + constant()
+ }
+
+ fun reset() {
+ accumulator = 0.0
+ }
+ }
+
+ /**
+ * Get the player's current speed in meters per second.
+ */
+ fun ClientPlayerEntity.flySpeed(onlyHorizontal: Boolean = false): Float {
+ var delta = this.pos.subtract(lastPos)
+ if (onlyHorizontal) {
+ delta = Vec3d(delta.x, 0.0, delta.z)
+ }
+ return SpeedUnit.MetersPerSecond.convertFromMinecraft(delta.length()).toFloat()
+ }
+
+ enum class Mode {
+ Speed,
+ Altitude;
+ }
+
+ enum class ControlState {
+ AttitudeControl,
+ Pitch40Fly
+ }
+
+ enum class Group(override val displayName: String) : NamedEnum {
+ SpeedControl("Speed Control"),
+ AltitudeControl("Altitude Control"),
+ Pitch40Control("Pitch 40 Control"),
+ Rotation("Rotation")
+ }
+
+ enum class Pitch40State {
+ GainSpeed,
+ PitchUp,
+ FlyUp,
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Pitch40.kt b/src/main/kotlin/com/lambda/module/modules/movement/Pitch40.kt
new file mode 100644
index 000000000..fe84454b1
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Pitch40.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 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.movement
+
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.Communication.info
+import com.lambda.util.SpeedUnit
+import net.minecraft.client.network.ClientPlayerEntity
+import net.minecraft.text.Text.literal
+import net.minecraft.util.math.Vec3d
+
+
+/*
+ * @author IceTank
+ * @since 07.12.2025
+*/
+object Pitch40 : Module(
+ name = "Pitch40",
+ description = "Allows you to fly forever",
+ tag = ModuleTag.MOVEMENT
+) {
+ val logHeightGain by setting("Log Height Gain", true, "Logs the height gained each cycle to the chat")
+
+ val PitchUpDefault = -49f // Start angle when going back up. negative pitch = looking up
+ val PitchDownDefault = 33f // Best angle for getting speed
+ val PitchAngleChangeSpeed = 0.5f
+ val PitchUpSpeedThreshold = 45f
+
+ var state = Pitch40State.GainSpeed
+ var lastAngle = PitchUpDefault
+
+ var lastPos = Vec3d.ZERO
+ var lastY = 0.0
+
+ init {
+ listen {
+ when (state) {
+ Pitch40State.GainSpeed -> {
+ player.pitch = PitchDownDefault
+ if (player.flySpeed() > PitchUpSpeedThreshold) {
+ state = Pitch40State.PitchUp
+ }
+ }
+ Pitch40State.PitchUp -> {
+ lastAngle -= 5f
+ player.pitch = lastAngle
+ if (lastAngle <= PitchUpDefault) {
+ state = Pitch40State.FlyUp
+ }
+ }
+ Pitch40State.FlyUp -> {
+ lastAngle += PitchAngleChangeSpeed
+ player.pitch = lastAngle
+ if (lastAngle >= 0f) {
+ state = Pitch40State.GainSpeed
+ if (logHeightGain)
+ info(literal("Height gained this cycle: %.2f meters".format(player.pos.y - lastY)))
+
+ lastY = player.pos.y
+ }
+ }
+ }
+ lastPos = player.pos
+ }
+
+ onEnable {
+ state = Pitch40State.GainSpeed
+ lastPos = player.pos
+ lastAngle = PitchUpDefault
+ }
+ }
+
+ /**
+ * Get the player's current speed in meters per second.
+ */
+ fun ClientPlayerEntity.flySpeed(): Float {
+ val delta = this.pos.subtract(lastPos)
+ return SpeedUnit.MetersPerSecond.convertFromMinecraft(delta.length()).toFloat()
+ }
+
+ enum class Pitch40State {
+ GainSpeed,
+ PitchUp,
+ FlyUp,
+ }
+}
\ No newline at end of file