Skip to content

Commit 808e30d

Browse files
committed
Add ElytraAttitudeControl module
1 parent 1512770 commit 808e30d

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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.modules.movement.BetterFirework.startFirework
24+
import com.lambda.module.tag.ModuleTag
25+
import com.lambda.threading.runSafe
26+
import com.lambda.util.NamedEnum
27+
import com.lambda.util.SpeedUnit
28+
import net.minecraft.client.network.ClientPlayerEntity
29+
import net.minecraft.entity.EntityType
30+
import net.minecraft.util.math.Vec3d
31+
32+
object ElytraAttitudeControl : Module(
33+
name = "ElytraAttitudeControl",
34+
description = "Automatically control attitude or speed while elytra flying",
35+
tag = ModuleTag.MOVEMENT,
36+
) {
37+
val controlValue by setting("Control Value", Mode.Altitude)
38+
39+
val maxPitchAngle by setting("Max Pitch Angle", 45.0, 0.0..90.0, 1.0, unit = "°", description = "Maximum pitch angle")
40+
val disableOnFirework by setting("Disable On Firework", false, description = "Disables the module when a firework is used")
41+
42+
val targetAltitude by setting("Target Altitude", 120, 0..256, 10, unit = " blocks", description = "Adjusts pitch to control altitude") { controlValue == Mode.Altitude }
43+
val altitudeControllerP by setting("Altitude Control P", 1.2, 0.0..2.0, 0.05).group(Group.AltitudeControl)
44+
val altitudeControllerD by setting("Altitude Control D", 0.85, 0.0..1.0, 0.05).group(Group.AltitudeControl)
45+
val altitudeControllerI by setting("Altitude Control I", 0.04, 0.0..1.0, 0.05).group(Group.AltitudeControl)
46+
val altitudeControllerConst by setting("Altitude Control Const", 0.0, 0.0..10.0, 0.1).group(Group.AltitudeControl)
47+
48+
val targetSpeed by setting("Target Speed", 28.0, 0.1..50.0, 0.1, unit = " m/s", description = "Adjusts pitch to control speed") { controlValue == Mode.Speed }
49+
val horizontalSpeed by setting("Horizontal Speed", false, description = "Uses horizontal speed instead of total speed for speed control") { controlValue == Mode.Speed }
50+
val speedControllerP by setting("Speed Control P", 6.75, 0.0..10.0, 0.05).group(Group.SpeedControl)
51+
val speedControllerD by setting("Speed Control D", 4.5, 0.0..5.0, 0.05).group(Group.SpeedControl)
52+
val speedControllerI by setting("Speed Control I", 0.3, 0.0..1.0, 0.05).group(Group.SpeedControl)
53+
54+
val useFirework by setting("Use Firework", false, "Automatically use fireworks to maintain speed or height")
55+
val minHeight by setting("Min Height", 50, 0..256, 10, unit = " blocks", description = "Minimum height to use fireworks") { useFirework }
56+
val minSpeed by setting("Min Speed", 15.0, 0.1..50.0, 0.1, unit = " m/s", description = "Minimum speed to use fireworks") { useFirework }
57+
58+
var lastPos : Vec3d = Vec3d.ZERO
59+
val speedController: PIController = PIController({ speedControllerP }, { speedControllerD }, { speedControllerI }, { 0.0 })
60+
val altitudeController: PIController = PIController({ altitudeControllerP }, { altitudeControllerD }, { altitudeControllerI }, { altitudeControllerConst })
61+
62+
var lastUsedFirework = 0L
63+
64+
init {
65+
listen<TickEvent.Pre> {
66+
if (!player.isGliding) return@listen
67+
if (player.hasFirework && disableOnFirework) return@listen
68+
69+
val outputPitch = when (controlValue) {
70+
Mode.Speed -> {
71+
var speed = player.pos.subtract(lastPos)
72+
if (horizontalSpeed) {
73+
speed = Vec3d(speed.x, 0.0, speed.z)
74+
}
75+
76+
speedController.getOutput(targetSpeed, SpeedUnit.MetersPerSecond.convertFromMinecraft(speed.length()))
77+
}
78+
Mode.Altitude -> {
79+
val currentAltitude = player.y
80+
-1 * altitudeController.getOutput(targetAltitude.toDouble(), currentAltitude) // Negative because in minecraft pitch > 0 is looking down not up
81+
}
82+
}
83+
val newPitch = outputPitch.coerceIn(-maxPitchAngle, maxPitchAngle)
84+
// lookAt(Rotation(player.yaw, newPitch.toFloat())).requestBy(this@ElytraAutopilot) // TODO: Use this when rotation system accepts pitch changes
85+
player.pitch = newPitch.toFloat()
86+
87+
lastPos = player.pos
88+
89+
if (useFirework) {
90+
val currentTime = System.currentTimeMillis()
91+
if (lastUsedFirework + 2000 > currentTime) {
92+
return@listen
93+
}
94+
95+
if (player.hasFirework) {
96+
return@listen
97+
}
98+
99+
if (minHeight >= player.y || SpeedUnit.MetersPerSecond.convertFromMinecraft(player.velocity.length()) < minSpeed) {
100+
lastUsedFirework = currentTime
101+
runSafe {
102+
startFirework(true)
103+
}
104+
}
105+
}
106+
}
107+
108+
onEnable {
109+
speedController.reset()
110+
altitudeController.reset()
111+
lastPos = player.pos
112+
}
113+
}
114+
115+
val ClientPlayerEntity.hasFirework: Boolean
116+
get() = clientWorld.getEntitiesByType(
117+
EntityType.FIREWORK_ROCKET,
118+
boundingBox.expand(4.0),
119+
{ it.distanceTo(this) < 4.0 }
120+
).isNotEmpty()
121+
122+
class PIController(val valueP: () -> Double, val valueD: () -> Double, val valueI: () -> Double, val constant: () -> Double) {
123+
var accumulator = 0.0 // Integral term accumulator
124+
var lastDiff = 0.0
125+
fun getOutput(target: Double, current: Double): Double {
126+
val diff = target - current
127+
val diffDt = diff - lastDiff
128+
accumulator += diff
129+
130+
accumulator = accumulator.coerceIn(-100.0, 100.0) // Prevent integral windup
131+
lastDiff = diff
132+
133+
return diffDt * valueD() + diff * valueP() + accumulator * valueI() + constant()
134+
}
135+
136+
fun reset() {
137+
accumulator = 0.0
138+
}
139+
}
140+
141+
enum class Mode {
142+
Speed,
143+
Altitude;
144+
}
145+
enum class Group(override val displayName: String) : NamedEnum {
146+
SpeedControl("Speed Control"),
147+
AltitudeControl("Altitude Control");
148+
}
149+
}

0 commit comments

Comments
 (0)