Skip to content

Commit 9642b08

Browse files
committed
Fall damage calculation
1 parent 08cb828 commit 9642b08

File tree

10 files changed

+228
-65
lines changed

10 files changed

+228
-65
lines changed

common/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,13 @@ import com.lambda.sound.SoundManager.playSound
2828
import com.lambda.util.Communication
2929
import com.lambda.util.Communication.prefix
3030
import com.lambda.util.Formatting.string
31-
import com.lambda.util.combat.CombatUtils.explosionDamage
3231
import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal
32+
import com.lambda.util.combat.DamageUtils.isFallDeadly
3333
import com.lambda.util.player.SlotUtils.combined
3434
import com.lambda.util.text.*
3535
import com.lambda.util.world.fastEntitySearch
3636
import net.minecraft.entity.damage.DamageSource
3737
import net.minecraft.entity.damage.DamageTypes
38-
import net.minecraft.entity.decoration.EndCrystalEntity
3938
import net.minecraft.entity.mob.CreeperEntity
4039
import net.minecraft.entity.player.PlayerEntity
4140
import net.minecraft.item.Items
@@ -49,7 +48,9 @@ object AutoDisconnect : Module(
4948
defaultTags = setOf(ModuleTag.COMBAT)
5049
) {
5150
private val health by setting("Health", true, "Disconnect from the server when health is below the set limit.")
52-
private val minimumHealth by setting("Min Health", 10, 6..36, 1, description = "Set the minimum health threshold for disconnection.", unit = " hearts") { health }
51+
private val minimumHealth by setting("Min Health", 10, 6..36, 1, "Set the minimum health threshold for disconnection.", unit = " hearts") { health }
52+
private val falls by setting("Falls", false, "Disconnect if the player will die of fall damage")
53+
private val fallDistance by setting("Falls Time", 10, 0..30, 1, "Number of blocks fallen before disconnecting for fall damage.", unit = " blocks") { falls }
5354
private val crystals by setting("Crystals", false, "Disconnect if an End Crystal explosion would be lethal.")
5455
private val creeper by setting("Creepers", true, "Disconnect when an ignited Creeper is nearby.")
5556
private val totem by setting("Totem", false, "Disconnect if the number of Totems of Undying is below the required amount.")
@@ -233,11 +234,18 @@ object AutoDisconnect : Module(
233234
}
234235
}),
235236
END_CRYSTAL({ crystals }, {
236-
if (hasDeadlyCrystal(1.0))
237+
if (hasDeadlyCrystal())
237238
buildText {
238239
literal("There was an end crystal close to you that would've killed you")
239240
}
240241
else null
241-
});
242+
}),
243+
FALL_DAMAGE({ falls }, {
244+
if (isFallDeadly() && player.fallDistance > fallDistance)
245+
buildText {
246+
literal("You were about to fall and die")
247+
}
248+
else null
249+
})
242250
}
243251
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.debug
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.combat.DamageUtils.fallDamage
26+
import com.lambda.util.combat.DamageUtils.isFallDeadly
27+
28+
object FallTest : Module(
29+
name = "FallTest",
30+
defaultTags = setOf(ModuleTag.DEBUG),
31+
) {
32+
init {
33+
listen<TickEvent.Pre> {
34+
val damage = fallDamage()
35+
36+
info("Fall damage = $damage, Deadly = ${isFallDeadly()}")
37+
}
38+
}
39+
}

common/src/main/kotlin/com/lambda/module/modules/movement/SafeWalk.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ object SafeWalk : Module(
4646
}
4747
}
4848

49-
private fun LivingEntity.isNearLedge(distance: Double, stepHeight: Double): Boolean {
49+
fun LivingEntity.isNearLedge(distance: Double, stepHeight: Double): Boolean {
5050
fun checkDirection(deltaX: Double, deltaZ: Double): Boolean {
5151
var dx = deltaX + motionX
5252
var dz = deltaZ + motionZ

common/src/main/kotlin/com/lambda/util/BlockUtils.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ object BlockUtils {
225225

226226
val Vec3i.blockPos: BlockPos get() = BlockPos(this)
227227
val Block.item: Item get() = asItem()
228-
val Vec3d.flooredPos: BlockPos get() = BlockPos(x.floorToInt(), y.floorToInt(), z.floorToInt())
229228
fun BlockPos.vecOf(direction: Direction): Vec3d = toCenterPos().add(Vec3d.of(direction.vector).multiply(0.5))
230229
fun BlockPos.offset(eightWayDirection: EightWayDirection, amount: Int): BlockPos =
231230
add(eightWayDirection.offsetX * amount, 0, eightWayDirection.offsetZ * amount)

common/src/main/kotlin/com/lambda/util/combat/CombatUtils.kt

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,70 +18,24 @@
1818
package com.lambda.util.combat
1919

2020
import com.lambda.context.SafeContext
21+
import com.lambda.util.combat.DamageUtils.scale
22+
import com.lambda.util.extension.fullHealth
2123
import com.lambda.util.math.dist
2224
import com.lambda.util.world.fastEntitySearch
23-
import net.minecraft.client.world.ClientWorld
24-
import net.minecraft.entity.EquipmentSlot
2525
import net.minecraft.entity.LivingEntity
26-
import net.minecraft.entity.damage.DamageSource
2726
import net.minecraft.entity.decoration.EndCrystalEntity
28-
import net.minecraft.entity.effect.StatusEffects.FIRE_RESISTANCE
29-
import net.minecraft.registry.tag.DamageTypeTags.DAMAGES_HELMET
30-
import net.minecraft.registry.tag.DamageTypeTags.IS_FIRE
31-
import net.minecraft.registry.tag.DamageTypeTags.IS_FREEZING
32-
import net.minecraft.registry.tag.EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES
3327
import net.minecraft.util.math.Vec3d
34-
import net.minecraft.world.Difficulty
35-
import net.minecraft.world.World
3628
import net.minecraft.world.explosion.Explosion
37-
import kotlin.math.min
3829

3930
object CombatUtils {
40-
/**
41-
* Scales damage up or down based on the player resistances and other variables
42-
*
43-
* @param entity The entity to calculate the damage for
44-
* @param damage The damage to apply
45-
*/
46-
fun DamageSource.scale(world: ClientWorld, entity: LivingEntity, damage: Double): Double {
47-
if (damage.isNaN() || damage.isInfinite())
48-
return Double.MAX_VALUE
49-
50-
if (entity.isInvulnerableTo(this) ||
51-
entity.isDead ||
52-
entity.blockedByShield(this) ||
53-
isIn(IS_FIRE) && entity.hasStatusEffect(FIRE_RESISTANCE)) return 0.0
54-
55-
if (isIn(IS_FREEZING) && entity.type.isIn(FREEZE_HURTS_EXTRA_TYPES))
56-
return damage * 5
57-
58-
if (isIn(DAMAGES_HELMET) && !entity.getEquippedStack(EquipmentSlot.HEAD).isEmpty)
59-
return damage * 0.75
60-
61-
return world.scaleDamage(
62-
entity.applyArmorToDamage(this,
63-
entity.modifyAppliedDamage(this, damage.toFloat())).toDouble()
64-
)
65-
}
66-
67-
/**
68-
* Scales the damage depending on the world difficulty
69-
*/
70-
fun World.scaleDamage(damage: Double): Double =
71-
when (difficulty) {
72-
Difficulty.EASY -> min(damage / 2 + 1, damage)
73-
Difficulty.HARD -> damage * 3 / 2
74-
else -> damage
75-
}
76-
7731
/**
7832
* Returns whether there is a deadly end crystal in proximity of the player
7933
*
8034
* @param minHealth The minimum health (in half hearts) at which an explosion is considered deadly
8135
*/
82-
fun SafeContext.hasDeadlyCrystal(minHealth: Double) =
36+
fun SafeContext.hasDeadlyCrystal(minHealth: Double = 0.0) =
8337
fastEntitySearch<EndCrystalEntity>(12.0)
84-
.any { player.health - crystalDamage(it.pos, player) <= minHealth }
38+
.any { player.fullHealth - crystalDamage(it.pos, player) <= minHealth }
8539

8640
/**
8741
* Calculates the damage dealt by an explosion to a living entity
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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.util.combat
19+
20+
import com.lambda.context.SafeContext
21+
import com.lambda.util.BlockUtils.blockState
22+
import com.lambda.util.extension.fullHealth
23+
import com.lambda.util.math.flooredBlockPos
24+
import com.lambda.util.player.prediction.buildPlayerPrediction
25+
import net.minecraft.block.BedBlock
26+
import net.minecraft.block.CobwebBlock
27+
import net.minecraft.block.HayBlock
28+
import net.minecraft.block.HoneyBlock
29+
import net.minecraft.block.PointedDripstoneBlock
30+
import net.minecraft.block.PointedDripstoneBlock.THICKNESS
31+
import net.minecraft.block.PointedDripstoneBlock.VERTICAL_DIRECTION
32+
import net.minecraft.block.PowderSnowBlock
33+
import net.minecraft.block.SlimeBlock
34+
import net.minecraft.block.enums.Thickness
35+
import net.minecraft.client.world.ClientWorld
36+
import net.minecraft.entity.EquipmentSlot
37+
import net.minecraft.entity.LivingEntity
38+
import net.minecraft.entity.damage.DamageSource
39+
import net.minecraft.entity.effect.StatusEffects.FIRE_RESISTANCE
40+
import net.minecraft.entity.effect.StatusEffects.JUMP_BOOST
41+
import net.minecraft.entity.player.PlayerEntity
42+
import net.minecraft.registry.tag.DamageTypeTags.DAMAGES_HELMET
43+
import net.minecraft.registry.tag.DamageTypeTags.IS_FIRE
44+
import net.minecraft.registry.tag.DamageTypeTags.IS_FREEZING
45+
import net.minecraft.registry.tag.EntityTypeTags.FALL_DAMAGE_IMMUNE
46+
import net.minecraft.registry.tag.EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES
47+
import net.minecraft.util.math.Direction
48+
import net.minecraft.world.Difficulty
49+
import net.minecraft.world.World
50+
import kotlin.math.ceil
51+
import kotlin.math.max
52+
import kotlin.math.min
53+
54+
object DamageUtils {
55+
/**
56+
* Returns whether the player will fall from high enough for the impact to be deadly
57+
*
58+
* @param minHealth The minimum health (in half hearts) at which the fall is considered deadly
59+
*/
60+
fun SafeContext.isFallDeadly(minHealth: Double = 0.0) =
61+
player.fullHealth - fallDamage() <= minHealth
62+
63+
/**
64+
* Calculates the fall damage for the player at the predicted position
65+
*/
66+
fun SafeContext.fallDamage(): Double {
67+
val prediction = buildPlayerPrediction()
68+
.skipUntil(60) { it.onGround }
69+
70+
val predictedPos = prediction.position
71+
val fallDistance = player.y - predictedPos.y + player.fallDistance
72+
73+
val state = blockState(predictedPos.flooredBlockPos)
74+
val block = state.block
75+
76+
val distance = fallDistance +
77+
when (block) {
78+
is BedBlock -> 0.5
79+
is PointedDripstoneBlock -> if (state.get(VERTICAL_DIRECTION) == Direction.UP && state.get(THICKNESS) == Thickness.TIP) 2.0 else 0.0
80+
else -> 0.0
81+
}
82+
83+
val multiplier = when (block) {
84+
is HayBlock, is HoneyBlock -> 0.2
85+
is PointedDripstoneBlock -> if (state.get(VERTICAL_DIRECTION) == Direction.UP && state.get(THICKNESS) == Thickness.TIP) 2.0 else 1.0
86+
is PowderSnowBlock, is SlimeBlock, is CobwebBlock -> 0.0
87+
else -> 1.0
88+
}
89+
90+
val source = player.damageSources.fall()
91+
return source.scale(world, player, player.fallDamage(distance, multiplier))
92+
}
93+
94+
fun LivingEntity.fallDamage(distance: Double, multiplier: Double): Double {
95+
if (type.isIn(FALL_DAMAGE_IMMUNE)) return 0.0
96+
97+
val modifier = getStatusEffect(JUMP_BOOST)?.amplifier?.plus(1.0) ?: 0.0
98+
return max(0.0, ceil((distance - 3.0 - modifier) * multiplier))
99+
}
100+
/**
101+
* Scales damage up or down based on the player resistances and other variables
102+
*
103+
* @param entity The entity to calculate the damage for
104+
* @param damage The damage to apply
105+
*/
106+
fun DamageSource.scale(world: ClientWorld, entity: LivingEntity, damage: Double): Double {
107+
if (damage.isNaN() || damage.isInfinite())
108+
return Double.MAX_VALUE
109+
110+
if (entity.isInvulnerableTo(this) ||
111+
entity.isDead ||
112+
entity.blockedByShield(this) ||
113+
isIn(IS_FIRE) && entity.hasStatusEffect(FIRE_RESISTANCE)) return 0.0
114+
115+
if (isIn(IS_FREEZING) && entity.type.isIn(FREEZE_HURTS_EXTRA_TYPES))
116+
return damage * 5
117+
118+
if (isIn(DAMAGES_HELMET) && !entity.getEquippedStack(EquipmentSlot.HEAD).isEmpty)
119+
return damage * 0.75
120+
121+
val appliedDamage = entity.applyArmorToDamage(this,
122+
entity.modifyAppliedDamage(this, damage.toFloat())).toDouble()
123+
124+
return if (entity is PlayerEntity && isScaledWithDifficulty)
125+
world.scaleDamage(appliedDamage) else appliedDamage
126+
}
127+
128+
/**
129+
* Scales the damage depending on the world difficulty
130+
*
131+
* Note: before using this function make sure to check if the damage scales with the given source
132+
*/
133+
fun World.scaleDamage(damage: Double): Double =
134+
when (difficulty) {
135+
Difficulty.PEACEFUL -> 0.0
136+
Difficulty.EASY -> min(damage / 2 + 1, damage)
137+
Difficulty.HARD -> damage * 3 / 2
138+
else -> damage
139+
}
140+
}

common/src/main/kotlin/com/lambda/util/extension/Entity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ package com.lambda.util.extension
1919

2020
import com.lambda.interaction.request.rotation.Rotation
2121
import net.minecraft.entity.Entity
22+
import net.minecraft.entity.LivingEntity
2223
import net.minecraft.util.math.Vec3d
2324

2425
val Entity.prevPos
2526
get() = Vec3d(prevX, prevY, prevZ)
2627

2728
val Entity.rotation
2829
get() = Rotation(yaw, pitch)
30+
31+
val LivingEntity.fullHealth: Double
32+
get() = health + absorptionAmount.toDouble()

common/src/main/kotlin/com/lambda/util/player/prediction/PredictionEntity.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ package com.lambda.util.player.prediction
2020
import com.lambda.Lambda.mc
2121
import com.lambda.context.SafeContext
2222
import com.lambda.interaction.request.rotation.Rotation
23+
import com.lambda.module.modules.movement.SafeWalk.isNearLedge
2324
import com.lambda.threading.runSafe
2425
import com.lambda.util.BlockUtils.blockState
25-
import com.lambda.util.BlockUtils.flooredPos
2626
import com.lambda.util.math.DOWN
2727
import com.lambda.util.math.MathUtils.toIntSign
2828
import com.lambda.util.math.MathUtils.toRadian
29+
import com.lambda.util.math.flooredBlockPos
2930
import com.lambda.util.math.plus
3031
import com.lambda.util.math.times
3132
import com.lambda.util.player.MovementUtils.motion
@@ -83,7 +84,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
8384

8485
// Other shit
8586
private var jumpingCooldown = player.jumpingCooldown
86-
private var velocityAffectingPos = player.supportingBlockPos.orElse((position + DOWN * 0.001).flooredPos)
87+
private var velocityAffectingPos = player.supportingBlockPos.orElse((position + DOWN * 0.001).flooredBlockPos)
8788

8889
private var horizontalCollision = player.horizontalCollision
8990
private var verticalCollision = player.verticalCollision
@@ -180,6 +181,10 @@ class PredictionEntity(val player: ClientPlayerEntity) {
180181
var movement = motion
181182
movement = adjustMovementForCollisions(movement)
182183

184+
if (player.isNearLedge(0.01, 0.0) && isSneaking) {
185+
movement = movement.multiply(0.0, 1.0, 0.0)
186+
}
187+
183188
if (movement.lengthSquared() > 1.0E-7) {
184189
position += movement
185190
}
@@ -202,7 +207,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
202207
}
203208

204209
val velocityMultiplier = run {
205-
val f = blockState(position.flooredPos).block.velocityMultiplier.toDouble()
210+
val f = blockState(position.flooredBlockPos).block.velocityMultiplier.toDouble()
206211
val g = blockState(velocityAffectingPos).block.velocityMultiplier.toDouble()
207212
if (f == 1.0) g else f
208213
}
@@ -214,7 +219,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
214219
boundingBox = normalized.offset(position)
215220
}
216221

217-
velocityAffectingPos = (position + DOWN * 0.001).flooredPos
222+
velocityAffectingPos = (position + DOWN * 0.001).flooredBlockPos
218223
}
219224

220225
/** @see net.minecraft.entity.LivingEntity.jump */
@@ -226,7 +231,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
226231

227232
/** @see net.minecraft.entity.Entity.getJumpVelocityMultiplier */
228233
val jumpHeight = run {
229-
val f = blockState(position.flooredPos).block.jumpVelocityMultiplier.toDouble()
234+
val f = blockState(position.flooredBlockPos).block.jumpVelocityMultiplier.toDouble()
230235
val g = blockState(velocityAffectingPos).block.jumpVelocityMultiplier.toDouble()
231236
if (f == 1.0) g else f
232237
} * 0.42 + player.jumpBoostVelocityModifier

0 commit comments

Comments
 (0)