Skip to content

Commit b146577

Browse files
authored
Feature: Elytra altitude control (173)
1 parent e1b76a4 commit b146577

File tree

3 files changed

+350
-0
lines changed

3 files changed

+350
-0
lines changed

src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ object BetterFirework : Module(
205205
* Use a firework from the hotbar or inventory if possible.
206206
* Return true if a firework has been used
207207
*/
208+
@JvmStatic
208209
fun SafeContext.startFirework(silent: Boolean) {
209210
val stack = selectStack(count = 1) { isItem(Items.FIREWORK_ROCKET) }
210211

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/*
2+
* Copyright 2025 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.movement
19+
20+
import com.lambda.config.groups.RotationSettings
21+
import com.lambda.event.events.TickEvent
22+
import com.lambda.event.listener.SafeListener.Companion.listen
23+
import com.lambda.interaction.managers.rotating.Rotation
24+
import com.lambda.interaction.managers.rotating.visibilty.lookAt
25+
import com.lambda.module.Module
26+
import com.lambda.module.modules.movement.BetterFirework.startFirework
27+
import com.lambda.module.tag.ModuleTag
28+
import com.lambda.threading.runSafe
29+
import com.lambda.util.Communication.info
30+
import com.lambda.util.NamedEnum
31+
import com.lambda.util.SpeedUnit
32+
import com.lambda.util.Timer
33+
import com.lambda.util.world.fastEntitySearch
34+
import net.minecraft.client.network.ClientPlayerEntity
35+
import net.minecraft.entity.projectile.FireworkRocketEntity
36+
import net.minecraft.text.Text.literal
37+
import net.minecraft.util.math.Vec3d
38+
import kotlin.time.Duration.Companion.seconds
39+
import kotlin.time.TimeSource
40+
41+
object ElytraAttitudeControl : Module(
42+
name = "ElytraAttitudeControl",
43+
description = "Automatically control attitude or speed while elytra flying",
44+
tag = ModuleTag.MOVEMENT,
45+
) {
46+
val controlValue by setting("Control Value", Mode.Altitude)
47+
48+
val maxPitchAngle by setting("Max Pitch Angle", 45.0, 0.0..90.0, 1.0, unit = "°", description = "Maximum pitch angle")
49+
val disableOnFirework by setting("Disable On Firework", false, description = "Disables the module when a firework is used")
50+
51+
val targetAltitude by setting("Target Altitude", 120, 0..256, 10, unit = " blocks", description = "Adjusts pitch to control altitude") { controlValue == Mode.Altitude }
52+
val altitudeControllerP by setting("Altitude Control P", 1.2, 0.0..2.0, 0.05).group(Group.AltitudeControl)
53+
val altitudeControllerD by setting("Altitude Control D", 0.85, 0.0..1.0, 0.05).group(Group.AltitudeControl)
54+
val altitudeControllerI by setting("Altitude Control I", 0.04, 0.0..1.0, 0.05).group(Group.AltitudeControl)
55+
val altitudeControllerConst by setting("Altitude Control Const", 0.0, 0.0..10.0, 0.1).group(Group.AltitudeControl)
56+
57+
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 }
58+
val horizontalSpeed by setting("Horizontal Speed", false, description = "Uses horizontal speed instead of total speed for speed control") { controlValue == Mode.Speed }
59+
val speedControllerP by setting("Speed Control P", 6.75, 0.0..10.0, 0.05).group(Group.SpeedControl)
60+
val speedControllerD by setting("Speed Control D", 4.5, 0.0..5.0, 0.05).group(Group.SpeedControl)
61+
val speedControllerI by setting("Speed Control I", 0.3, 0.0..1.0, 0.05).group(Group.SpeedControl)
62+
63+
val useFireworkOnHeight by setting("Use Firework On Height", false, "Use fireworks when below a certain height")
64+
val minHeight by setting("Min Height", 50, 0..256, 10, unit = " blocks", description = "Minimum height to use firework") { useFireworkOnHeight }
65+
66+
val useFireworkOnSpeed by setting("Use Firework On Speed", false, "Use fireworks based on speed")
67+
val minSpeed by setting("Min Speed", 20.0, 0.1..50.0, 0.1, unit = " m/s", description = "Minimum speed to use fireworks") { useFireworkOnSpeed }
68+
69+
var lastPos: Vec3d = Vec3d.ZERO
70+
val speedController: PIController = PIController({ speedControllerP }, { speedControllerD }, { speedControllerI }, { 0.0 })
71+
val altitudeController: PIController = PIController({ altitudeControllerP }, { altitudeControllerD }, { altitudeControllerI }, { altitudeControllerConst })
72+
73+
val usePitch40OnHeight by setting("Use Pitch 40 On Height", false, "Use Pitch 40 to gain height and speed")
74+
val logHeightGain by setting("Log Height Gain", false, "Logs the height gained each cycle to the chat") { usePitch40OnHeight }.group(Group.Pitch40Control)
75+
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)
76+
val pitch40ExitHeight by setting("Exit height", 190, 0..256, 10, unit = " blocks", description = "Height to exit Pitch 40 mode") { usePitch40OnHeight }.group(Group.Pitch40Control)
77+
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)
78+
val pitch40DownAngle by setting("Down Angle", 33f, 0f..90f, .5f, description = "Angle to dive down at to gain speed") { usePitch40OnHeight }.group(Group.Pitch40Control)
79+
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)
80+
val pitch40SpeedThreshold by setting("Speed Threshold", 41f, 10f..100f, .5f, description = "Speed at which to start pitching up") { usePitch40OnHeight }.group(Group.Pitch40Control)
81+
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)
82+
83+
override val rotationConfig = RotationSettings(this, Group.Rotation)
84+
85+
var controlState = ControlState.AttitudeControl
86+
var state = Pitch40State.GainSpeed
87+
var lastAngle = pitch40UpStartAngle
88+
var lastCycleFinish = TimeSource.Monotonic.markNow()
89+
var lastY = 0.0
90+
91+
val usageDelay = Timer()
92+
93+
init {
94+
listen<TickEvent.Pre> {
95+
if (!player.isGliding) return@listen
96+
run {
97+
when (controlState) {
98+
ControlState.AttitudeControl -> {
99+
if (disableOnFirework && player.hasFirework) {
100+
return@run
101+
}
102+
if (usePitch40OnHeight) {
103+
if (player.y < minHeightForPitch40) {
104+
controlState = ControlState.Pitch40Fly
105+
lastY = player.pos.y
106+
return@run
107+
}
108+
}
109+
val outputPitch = when (controlValue) {
110+
Mode.Speed -> {
111+
speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
112+
}
113+
Mode.Altitude -> {
114+
-1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
115+
}
116+
}.coerceIn(-maxPitchAngle, maxPitchAngle)
117+
lookAt(Rotation(player.yaw, outputPitch.toFloat())).requestBy(this@ElytraAttitudeControl)
118+
119+
if (usageDelay.timePassed(2.seconds) && !player.hasFirework) {
120+
if (useFireworkOnHeight && minHeight > player.y) {
121+
usageDelay.reset()
122+
runSafe {
123+
startFirework(true)
124+
}
125+
}
126+
if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
127+
usageDelay.reset()
128+
runSafe {
129+
startFirework(true)
130+
}
131+
}
132+
}
133+
}
134+
ControlState.Pitch40Fly -> when (state) {
135+
Pitch40State.GainSpeed -> {
136+
player.pitch = pitch40DownAngle
137+
if (player.flySpeed() > pitch40SpeedThreshold) {
138+
state = Pitch40State.PitchUp
139+
}
140+
}
141+
Pitch40State.PitchUp -> {
142+
lastAngle -= 5f
143+
player.pitch = lastAngle
144+
if (lastAngle <= pitch40UpStartAngle) {
145+
state = Pitch40State.FlyUp
146+
if (pitch40UseFireworkOnUpTrajectory) {
147+
runSafe {
148+
startFirework(true)
149+
}
150+
}
151+
}
152+
}
153+
Pitch40State.FlyUp -> {
154+
lastAngle += pitch40AngleChangeRate
155+
player.pitch = lastAngle
156+
if (lastAngle >= 0f) {
157+
state = Pitch40State.GainSpeed
158+
if (logHeightGain) {
159+
var timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
160+
var heightDelta = player.pos.y - lastY
161+
var heightPerMinute = (heightDelta) / (timeDelta / 1000.0) * 60.0
162+
info(literal("Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)".format(heightDelta, timeDelta / 1000.0, heightPerMinute)))
163+
}
164+
165+
lastCycleFinish = TimeSource.Monotonic.markNow()
166+
lastY = player.pos.y
167+
if (pitch40ExitHeight < player.y) {
168+
controlState = ControlState.AttitudeControl
169+
speedController.reset()
170+
altitudeController.reset()
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
lastPos = player.pos
178+
}
179+
180+
onEnable {
181+
speedController.reset()
182+
altitudeController.reset()
183+
lastPos = player.pos
184+
state = Pitch40State.GainSpeed
185+
controlState = ControlState.AttitudeControl
186+
lastAngle = pitch40UpStartAngle
187+
}
188+
}
189+
190+
val ClientPlayerEntity.hasFirework: Boolean
191+
get() = runSafe { return fastEntitySearch<FireworkRocketEntity>(4.0) { it.shooter == this.player }.any() } ?: false
192+
193+
class PIController(val valueP: () -> Double, val valueD: () -> Double, val valueI: () -> Double, val constant: () -> Double) {
194+
var accumulator = 0.0 // Integral term accumulator
195+
var lastDiff = 0.0
196+
fun getOutput(target: Double, current: Double): Double {
197+
val diff = target - current
198+
val diffDt = diff - lastDiff
199+
accumulator += diff
200+
201+
accumulator = accumulator.coerceIn(-100.0, 100.0) // Prevent integral windup
202+
lastDiff = diff
203+
204+
return diffDt * valueD() + diff * valueP() + accumulator * valueI() + constant()
205+
}
206+
207+
fun reset() {
208+
accumulator = 0.0
209+
}
210+
}
211+
212+
/**
213+
* Get the player's current speed in meters per second.
214+
*/
215+
fun ClientPlayerEntity.flySpeed(onlyHorizontal: Boolean = false): Float {
216+
var delta = this.pos.subtract(lastPos)
217+
if (onlyHorizontal) {
218+
delta = Vec3d(delta.x, 0.0, delta.z)
219+
}
220+
return SpeedUnit.MetersPerSecond.convertFromMinecraft(delta.length()).toFloat()
221+
}
222+
223+
enum class Mode {
224+
Speed,
225+
Altitude;
226+
}
227+
228+
enum class ControlState {
229+
AttitudeControl,
230+
Pitch40Fly
231+
}
232+
233+
enum class Group(override val displayName: String) : NamedEnum {
234+
SpeedControl("Speed Control"),
235+
AltitudeControl("Altitude Control"),
236+
Pitch40Control("Pitch 40 Control"),
237+
Rotation("Rotation")
238+
}
239+
240+
enum class Pitch40State {
241+
GainSpeed,
242+
PitchUp,
243+
FlyUp,
244+
}
245+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2025 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.movement
19+
20+
import com.lambda.event.events.TickEvent
21+
import com.lambda.event.listener.SafeListener.Companion.listen
22+
import com.lambda.module.Module
23+
import com.lambda.module.tag.ModuleTag
24+
import com.lambda.util.Communication.info
25+
import com.lambda.util.SpeedUnit
26+
import net.minecraft.client.network.ClientPlayerEntity
27+
import net.minecraft.text.Text.literal
28+
import net.minecraft.util.math.Vec3d
29+
30+
31+
/*
32+
* @author IceTank
33+
* @since 07.12.2025
34+
*/
35+
object Pitch40 : Module(
36+
name = "Pitch40",
37+
description = "Allows you to fly forever",
38+
tag = ModuleTag.MOVEMENT
39+
) {
40+
val logHeightGain by setting("Log Height Gain", true, "Logs the height gained each cycle to the chat")
41+
42+
val PitchUpDefault = -49f // Start angle when going back up. negative pitch = looking up
43+
val PitchDownDefault = 33f // Best angle for getting speed
44+
val PitchAngleChangeSpeed = 0.5f
45+
val PitchUpSpeedThreshold = 45f
46+
47+
var state = Pitch40State.GainSpeed
48+
var lastAngle = PitchUpDefault
49+
50+
var lastPos = Vec3d.ZERO
51+
var lastY = 0.0
52+
53+
init {
54+
listen<TickEvent.Pre> {
55+
when (state) {
56+
Pitch40State.GainSpeed -> {
57+
player.pitch = PitchDownDefault
58+
if (player.flySpeed() > PitchUpSpeedThreshold) {
59+
state = Pitch40State.PitchUp
60+
}
61+
}
62+
Pitch40State.PitchUp -> {
63+
lastAngle -= 5f
64+
player.pitch = lastAngle
65+
if (lastAngle <= PitchUpDefault) {
66+
state = Pitch40State.FlyUp
67+
}
68+
}
69+
Pitch40State.FlyUp -> {
70+
lastAngle += PitchAngleChangeSpeed
71+
player.pitch = lastAngle
72+
if (lastAngle >= 0f) {
73+
state = Pitch40State.GainSpeed
74+
if (logHeightGain)
75+
info(literal("Height gained this cycle: %.2f meters".format(player.pos.y - lastY)))
76+
77+
lastY = player.pos.y
78+
}
79+
}
80+
}
81+
lastPos = player.pos
82+
}
83+
84+
onEnable {
85+
state = Pitch40State.GainSpeed
86+
lastPos = player.pos
87+
lastAngle = PitchUpDefault
88+
}
89+
}
90+
91+
/**
92+
* Get the player's current speed in meters per second.
93+
*/
94+
fun ClientPlayerEntity.flySpeed(): Float {
95+
val delta = this.pos.subtract(lastPos)
96+
return SpeedUnit.MetersPerSecond.convertFromMinecraft(delta.length()).toFloat()
97+
}
98+
99+
enum class Pitch40State {
100+
GainSpeed,
101+
PitchUp,
102+
FlyUp,
103+
}
104+
}

0 commit comments

Comments
 (0)