diff --git a/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/src/main/kotlin/com/lambda/config/groups/Targeting.kt index a50753e8b..f53d76573 100644 --- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt +++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt @@ -29,11 +29,15 @@ import com.lambda.util.NamedEnum import com.lambda.util.extension.fullHealth import com.lambda.util.math.distSq import com.lambda.util.world.fastEntitySearch +import com.lambda.world.LambdaAngerManagement import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.client.network.OtherClientPlayerEntity import net.minecraft.entity.LivingEntity import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.entity.mob.AmbientEntity +import net.minecraft.entity.mob.Angerable import net.minecraft.entity.mob.HostileEntity +import net.minecraft.entity.passive.AnimalEntity import net.minecraft.entity.passive.PassiveEntity import java.util.* @@ -50,166 +54,194 @@ import java.util.* * @param maxRange The maximum range within which entities can be targeted. */ abstract class Targeting( - private val c: Configurable, - baseGroup: NamedEnum, - private val defaultRange: Double, - private val maxRange: Double, + private val c: Configurable, + baseGroup: NamedEnum, + private val defaultRange: Double, + private val maxRange: Double, ) : SettingGroup(c), TargetingConfig { - /** - * The range within which entities can be targeted. This value is configurable and constrained - * between 1.0 and [maxRange]. - */ - override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup) - - /** - * Whether players are included in the targeting scope. - */ - override val players by c.setting("Players", true).group(baseGroup) - - /** - * Whether friends are included in the targeting scope. - * Requires [players] to be true. - */ - override val friends by c.setting("Friends", false) { players }.group(baseGroup) - - /** - * Whether mobs are included in the targeting scope. - */ - private val mobs by c.setting("Mobs", true).group(baseGroup) - - /** - * Whether hostile mobs are included in the targeting scope - */ - private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup) - - /** - * Whether passive animals are included in the targeting scope - */ - private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup) - - /** - * Indicates whether hostile entities are included in the targeting scope. - */ - override val hostiles get() = mobs && hostilesSetting - - /** - * Indicates whether passive animals are included in the targeting scope. - */ - override val animals get() = mobs && animalsSetting - - /** - * Whether invisible entities are included in the targeting scope. - */ - override val invisible by c.setting("Invisible", true).group(baseGroup) - - /** - * Whether dead entities are included in the targeting scope. - */ - override val dead by c.setting("Dead", false).group(baseGroup) - - /** - * Validates whether a given entity is targetable by the player based on current settings. - * - * @param player The [ClientPlayerEntity] performing the targeting. - * @param entity The [LivingEntity] being evaluated. - * @return `true` if the entity is valid for targeting, `false` otherwise. - */ - open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when { - !players && entity is OtherClientPlayerEntity -> false - players && entity is OtherClientPlayerEntity && entity.isFriend -> false - !animals && entity is PassiveEntity -> false - !hostiles && entity is HostileEntity -> false - entity is ArmorStandEntity -> false - - !invisible && entity.isInvisibleTo(player) -> false - !dead && entity.isDead -> false - - else -> true - } - - /** - * Subclass for targeting entities specifically for combat purposes. - * - * @property fov The field of view limit within which entities are considered for targeting. Configurable. - * @property priority The priority used to determine which entity is targeted when multiple candidates are available. - */ - class Combat( - c: Configurable, - baseGroup: NamedEnum, - defaultRange: Double = 5.0, - maxRange: Double = 16.0, - ) : Targeting(c, baseGroup, defaultRange, maxRange) { - - /** - * The field of view limit for targeting entities. Configurable between 5 and 180 degrees. - */ - val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup) - - /** - * The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance]. - */ - val priority by c.setting("Priority", Priority.Distance).group(baseGroup) - - /** - * Validates whether a given entity is targetable for combat based on the field of view limit and other settings. - * - * @param player The [ClientPlayerEntity] performing the targeting. - * @param entity The [LivingEntity] being evaluated. - * @return `true` if the entity is valid for targeting, `false` otherwise. - */ - override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean { - if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false - if (entity.uuid in illegalTargets) return false - return super.validate(player, entity) - } - - /** - * Gets the best target for combat based on the current settings and priority. - * - * @return The best [LivingEntity] target, or `null` if no valid target is found. - */ - fun target(): LivingEntity? = runSafe { - return@runSafe fastEntitySearch(targetingRange) { - validate(player, it) - }.minByOrNull { - priority.factor(this, it) - } - } - - private val illegalTargets = setOf( - UUID(5706954458220675710, -6736729783554821869), - UUID(-2945922493004570036, -7599209072395336449) - ) - } - - /** - * Subclass for targeting entities for ESP (Extrasensory Perception) purposes. - */ - class ESP( - c: Configurable, - baseGroup: NamedEnum, - ) : Targeting(c, baseGroup, 128.0, 1024.0) - - /** - * Enum representing the different priority factors used for determining the best target. - * - * @property factor A lambda function that calculates the priority factor for a given [LivingEntity]. - */ - @Suppress("Unused") - enum class Priority(val factor: SafeContext.(LivingEntity) -> Double) { - /** - * Prioritizes entities based on their distance from the player. - */ - Distance({ player.pos distSq it.pos }), - - /** - * Prioritizes entities based on their health. - */ - Health({ it.fullHealth }), - - /** - * Prioritizes entities based on their angle relative to the player's field of view. - */ - Fov({ player.rotation dist player.eyePos.rotationTo(it.pos) }) - } + /** + * The range within which entities can be targeted. This value is configurable and constrained + * between 1.0 and [maxRange]. + */ + override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup) + + /** + * Whether players are included in the targeting scope. + */ + override val players by c.setting("Players", true).group(baseGroup) + + /** + * Whether friends are included in the targeting scope. + * Requires [players] to be true. + */ + override val friends by c.setting("Friends", false) { players }.group(baseGroup) + + /** + * Whether mobs are included in the targeting scope. + */ + private val mobs by c.setting("Mobs", true).group(baseGroup) + + /** + * Whether hostile mobs are included in the targeting scope + */ + private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup) + + /** + * Whether hostile entities should be only attacked when they are angry + */ + private val hostileOnlyAngrySetting by c.setting( + "Hostiles Only Angry", true, + "Only attacks angerable entities if they are angered. This does not affect for example zombies but does affect endermen." + ) { hostilesSetting }.group(baseGroup) + + /** + * Whether passive entities are included in the targeting scope + */ + private val passivesSetting by c.setting("Passives", false) { mobs }.group(baseGroup) + + /** + * Whether animals are included in the targeting scope + */ + private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup) + + /** + * Indicates whether hostile entities are included in the targeting scope. + */ + override val hostiles get() = mobs && hostilesSetting + + override val hostilesOnlyAngry: Boolean + get() = mobs && hostilesSetting && hostileOnlyAngrySetting + + /** + * Indicates whether passive entities are included in the targeting scope. + */ + override val passives get() = mobs && passivesSetting + + /** + * Indicates whether animals are included in the targeting scope. + */ + override val animals get() = mobs && animalsSetting + + /** + * Whether invisible entities are included in the targeting scope. + */ + override val invisible by c.setting("Invisible", true).group(baseGroup) + + /** + * Whether dead entities are included in the targeting scope. + */ + override val dead by c.setting("Dead", false).group(baseGroup) + + /** + * Validates whether a given entity is targetable by the player based on current settings. + * + * @param player The [ClientPlayerEntity] performing the targeting. + * @param entity The [LivingEntity] being evaluated. + * @return `true` if the entity is valid for targeting, `false` otherwise. + */ + open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when { + players && entity is OtherClientPlayerEntity -> true + !players && entity is OtherClientPlayerEntity && entity.isFriend -> true + animals && (entity is AnimalEntity || entity is AmbientEntity) -> true + passives && entity is PassiveEntity -> true + hostiles && entity is HostileEntity -> { + if (hostilesOnlyAngry && entity is Angerable) { + LambdaAngerManagement.isEntityAngry(entity.uuid) + } else { + true + } + } + entity is ArmorStandEntity -> false + + invisible && entity.isInvisibleTo(player) -> true + dead && entity.isDead -> true + + else -> false + } + + /** + * Subclass for targeting entities specifically for combat purposes. + * + * @property fov The field of view limit within which entities are considered for targeting. Configurable. + * @property priority The priority used to determine which entity is targeted when multiple candidates are available. + */ + class Combat( + c: Configurable, + baseGroup: NamedEnum, + defaultRange: Double = 5.0, + maxRange: Double = 16.0, + ) : Targeting(c, baseGroup, defaultRange, maxRange) { + + /** + * The field of view limit for targeting entities. Configurable between 5 and 180 degrees. + */ + val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup) + + /** + * The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance]. + */ + val priority by c.setting("Priority", Priority.Distance).group(baseGroup) + + /** + * Validates whether a given entity is targetable for combat based on the field of view limit and other settings. + * + * @param player The [ClientPlayerEntity] performing the targeting. + * @param entity The [LivingEntity] being evaluated. + * @return `true` if the entity is valid for targeting, `false` otherwise. + */ + override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean { + if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false + if (entity.uuid in illegalTargets) return false + return super.validate(player, entity) + } + + /** + * Gets the best target for combat based on the current settings and priority. + * + * @return The best [LivingEntity] target, or `null` if no valid target is found. + */ + fun target(): LivingEntity? = runSafe { + return@runSafe fastEntitySearch(targetingRange) { + validate(player, it) + }.minByOrNull { + priority.factor(this, it) + } + } + + private val illegalTargets = setOf( + UUID(5706954458220675710, -6736729783554821869), + UUID(-2945922493004570036, -7599209072395336449) + ) + } + + /** + * Subclass for targeting entities for ESP (Extrasensory Perception) purposes. + */ + class ESP( + c: Configurable, + baseGroup: NamedEnum, + ) : Targeting(c, baseGroup, 128.0, 1024.0) + + /** + * Enum representing the different priority factors used for determining the best target. + * + * @property factor A lambda function that calculates the priority factor for a given [LivingEntity]. + */ + @Suppress("Unused") + enum class Priority(val factor: SafeContext.(LivingEntity) -> Double) { + /** + * Prioritizes entities based on their distance from the player. + */ + Distance({ player.pos distSq it.pos }), + + /** + * Prioritizes entities based on their health. + */ + Health({ it.fullHealth }), + + /** + * Prioritizes entities based on their angle relative to the player's field of view. + */ + Fov({ player.rotation dist player.eyePos.rotationTo(it.pos) }) + } } diff --git a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt index a54df2231..24c0b3ffc 100644 --- a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt +++ b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt @@ -23,6 +23,8 @@ interface TargetingConfig { val players: Boolean val friends: Boolean val hostiles: Boolean + val hostilesOnlyAngry: Boolean + val passives: Boolean val animals: Boolean val invisible: Boolean diff --git a/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt b/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt new file mode 100644 index 000000000..7897247e2 --- /dev/null +++ b/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt @@ -0,0 +1,124 @@ +/* + * 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.world + +import com.lambda.Lambda.mc +import com.lambda.core.Loadable +import com.lambda.event.events.ConnectionEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import net.minecraft.entity.Entity +import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket +import net.minecraft.util.math.Vec3d +import java.util.* + +class LambdaAngerManagement: Loadable { + + init { + listen { event -> + if (event.packet is PlaySoundS2CPacket) { + val uuid = findClosestEntity(event.packet) ?: return@listen // this was not a sound that entities emit when angry + registerAngryEntity(uuid) + } + } + listen { + angryEntities.clear() + } + // todo: task every 5 minutes or something to clean up old angry entities + } + + companion object { + const val ANGER_DURATION_MS = 60000L + private val angryEntities = mutableMapOf() + + @JvmStatic + private fun matchLocations(vec3d: Vec3d, radius: Double = 3.0): UUID? { + val maxDistSq = radius * radius + return mc.world?.entities + ?.asSequence() + ?.map { it to it.distanceSqTo(vec3d) } + ?.filter { it.second <= maxDistSq } + ?.minByOrNull { it.second } + ?.first?.uuid + } + + fun Entity.distanceSqTo(vec3d: Vec3d): Double { + val dx = this.x - vec3d.x + val dy = this.y - vec3d.y + val dz = this.z - vec3d.z + return dx * dx + dy * dy + dz * dz + } + + @JvmStatic + private fun findClosestEntity(packet: PlaySoundS2CPacket): UUID? { + val vec3 = Vec3d(packet.x, packet.y, packet.z) + val soundId = packet.sound.value().id.path + return when { + soundId.contains(".growl") || // (wolf) randomly while angry + soundId.contains(".stare") || // when it mad when u look at it + soundId.contains(".scream") || // randomly when hostile + soundId.contains(".angry") // (zombie pigmen) randomly when angry and when getting angry + -> matchLocations(vec3) + else -> null + } + } + /* + todo: spiders + + things that give them away when they are aggro: + - head directly towards the player + - the "jump attack", can be easily detected as normally they don't have + that much positive y velocity when climbing + - spiders could be tracked separately and the "aggro score" could be + decreased if they are not moving as their normal pathing behavior is to + path randomly around ~5-10 blocks, no jumps, and then stop for a while + */ + + @JvmStatic + fun registerAngryEntity(entityUuid: UUID) { + angryEntities[entityUuid] = System.currentTimeMillis() + } + + @JvmStatic + fun isEntityAngry(entityUuid: UUID): Boolean { + val timestamp = angryEntities[entityUuid] ?: return false + val isAngry = System.currentTimeMillis() - timestamp < ANGER_DURATION_MS + if (!isAngry) { + angryEntities.remove(entityUuid) + } + return isAngry + } + + + @JvmStatic + suspend fun cleanupUnloadedEntities() = coroutineScope { + async { + angryEntities.keys.removeIf { uuid -> + !(mc.world?.entities?.any { it.uuid == uuid } ?: false) + } + } + } + + @JvmStatic + fun removeAngryEntity(entityUuid: UUID) { + angryEntities.remove(entityUuid) + } + } +} \ No newline at end of file