From da6ead8dabea242858898bbbdd3f1714db5c1a2e Mon Sep 17 00:00:00 2001 From: fretuks Date: Sun, 19 Jul 2026 20:48:06 +0200 Subject: [PATCH] implement many major AI changes. --- .../adaptivemobs/ai/AdaptiveAIGoalUtils.java | 21 ++ .../adaptivemobs/ai/AdaptiveGoalInjector.java | 9 +- .../adaptivemobs/ai/AdaptiveMemory.java | 7 +- .../adaptivemobs/ai/AdaptiveMobHooks.java | 2 +- .../ai/AdaptiveSpecialAbilities.java | 17 +- .../AdaptiveAirbornePositioningGoal.java | 5 +- .../ai/goals/AdaptiveAntiCheeseGoal.java | 346 ++++++++++++++---- .../ai/goals/AdaptiveCreeperPressureGoal.java | 150 +++++++- .../ai/goals/AdaptiveEndermanTacticsGoal.java | 151 +++++++- .../goals/AdaptiveEndermiteTacticsGoal.java | 8 +- .../ai/goals/AdaptiveGuardianTacticsGoal.java | 7 +- .../ai/goals/AdaptiveRavagerTacticsGoal.java | 8 +- .../ai/goals/AdaptiveSharedTargetGoal.java | 8 +- .../ai/goals/AdaptiveShulkerTacticsGoal.java | 9 +- .../ai/goals/AdaptiveSpiderTacticsGoal.java | 62 +++- .../ai/goals/AdaptiveTargetPriorityGoal.java | 6 +- .../ai/goals/AdaptiveTargetSanitizerGoal.java | 66 ++++ .../ai/goals/AdaptiveThreatMemoryGoal.java | 3 + .../ai/goals/AdaptiveWardenTacticsGoal.java | 9 +- .../ai/goals/SharedTargetGoal.java | 8 +- .../event/AdaptiveMobsEvents.java | 11 +- .../resources/META-INF/accesstransformer.cfg | 3 + 22 files changed, 799 insertions(+), 117 deletions(-) create mode 100644 src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetSanitizerGoal.java diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveAIGoalUtils.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveAIGoalUtils.java index c79a7b7..4b9109a 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveAIGoalUtils.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveAIGoalUtils.java @@ -15,6 +15,8 @@ import java.util.function.IntSupplier; public final class AdaptiveAIGoalUtils { + private static final String ADAPTIVE_STACKED_ZOMBIE_KEY = "am_adaptive_stacked_zombie"; + private AdaptiveAIGoalUtils() { } @@ -46,6 +48,25 @@ public final class AdaptiveAIGoalUtils { return stack.isEdible(); } + public static boolean isValidAdaptiveTarget(LivingEntity entity) { + if (entity == null || !entity.isAlive()) { + return false; + } + return !(entity instanceof Player player) || (!player.isCreative() && !player.isSpectator()); + } + + public static void markAdaptiveStackedZombie(Mob mob) { + mob.getPersistentData().putBoolean(ADAPTIVE_STACKED_ZOMBIE_KEY, true); + } + + public static boolean isAdaptiveStackedZombie(Mob mob) { + return mob.getPersistentData().getBoolean(ADAPTIVE_STACKED_ZOMBIE_KEY); + } + + public static void clearAdaptiveStackedZombie(Mob mob) { + mob.getPersistentData().remove(ADAPTIVE_STACKED_ZOMBIE_KEY); + } + public static boolean isLikelyMeleeWeapon(ItemStack stack) { return stack.is(Items.WOODEN_SWORD) || stack.is(Items.STONE_SWORD) || stack.is(Items.IRON_SWORD) || stack.is(Items.GOLDEN_SWORD) || stack.is(Items.DIAMOND_SWORD) || stack.is(Items.NETHERITE_SWORD) diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java index 81ab166..56b5870 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java @@ -27,6 +27,7 @@ import net.fretux.adaptivemobs.ai.goals.AdaptiveSpiderDisengageGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveSpiderTacticsGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveSupportGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveTargetPriorityGoal; +import net.fretux.adaptivemobs.ai.goals.AdaptiveTargetSanitizerGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveThreatMemoryGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveWardenTacticsGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveWitchTacticsGoal; @@ -92,6 +93,7 @@ public final class AdaptiveGoalInjector { } PathfinderMob pathfinder = mob instanceof PathfinderMob pf ? pf : null; IntSupplier tier = () -> AdaptiveDifficultyManager.getMobTier(level, mob.getType()); + mob.targetSelector.addGoal(0, new AdaptiveTargetSanitizerGoal(mob)); if (tier.getAsInt() >= 4) { mob.targetSelector.addGoal(2, new AdaptiveSharedTargetGoal(mob, tier, 4, 12.0D)); @@ -106,6 +108,7 @@ public final class AdaptiveGoalInjector { // otherwise it can never take over while the zombie is already fighting. mob.goalSelector.addGoal(1, new AdaptiveZombiePackTacticsGoal((Zombie) mob, tier)); } else if (pathfinder != null && mob instanceof Creeper creeper && AMConfig.AI_CREEPER.get()) { + creeper.maxSwell = creeperFuseTicks(tier.getAsInt()); mob.goalSelector.addGoal(2, new AdaptiveCreeperPressureGoal(creeper, tier)); mob.goalSelector.addGoal(4, new AdaptiveFlankGoal(pathfinder, tier, 2, 3.0D, 1.0D, false)); } else if (pathfinder != null && mob instanceof Spider && AMConfig.AI_SPIDER.get()) { @@ -145,7 +148,7 @@ public final class AdaptiveGoalInjector { mob.goalSelector.addGoal(8, new AdaptiveBiomeTacticsGoal(pathfinder, tier)); } if (pathfinder != null && AMConfig.ENABLE_ANTI_CHEESE_AI.get()) { - mob.goalSelector.addGoal(mob instanceof Zombie ? 1 : 7, new AdaptiveAntiCheeseGoal(pathfinder, tier)); + mob.goalSelector.addGoal(0, new AdaptiveAntiCheeseGoal(pathfinder, tier)); } if (pathfinder != null && isRangedMob(mob) && rangedEnabled(mob)) { mob.goalSelector.addGoal(3, new AdaptiveRangedAttackGoal(pathfinder, tier, rangedOptimalRange(mob), 1.0D)); @@ -307,6 +310,10 @@ public final class AdaptiveGoalInjector { return 1.05D; } + private static int creeperFuseTicks(int tier) { + return Math.max(18, 30 - Math.max(0, tier - 1) * 3); + } + private static boolean conservativeMelee(Mob mob) { return mob instanceof PiglinBrute || mob instanceof Ravager || mob instanceof Warden || mob instanceof ElderGuardian; } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMemory.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMemory.java index 1832f5c..1acb3dd 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMemory.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMemory.java @@ -28,6 +28,9 @@ public final class AdaptiveMemory { if (!(mob.level() instanceof ServerLevel level)) { return; } + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(player) || !mob.canAttack(player)) { + return; + } THREATS.put(mob, new ThreatEntry(player.getUUID(), level.getGameTime() + ticks)); } @@ -41,7 +44,9 @@ public final class AdaptiveMemory { return Optional.empty(); } ServerPlayer player = level.getServer().getPlayerList().getPlayer(entry.playerId); - return player != null && player.isAlive() ? Optional.of(player) : Optional.empty(); + return AdaptiveAIGoalUtils.isValidAdaptiveTarget(player) && mob.canAttack(player) + ? Optional.of(player) + : Optional.empty(); } public static void rememberSound(ServerLevel level, BlockPos pos, SoundKind kind, int ticks) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMobHooks.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMobHooks.java index 7618731..18a9e38 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMobHooks.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveMobHooks.java @@ -27,7 +27,7 @@ public final class AdaptiveMobHooks { return; } LivingEntity target = mob.getTarget(); - if (target == null || !target.isAlive()) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = AdaptiveDifficultyManager.getMobTier(level, mob.getType()); diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java index 2129ef2..f24b1b9 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java @@ -91,9 +91,11 @@ public final class AdaptiveSpecialAbilities { if (mob.getRandom().nextFloat() < poisonChance) { arrow.addEffect(new MobEffectInstance(MobEffects.POISON, 80 + mob.getRandom().nextInt(41), 0)); } - if (tier >= 5 && target != null && AdaptiveAIGoalUtils.isBlockingShield(target) && mob.getRandom().nextFloat() < 0.18F) { + if (tier >= 5 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(target) + && AdaptiveAIGoalUtils.isBlockingShield(target) && mob.getRandom().nextFloat() < 0.18F) { arrow.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 60, 0)); - } else if (tier >= 5 && target != null && target.isSprinting() && mob.getRandom().nextFloat() < 0.18F) { + } else if (tier >= 5 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(target) && target.isSprinting() + && mob.getRandom().nextFloat() < 0.18F) { arrow.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 60, 0)); } return; @@ -145,7 +147,8 @@ public final class AdaptiveSpecialAbilities { continue; } ServerLevel level = event.getServer().getLevel(pending.dimension); - if (level != null && pending.mob.isAlive() && pending.target.isAlive()) { + if (level != null && pending.mob.isAlive() && AdaptiveAIGoalUtils.isValidAdaptiveTarget(pending.target) + && pending.mob.canAttack(pending.target)) { Vec3 side = AdaptivePositioningUtils.sidePosition(pending.mob, pending.target, 1.6D, pending.mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D); pending.mob.moveTo(side.x, side.y, side.z, pending.mob.getYRot(), pending.mob.getXRot()); @@ -159,7 +162,7 @@ public final class AdaptiveSpecialAbilities { private static void onMobDamaged(ServerLevel level, Mob mob, LivingEntity attacker, Entity directEntity) { int tier = tier(level, mob); - if (tier <= 0) { + if (tier <= 0 || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(attacker) || !mob.canAttack(attacker)) { return; } if (tier >= 5 && mob instanceof ZombifiedPiglin && ready(mob, "blood_call", 160)) { @@ -170,7 +173,7 @@ public final class AdaptiveSpecialAbilities { } AdaptiveAIGoalUtils.debug(mob, () -> tier, "zombified piglin blood call"); } - if (tier >= 4 && mob instanceof Spider && ready(mob, "web_retreat", 150)) { + if (tier >= 4 && mob instanceof Spider && ready(mob, "web_retreat", 600)) { placeTemporaryCobweb(level, attacker.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61)); } if (tier >= 4 && mob instanceof EnderMan enderman && directEntity == attacker && ready(mob, "backstep_ambush", tier >= 5 ? 80 : 130)) { @@ -215,7 +218,7 @@ public final class AdaptiveSpecialAbilities { private static void onPlayerHurt(ServerLevel level, ServerPlayer player, Mob mob, Entity directEntity, LivingHurtEvent event) { int tier = tier(level, mob); - if (tier <= 0) { + if (tier <= 0 || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { return; } if (tier >= 4 && mob instanceof Drowned && mob.isInWater() && player.isInWater() && ready(mob, "dragging_grip", 160)) { @@ -227,7 +230,7 @@ public final class AdaptiveSpecialAbilities { player.addEffect(new MobEffectInstance(MobEffects.WITHER, tier >= 5 ? 140 : 100, 0)); shove(player, mob.position(), tier >= 5 ? 0.55D : 0.35D, 0.08D); } - if (mob instanceof Spider && tier >= 4 && ready(mob, "web_hit", mob instanceof CaveSpider ? 100 : 140)) { + if (mob instanceof Spider && tier >= 4 && ready(mob, "web_hit", mob instanceof CaveSpider ? 400 : 600)) { placeTemporaryCobweb(level, player.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61)); } if (mob instanceof CaveSpider && tier >= 3 diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAirbornePositioningGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAirbornePositioningGoal.java index f7f4454..d9eb709 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAirbornePositioningGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAirbornePositioningGoal.java @@ -34,14 +34,15 @@ public class AdaptiveAirbornePositioningGoal extends Goal { @Override public boolean canUse() { - return tierSupplier.getAsInt() >= 2 && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0; + return tierSupplier.getAsInt() >= 2 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(mob.getTarget()) + && --cooldown <= 0; } @Override public void start() { cooldown = AdaptiveAIGoalUtils.nextCooldown(mob); LivingEntity target = mob.getTarget(); - if (target == null || !target.isAlive()) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java index 79ae789..ad74c68 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java @@ -6,17 +6,24 @@ import net.fretux.adaptivemobs.config.AMConfig; import net.minecraft.tags.FluidTags; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.PathfinderMob; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.monster.AbstractSkeleton; -import net.minecraft.world.entity.monster.Creeper; import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.entity.monster.Enemy; import net.minecraft.world.entity.monster.Pillager; +import net.minecraft.world.entity.monster.Spider; import net.minecraft.world.entity.monster.Zombie; import net.minecraft.world.entity.vehicle.AbstractMinecart; import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.entity.vehicle.ChestBoat; import net.minecraft.core.BlockPos; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; @@ -26,9 +33,13 @@ import java.util.function.IntSupplier; public class AdaptiveAntiCheeseGoal extends Goal { + private static final double TRAP_VEHICLE_BREAK_DISTANCE_SQR = 9.0D; + private final PathfinderMob mob; private final IntSupplier tierSupplier; private int cooldown; + private LivingEntity activePillarTarget; + private int activePillarTicks; public AdaptiveAntiCheeseGoal(PathfinderMob mob, IntSupplier tierSupplier) { this.mob = mob; @@ -38,28 +49,32 @@ public class AdaptiveAntiCheeseGoal extends Goal { @Override public boolean canUse() { - if (!AMConfig.ENABLE_ANTI_CHEESE_AI.get() || tierSupplier.getAsInt() < 3) { + if (!AMConfig.ENABLE_ANTI_CHEESE_AI.get()) { return false; } LivingEntity target = mob.getTarget(); - if (target == null || !target.isAlive()) { - return false; - } - if (isInTrapVehicle() || isNearTrapVehicle() || nearestTrapBlock() != null) { + if (isInTrapVehicle() || isTooCloseToTrapVehicle() || nearestTrapBlock() != null) { return true; } - if (--cooldown > 0) { + if (trappedAllyVehicle() != null) { + return true; + } + if (mob instanceof Zombie && mob.isPassenger() && shouldUnstack(target)) { + return true; + } + if (tierSupplier.getAsInt() < 3) { + return false; + } + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return false; } if (mob instanceof Zombie && isVerticalObstacleCheese(target)) { return true; } - if ((mob instanceof AbstractSkeleton || mob instanceof Pillager) && !mob.hasLineOfSight(target)) { - return true; + if (--cooldown > 0) { + return false; } - if (mob instanceof Creeper creeper && target.hasLineOfSight(creeper) - && AdaptivePositioningUtils.isOpenToSky(mob) - && Math.sqrt(mob.distanceToSqr(target)) > 4.0D) { + if ((mob instanceof AbstractSkeleton || mob instanceof Pillager) && !mob.hasLineOfSight(target)) { return true; } if (mob instanceof EnderMan && (mob.level().getFluidState(mob.blockPosition()).is(FluidTags.WATER) @@ -75,40 +90,46 @@ public class AdaptiveAntiCheeseGoal extends Goal { @Override public void start() { LivingEntity target = mob.getTarget(); - if (target == null) { - return; - } - mob.getLookControl().setLookAt(target, 30.0F, 30.0F); - if (avoidTrap()) { cooldown = 8 + mob.getRandom().nextInt(16); return; } - cooldown = 55 + mob.getRandom().nextInt(80); - if (mob instanceof Zombie && isVerticalObstacleCheese(target)) { - if (tryCorpseLadderSurge(target)) { - return; - } - if (tryZombieStack(target)) { - return; - } - Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 2.5D, 0.0D); - moveIfReasonable(dest, 1.0D, "zombie pillar gather"); + Entity trappedAllyVehicle = trappedAllyVehicle(); + if (trappedAllyVehicle != null && tryDestroyTrapVehicle(trappedAllyVehicle)) { + cooldown = 10 + mob.getRandom().nextInt(20); return; } + if (mob instanceof Zombie && mob.isPassenger() && shouldUnstack(target)) { + mob.stopRiding(); + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + cooldown = 12 + mob.getRandom().nextInt(18); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie unstacking"); + return; + } + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { + return; + } + mob.getLookControl().setLookAt(target, 30.0F, 30.0F); + + if (mob instanceof Zombie && isVerticalObstacleCheese(target)) { + activePillarTarget = target; + activePillarTicks = 24 + mob.getRandom().nextInt(16); + cooldown = 6 + mob.getRandom().nextInt(8); + if (tryCorpseLadderPressure(target)) { + return; + } + if (isPillaring(target) && tryZombieStack(target)) { + return; + } + pressurePillarBase(target); + return; + } + cooldown = 55 + mob.getRandom().nextInt(80); if ((mob instanceof AbstractSkeleton || mob instanceof Pillager) && !mob.hasLineOfSight(target)) { Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 7.0D, 0.0D); moveIfReasonable(dest, 1.0D, "ranged tiny-window reposition"); return; } - if (mob instanceof Creeper creeper && target.hasLineOfSight(creeper) - && AdaptivePositioningUtils.isOpenToSky(mob) - && Math.sqrt(mob.distanceToSqr(target)) > 4.0D) { - creeper.setSwellDir(-1); - Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 5.0D, 0.0D); - moveIfReasonable(dest, 1.0D, "creeper open-ground hesitation"); - return; - } if (mob instanceof EnderMan && (mob.level().getFluidState(mob.blockPosition()).is(FluidTags.WATER) || target.level().getFluidState(target.blockPosition()).is(FluidTags.WATER))) { Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 7.0D); @@ -119,18 +140,20 @@ public class AdaptiveAntiCheeseGoal extends Goal { private boolean avoidTrap() { Entity vehicle = mob.getVehicle(); if (vehicle instanceof Boat || vehicle instanceof AbstractMinecart) { - mob.stopRiding(); + mob.getNavigation().stop(); + tryDestroyTrapVehicle(vehicle); moveAwayFrom(vehicle.position(), 1.15D, "trap vehicle escape"); return true; } Entity trapVehicle = nearestTrapVehicle(); - if (trapVehicle != null && mob.distanceToSqr(trapVehicle) < 2.9D) { - moveAwayFrom(trapVehicle.position(), 1.05D, "trap vehicle avoidance"); + if (trapVehicle != null && mob.distanceToSqr(trapVehicle) < TRAP_VEHICLE_BREAK_DISTANCE_SQR) { + tryDestroyTrapVehicle(trapVehicle); return true; } - Vec3 trapBlock = nearestTrapBlock(); + BlockPos trapBlock = nearestTrapBlock(); if (trapBlock != null) { - moveAwayFrom(trapBlock, 1.05D, "trap block avoidance"); + tryDestroyTrapBlock(trapBlock); + moveAwayFrom(Vec3.atCenterOf(trapBlock), 1.05D, "trap block avoidance"); return true; } return false; @@ -141,13 +164,13 @@ public class AdaptiveAntiCheeseGoal extends Goal { return vehicle instanceof Boat || vehicle instanceof AbstractMinecart; } - private boolean isNearTrapVehicle() { + private boolean isTooCloseToTrapVehicle() { Entity trapVehicle = nearestTrapVehicle(); - return trapVehicle != null && mob.distanceToSqr(trapVehicle) < 2.9D; + return trapVehicle != null && mob.distanceToSqr(trapVehicle) < TRAP_VEHICLE_BREAK_DISTANCE_SQR; } private Entity nearestTrapVehicle() { - List vehicles = mob.level().getEntitiesOfClass(Entity.class, mob.getBoundingBox().inflate(2.5D), + List vehicles = mob.level().getEntitiesOfClass(Entity.class, mob.getBoundingBox().inflate(3.5D), entity -> entity instanceof Boat || entity instanceof AbstractMinecart); Entity nearest = null; double best = Double.MAX_VALUE; @@ -161,8 +184,105 @@ public class AdaptiveAntiCheeseGoal extends Goal { return nearest; } - private Vec3 nearestTrapBlock() { - Vec3 nearest = null; + private Entity trappedAllyVehicle() { + List vehicles = mob.level().getEntitiesOfClass(Entity.class, mob.getBoundingBox().inflate(8.0D), + entity -> isTrapVehicle(entity) && hasTrappedHostilePassenger(entity)); + Entity nearest = null; + double best = Double.MAX_VALUE; + for (Entity vehicle : vehicles) { + double dist = mob.distanceToSqr(vehicle); + if (dist < best) { + best = dist; + nearest = vehicle; + } + } + return nearest; + } + + private boolean hasTrappedHostilePassenger(Entity vehicle) { + for (Entity passenger : vehicle.getPassengers()) { + if (passenger != mob && passenger instanceof Mob trapped && trapped instanceof Enemy && trapped.isAlive()) { + return true; + } + } + return false; + } + + @Override + public boolean canContinueToUse() { + return activePillarTicks > 0 + && mob instanceof Zombie + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(activePillarTarget) + && isVerticalObstacleCheese(activePillarTarget); + } + + @Override + public void tick() { + if (activePillarTicks-- <= 0 || !(mob instanceof Zombie) + || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(activePillarTarget)) { + return; + } + mob.getLookControl().setLookAt(activePillarTarget, 30.0F, 30.0F); + if (mob.isPassenger()) { + tryCorpseLadderPressure(activePillarTarget); + return; + } + if (!tryZombieStack(activePillarTarget)) { + pressurePillarBase(activePillarTarget); + } + } + + private boolean isTrapVehicle(Entity entity) { + return entity instanceof Boat || entity instanceof AbstractMinecart; + } + + private boolean tryDestroyTrapVehicle(Entity vehicle) { + if (mob.isPassenger() && mob.getVehicle() != vehicle) { + return false; + } + if (mob.distanceToSqr(vehicle) > TRAP_VEHICLE_BREAK_DISTANCE_SQR) { + mob.getNavigation().moveTo(vehicle.getX(), vehicle.getY(), vehicle.getZ(), 1.12D); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "moving to break ally trap vehicle"); + return true; + } + mob.swing(mob.getUsedItemHand()); + dropTrapVehicleItem(vehicle); + vehicle.ejectPassengers(); + vehicle.discard(); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "breaking ally trap vehicle"); + return true; + } + + private void dropTrapVehicleItem(Entity vehicle) { + Item drop = null; + if (vehicle instanceof Boat boat) { + drop = boatDrop(boat); + } else if (vehicle instanceof AbstractMinecart) { + drop = Items.MINECART; + } + if (drop != null) { + vehicle.spawnAtLocation(drop); + } + } + + private Item boatDrop(Boat boat) { + boolean chest = boat instanceof ChestBoat; + return switch (boat.getVariant()) { + case OAK -> chest ? Items.OAK_CHEST_BOAT : Items.OAK_BOAT; + case SPRUCE -> chest ? Items.SPRUCE_CHEST_BOAT : Items.SPRUCE_BOAT; + case BIRCH -> chest ? Items.BIRCH_CHEST_BOAT : Items.BIRCH_BOAT; + case JUNGLE -> chest ? Items.JUNGLE_CHEST_BOAT : Items.JUNGLE_BOAT; + case ACACIA -> chest ? Items.ACACIA_CHEST_BOAT : Items.ACACIA_BOAT; + case DARK_OAK -> chest ? Items.DARK_OAK_CHEST_BOAT : Items.DARK_OAK_BOAT; + case MANGROVE -> chest ? Items.MANGROVE_CHEST_BOAT : Items.MANGROVE_BOAT; + case CHERRY -> chest ? Items.CHERRY_CHEST_BOAT : Items.CHERRY_BOAT; + case BAMBOO -> chest ? Items.BAMBOO_CHEST_RAFT : Items.BAMBOO_RAFT; + default -> chest ? Items.OAK_CHEST_BOAT : Items.OAK_BOAT; + }; + } + + private BlockPos nearestTrapBlock() { + BlockPos nearest = null; double best = Double.MAX_VALUE; for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { @@ -171,8 +291,8 @@ public class AdaptiveAntiCheeseGoal extends Goal { if (!isTrapBlock(state)) { continue; } - Vec3 pos = Vec3.atCenterOf(mob.blockPosition().offset(x, y, z)); - double dist = mob.position().distanceToSqr(pos); + BlockPos pos = mob.blockPosition().offset(x, y, z); + double dist = mob.position().distanceToSqr(Vec3.atCenterOf(pos)); if (dist < best) { best = dist; nearest = pos; @@ -189,6 +309,24 @@ public class AdaptiveAntiCheeseGoal extends Goal { || state.is(Blocks.SWEET_BERRY_BUSH); } + private void tryDestroyTrapBlock(BlockPos pos) { + BlockState state = mob.level().getBlockState(pos); + if (!isTrapBlock(state)) { + return; + } + if (mob instanceof Spider && state.is(Blocks.COBWEB)) { + return; + } + if (state.is(Blocks.COBWEB)) { + Block.popResource(mob.level(), pos, new ItemStack(Items.STRING)); + } else if (state.is(Blocks.SWEET_BERRY_BUSH)) { + Block.popResource(mob.level(), pos, new ItemStack(Items.SWEET_BERRIES)); + } + mob.level().destroyBlock(pos, false, mob); + mob.getNavigation().stop(); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "breaking trap block"); + } + private void moveAwayFrom(Vec3 threat, double speed, String debug) { Vec3 away = mob.position().subtract(threat); double len = Math.sqrt(away.x * away.x + away.z * away.z); @@ -208,15 +346,15 @@ public class AdaptiveAntiCheeseGoal extends Goal { zombie -> zombie != mob && zombie.isAlive() && !zombie.isVehicle() - && !zombie.isPassenger() && zombie.getTarget() == target - && Math.abs(zombie.getY() - mob.getY()) < 1.25D); + && !isInVehicleChain(zombie, mob) + && Math.abs(zombie.getY() - mob.getY()) < 8.0D); Zombie bestCarrier = null; - double best = Double.MAX_VALUE; + double best = -1.0D; for (Zombie carrier : carriers) { - double dist = mob.distanceToSqr(carrier); - if (dist < best) { - best = dist; + double score = stackHeight(carrier) * 10.0D - mob.distanceToSqr(carrier); + if (score > best) { + best = score; bestCarrier = carrier; } } @@ -225,32 +363,108 @@ public class AdaptiveAntiCheeseGoal extends Goal { } boolean mounted = mob.startRiding(bestCarrier, true); if (mounted) { + AdaptiveAIGoalUtils.markAdaptiveStackedZombie(mob); AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie stacking over obstacle"); } return mounted; } - private boolean tryCorpseLadderSurge(LivingEntity target) { - if (tierSupplier.getAsInt() < 5 || !mob.isPassenger()) { + private boolean tryCorpseLadderPressure(LivingEntity target) { + if (!(mob instanceof Zombie) || !mob.isPassenger()) { return false; } + Entity base = stackBase(mob); + Vec3 baseDest = new Vec3(target.getX(), base.getY(), target.getZ()); + if (base instanceof PathfinderMob baseMob) { + baseMob.getNavigation().moveTo(baseDest.x, baseDest.y, baseDest.z, 1.05D); + } + double vertical = target.getY() - mob.getY(); double horizontal = Math.sqrt((target.getX() - mob.getX()) * (target.getX() - mob.getX()) + (target.getZ() - mob.getZ()) * (target.getZ() - mob.getZ())); - if (vertical < 0.75D || vertical > 2.25D || horizontal > 2.0D) { - return false; + if (horizontal > 3.2D || vertical < 0.25D || vertical > 5.5D) { + return true; } - List nearby = mob.level().getEntitiesOfClass(Zombie.class, mob.getBoundingBox().inflate(2.5D), - zombie -> zombie.isAlive() && zombie.getTarget() == target); - if (nearby.size() < 3) { - return false; - } - mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, 0.45D, 0.0D)); + + Vec3 toward = target.position().subtract(mob.position()); + double len = Math.max(0.001D, Math.sqrt(toward.x * toward.x + toward.z * toward.z)); + double lift = tierSupplier.getAsInt() >= 5 ? 0.42D : 0.32D; + mob.setDeltaMovement(mob.getDeltaMovement().add(toward.x / len * 0.18D, lift, toward.z / len * 0.18D)); mob.hurtMarked = true; - AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie corpse ladder surge"); + + if (vertical <= 1.8D && horizontal <= 2.4D) { + mob.stopRiding(); + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + } + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie corpse ladder pressure"); return true; } + private void pressurePillarBase(LivingEntity target) { + Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 1.4D, 0.0D); + if (!moveIfReasonable(dest, 1.08D, "zombie pillar gather")) { + mob.getNavigation().moveTo(target.getX(), mob.getY(), target.getZ(), 1.08D); + } + double horizontal = Math.sqrt((target.getX() - mob.getX()) * (target.getX() - mob.getX()) + + (target.getZ() - mob.getZ()) * (target.getZ() - mob.getZ())); + double vertical = target.getY() - mob.getY(); + if (horizontal < 2.0D && vertical > 1.8D && mob.onGround()) { + Vec3 toward = target.position().subtract(mob.position()); + double len = Math.max(0.001D, Math.sqrt(toward.x * toward.x + toward.z * toward.z)); + mob.setDeltaMovement(mob.getDeltaMovement().add(toward.x / len * 0.10D, 0.26D, toward.z / len * 0.10D)); + mob.hurtMarked = true; + } + } + + private boolean shouldUnstack(LivingEntity target) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + return true; + } + if (mob.distanceToSqr(target) > 8.0D * 8.0D) { + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + return true; + } + Entity base = stackBase(mob); + double verticalFromBase = target.getY() - base.getY(); + double horizontalFromBase = Math.sqrt((target.getX() - base.getX()) * (target.getX() - base.getX()) + + (target.getZ() - base.getZ()) * (target.getZ() - base.getZ())); + if (verticalFromBase > 2.5D && horizontalFromBase < 5.5D) { + return false; + } + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + return !isVerticalObstacleCheese(target); + } + + private int stackHeight(Entity entity) { + int height = 1; + Entity cursor = entity; + while (cursor.getVehicle() instanceof Zombie) { + height++; + cursor = cursor.getVehicle(); + } + return height; + } + + private boolean isInVehicleChain(Entity start, Entity candidate) { + Entity cursor = start; + while (cursor != null) { + if (cursor == candidate) { + return true; + } + cursor = cursor.getVehicle(); + } + return false; + } + + private Entity stackBase(Entity entity) { + Entity cursor = entity; + while (cursor.getVehicle() instanceof Zombie) { + cursor = cursor.getVehicle(); + } + return cursor; + } + private boolean isVerticalObstacleCheese(LivingEntity target) { return isPillaring(target) || isSimpleWallBlocked(target); } @@ -285,10 +499,12 @@ public class AdaptiveAntiCheeseGoal extends Goal { return !mob.level().getBlockState(pos).getCollisionShape(mob.level(), pos).isEmpty(); } - private void moveIfReasonable(Vec3 dest, double speed, String debug) { + private boolean moveIfReasonable(Vec3 dest, double speed, String debug) { if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) { mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed); AdaptiveAIGoalUtils.debug(mob, tierSupplier, debug); + return true; } + return false; } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperPressureGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperPressureGoal.java index 601ea55..e3dcf9b 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperPressureGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperPressureGoal.java @@ -2,18 +2,28 @@ package net.fretux.adaptivemobs.ai.goals; import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.monster.Creeper; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.Path; import net.minecraft.world.phys.Vec3; import java.util.EnumSet; +import java.util.List; import java.util.function.IntSupplier; public class AdaptiveCreeperPressureGoal extends Goal { private final Creeper creeper; private final IntSupplier tierSupplier; + private int pathCheckCooldown; + private boolean targetReachable; public AdaptiveCreeperPressureGoal(Creeper creeper, IntSupplier tierSupplier) { this.creeper = creeper; @@ -24,13 +34,13 @@ public class AdaptiveCreeperPressureGoal extends Goal { @Override public boolean canUse() { LivingEntity target = creeper.getTarget(); - return tierSupplier.getAsInt() >= 3 && target != null && target.isAlive(); + return tierSupplier.getAsInt() >= 3 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(target); } @Override public void tick() { LivingEntity target = creeper.getTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); @@ -38,14 +48,26 @@ public class AdaptiveCreeperPressureGoal extends Goal { double stopSwell = tier >= 4 ? 7.0D : 6.0D; double dist = Math.sqrt(creeper.distanceToSqr(target)); creeper.getLookControl().setLookAt(target, 30.0F, 30.0F); + updateReachability(target); + + if (avoidTrapOnApproach(target)) { + creeper.setSwellDir(-1); + return; + } + + if (!targetReachable) { + keepDistance(target, dist); + return; + } if (AdaptiveAIGoalUtils.isBlockingShield(target) && dist <= stopSwell) { creeper.setSwellDir(-1); - Vec3 dest = AdaptivePositioningUtils.sidePosition(creeper, target, Math.max(2.8D, dist), - creeper.getRandom().nextBoolean() ? 1.0D : -1.0D, tier >= 5 ? 0.25D : 0.0D); - if (AdaptivePositioningUtils.isReasonableGround(creeper, dest)) { - creeper.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.05D); + Vec3 dest = behindTarget(target); + if (!AdaptivePositioningUtils.isReasonableGround(creeper, dest)) { + dest = AdaptivePositioningUtils.sidePosition(creeper, target, Math.max(2.8D, dist), + creeper.getRandom().nextBoolean() ? 1.0D : -1.0D, tier >= 5 ? 0.25D : 0.0D); } + moveIfReasonable(dest, 1.12D, "creeper shield backstep"); return; } if (dist <= startSwell || (tier >= 4 && target.getLastHurtMob() != null && dist <= stopSwell)) { @@ -53,6 +75,122 @@ public class AdaptiveCreeperPressureGoal extends Goal { creeper.getNavigation().stop(); } else { creeper.setSwellDir(-1); + creeper.getNavigation().moveTo(target, tier >= 5 ? 1.12D : 1.05D); } } + + private void updateReachability(LivingEntity target) { + if (pathCheckCooldown-- > 0) { + return; + } + pathCheckCooldown = 15 + creeper.getRandom().nextInt(10); + Path path = creeper.getNavigation().createPath(target, 0); + targetReachable = path != null && path.canReach(); + } + + private void keepDistance(LivingEntity target, double dist) { + creeper.setSwellDir(-1); + double desired = 8.0D; + if (dist < desired) { + Vec3 dest = AdaptivePositioningUtils.retreatPosition(creeper, target, desired - dist + 3.0D); + if (!moveIfReasonable(dest, 1.05D, "creeper unreachable spacing")) { + Vec3 side = AdaptivePositioningUtils.sidePosition(creeper, target, desired, 1.0D, 0.0D); + moveIfReasonable(side, 1.0D, "creeper unreachable side spacing"); + } + } else { + creeper.getNavigation().stop(); + } + } + + private Vec3 behindTarget(LivingEntity target) { + Vec3 look = target.getLookAngle(); + double len = Math.sqrt(look.x * look.x + look.z * look.z); + if (len < 1.0E-3D) { + return target.position(); + } + return target.position().subtract(look.x / len * 1.8D, 0.0D, look.z / len * 1.8D); + } + + private boolean avoidTrapOnApproach(LivingEntity target) { + Entity trapVehicle = nearestTrapVehicle(); + if (trapVehicle != null && creeper.distanceToSqr(trapVehicle) < 9.0D) { + return moveAwayFrom(trapVehicle.position(), 1.1D, "creeper trap vehicle avoidance"); + } + Vec3 trapBlock = nearestTrapBlock(); + if (trapBlock != null && creeper.position().distanceToSqr(trapBlock) < 6.25D) { + return moveAwayFrom(trapBlock, 1.1D, "creeper trap block avoidance"); + } + Vec3 toTarget = target.position().subtract(creeper.position()); + double len = Math.sqrt(toTarget.x * toTarget.x + toTarget.z * toTarget.z); + if (len < 1.0E-3D) { + return false; + } + Vec3 ahead = creeper.position().add(toTarget.x / len * 2.0D, 0.0D, toTarget.z / len * 2.0D); + BlockState aheadState = creeper.level().getBlockState(BlockPos.containing(ahead)); + return isTrapBlock(aheadState) && moveAwayFrom(ahead, 1.1D, "creeper approach trap avoidance"); + } + + private Entity nearestTrapVehicle() { + List vehicles = creeper.level().getEntitiesOfClass(Entity.class, creeper.getBoundingBox().inflate(2.8D), + entity -> entity instanceof Boat || entity instanceof AbstractMinecart); + Entity nearest = null; + double best = Double.MAX_VALUE; + for (Entity entity : vehicles) { + double dist = creeper.distanceToSqr(entity); + if (dist < best) { + best = dist; + nearest = entity; + } + } + return nearest; + } + + private Vec3 nearestTrapBlock() { + Vec3 nearest = null; + double best = Double.MAX_VALUE; + BlockPos origin = creeper.blockPosition(); + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + for (int z = -1; z <= 1; z++) { + BlockPos pos = origin.offset(x, y, z); + if (!isTrapBlock(creeper.level().getBlockState(pos))) { + continue; + } + Vec3 center = Vec3.atCenterOf(pos); + double dist = creeper.position().distanceToSqr(center); + if (dist < best) { + best = dist; + nearest = center; + } + } + } + } + return nearest; + } + + private boolean isTrapBlock(BlockState state) { + return state.is(Blocks.COBWEB) + || state.is(Blocks.POWDER_SNOW) + || state.is(Blocks.SWEET_BERRY_BUSH); + } + + private boolean moveAwayFrom(Vec3 threat, double speed, String debug) { + Vec3 away = creeper.position().subtract(threat); + double len = Math.sqrt(away.x * away.x + away.z * away.z); + if (len < 1.0E-3D) { + away = new Vec3(creeper.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D, 0.0D); + len = 1.0D; + } + Vec3 dest = creeper.position().add(away.x / len * 4.0D, 0.0D, away.z / len * 4.0D); + return moveIfReasonable(dest, speed, debug); + } + + private boolean moveIfReasonable(Vec3 dest, double speed, String debug) { + if (AdaptivePositioningUtils.isReasonableGround(creeper, dest)) { + creeper.getNavigation().moveTo(dest.x, dest.y, dest.z, speed); + AdaptiveAIGoalUtils.debug(creeper, tierSupplier, debug); + return true; + } + return false; + } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java index a2937c4..8e677d0 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java @@ -5,11 +5,16 @@ import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils; import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils; import net.fretux.adaptivemobs.config.AMConfig; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.FluidTags; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import java.util.EnumSet; @@ -17,9 +22,32 @@ import java.util.function.IntSupplier; public class AdaptiveEndermanTacticsGoal extends Goal { + private static final int ANTI_PILLAR_BUILD_COOLDOWN_MIN = 4; + private static final int ANTI_PILLAR_BUILD_COOLDOWN_RANDOM = 4; + private static final BlockState[] OVERWORLD_SCAFFOLD_BLOCKS = { + Blocks.DIRT.defaultBlockState(), + Blocks.GRASS_BLOCK.defaultBlockState(), + Blocks.COARSE_DIRT.defaultBlockState(), + Blocks.ROOTED_DIRT.defaultBlockState(), + Blocks.GRAVEL.defaultBlockState(), + Blocks.SAND.defaultBlockState(), + Blocks.RED_SAND.defaultBlockState(), + Blocks.COBBLESTONE.defaultBlockState(), + Blocks.STONE.defaultBlockState() + }; + private static final BlockState[] END_SCAFFOLD_BLOCKS = { + Blocks.END_STONE.defaultBlockState(), + Blocks.OBSIDIAN.defaultBlockState(), + Blocks.COBBLESTONE.defaultBlockState() + }; + private final EnderMan enderman; private final IntSupplier tierSupplier; private int cooldown; + private BlockPos rampTargetFeet; + private Direction rampApproach; + private int rampRise; + private int rampBaseY; public AdaptiveEndermanTacticsGoal(EnderMan enderman, IntSupplier tierSupplier) { this.enderman = enderman; @@ -31,15 +59,14 @@ public class AdaptiveEndermanTacticsGoal extends Goal { public boolean canUse() { return AMConfig.ENABLE_ENDERMAN_AI.get() && tierSupplier.getAsInt() >= 2 - && enderman.getTarget() != null - && enderman.getTarget().isAlive() + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(enderman.getTarget()) && --cooldown <= 0; } @Override public void start() { LivingEntity target = enderman.getTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); @@ -51,10 +78,18 @@ public class AdaptiveEndermanTacticsGoal extends Goal { if (inEnd && tier >= 4) { double radius = tier >= 5 ? 18.0D : 12.0D; AdaptiveTargetingUtils.nearbyHostiles(enderman, radius, - other -> other instanceof EnderMan && (other.getTarget() == null || other.getTarget() == target)) + other -> other instanceof EnderMan + && (other.getTarget() == null || other.getTarget() == target) + && other.canAttack(target)) .forEach(other -> other.setTarget(target)); AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman coordinated target"); } + if (tier >= 4 && AMConfig.ENABLE_ANTI_CHEESE_AI.get() && isToweredTarget(target)) { + cooldown = ANTI_PILLAR_BUILD_COOLDOWN_MIN + enderman.getRandom().nextInt(ANTI_PILLAR_BUILD_COOLDOWN_RANDOM); + if (tryPlaceMobStep(target, inEnd)) { + return; + } + } if (tier >= 3 || waterNearby || strongMelee) { Vec3 side = AdaptivePositioningUtils.sidePosition(enderman, target, strongMelee ? 8.0D : (inEnd && tier >= 5 ? 5.0D : 3.0D), enderman.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D); @@ -71,4 +106,112 @@ public class AdaptiveEndermanTacticsGoal extends Goal { } } } + + private boolean isToweredTarget(LivingEntity target) { + double vertical = target.getY() - enderman.getY(); + double horizontal = Math.sqrt((target.getX() - enderman.getX()) * (target.getX() - enderman.getX()) + + (target.getZ() - enderman.getZ()) * (target.getZ() - enderman.getZ())); + return vertical > 2.5D && vertical <= 16.0D && horizontal < 9.0D; + } + + private boolean tryPlaceMobStep(LivingEntity target, boolean inEnd) { + if (!(enderman.level() instanceof ServerLevel level)) { + return false; + } + BlockState scaffoldState = randomScaffoldState(inEnd); + prepareScaffoldBlock(level, scaffoldState); + BlockPos targetFeet = BlockPos.containing(target.getX(), target.getY(), target.getZ()); + if (!targetFeet.equals(rampTargetFeet)) { + rampTargetFeet = targetFeet; + rampApproach = sharedRampDirection(targetFeet); + rampRise = Math.min(8, Math.max(1, (int) Math.floor(target.getY() - enderman.getY()))); + rampBaseY = targetFeet.getY() - rampRise; + } + for (int step = 0; step < rampRise; step++) { + int distance = Math.max(1, rampRise - step); + BlockPos pos = new BlockPos(targetFeet.getX(), rampBaseY + step, targetFeet.getZ()).relative(rampApproach, distance); + if (canPlaceRampStep(level, pos, rampApproach, step == 0)) { + BlockState carried = enderman.getCarriedBlock(); + level.setBlock(pos, isUsefulScaffoldBlock(carried) ? carried : scaffoldState, 3); + enderman.setCarriedBlock(null); + enderman.randomTeleport(pos.getX() + 0.5D, pos.getY() + 1.0D, pos.getZ() + 0.5D, true); + AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman placed anti-pillar scaffold"); + return true; + } + } + return false; + } + + private BlockState randomScaffoldState(boolean inEnd) { + BlockState[] options = inEnd ? END_SCAFFOLD_BLOCKS : OVERWORLD_SCAFFOLD_BLOCKS; + return options[enderman.getRandom().nextInt(options.length)]; + } + + private void prepareScaffoldBlock(ServerLevel level, BlockState scaffoldState) { + BlockState carried = enderman.getCarriedBlock(); + if (isUsefulScaffoldBlock(carried)) { + return; + } + if (carried != null) { + if (!tryPlaceDiscardedBlock(level, carried)) { + enderman.spawnAtLocation(new ItemStack(carried.getBlock())); + } + } + enderman.setCarriedBlock(scaffoldState); + AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman swapped carried block for scaffold"); + } + + private boolean isUsefulScaffoldBlock(BlockState state) { + return state != null && !state.liquid() + && !state.getCollisionShape(enderman.level(), enderman.blockPosition()).isEmpty(); + } + + private boolean tryPlaceDiscardedBlock(ServerLevel level, BlockState carried) { + BlockPos origin = enderman.blockPosition(); + BlockPos[] candidates = { + origin, + origin.relative(Direction.NORTH), + origin.relative(Direction.EAST), + origin.relative(Direction.SOUTH), + origin.relative(Direction.WEST) + }; + for (BlockPos pos : candidates) { + if (canPlaceDiscardedBlock(level, carried, pos)) { + level.setBlock(pos, carried, 3); + return true; + } + } + return false; + } + + private boolean canPlaceDiscardedBlock(ServerLevel level, BlockState carried, BlockPos pos) { + return level.getWorldBorder().isWithinBounds(pos) + && level.getBlockState(pos).isAir() + && !level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty() + && carried.canSurvive(level, pos); + } + + private Direction sharedRampDirection(BlockPos targetFeet) { + Direction[] directions = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + int index = Math.floorMod(targetFeet.getX() * 31 + targetFeet.getZ() * 17, directions.length); + return directions[index]; + } + + private boolean canPlaceRampStep(ServerLevel level, BlockPos pos, Direction approach, boolean firstStep) { + return level.getWorldBorder().isWithinBounds(pos) + && level.getBlockState(pos).isAir() + && !level.getFluidState(pos).is(FluidTags.WATER) + && !level.getFluidState(pos).is(FluidTags.LAVA) + && (firstStep ? hasGroundSupport(level, pos) : hasPreviousRampStep(level, pos, approach)); + } + + private boolean hasPreviousRampStep(ServerLevel level, BlockPos pos, Direction approach) { + BlockPos previousStep = pos.relative(approach).below(); + return !level.getBlockState(previousStep).getCollisionShape(level, previousStep).isEmpty(); + } + + private boolean hasGroundSupport(ServerLevel level, BlockPos pos) { + BlockPos below = pos.below(); + return !level.getBlockState(below).getCollisionShape(level, below).isEmpty(); + } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermiteTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermiteTacticsGoal.java index 3d4f0de..a42f3a9 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermiteTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermiteTacticsGoal.java @@ -38,7 +38,7 @@ public class AdaptiveEndermiteTacticsGoal extends Goal { public void start() { cooldown = 18 + mob.getRandom().nextInt(28); LivingEntity target = currentTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } rememberedTarget = target; @@ -61,10 +61,12 @@ public class AdaptiveEndermiteTacticsGoal extends Goal { private LivingEntity currentTarget() { LivingEntity target = mob.getTarget(); - if (target != null && target.isAlive()) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return target; } - if (rememberedTarget != null && rememberedTarget.isAlive() && mob.distanceToSqr(rememberedTarget) < 16.0D * 16.0D) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(rememberedTarget) + && mob.distanceToSqr(rememberedTarget) < 16.0D * 16.0D + && mob.canAttack(rememberedTarget)) { return rememberedTarget; } rememberedTarget = null; diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveGuardianTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveGuardianTacticsGoal.java index 942c729..3aa5324 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveGuardianTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveGuardianTacticsGoal.java @@ -32,8 +32,7 @@ public class AdaptiveGuardianTacticsGoal extends Goal { && AMConfig.ENABLE_GUARDIAN_ADVANCED_AI.get() && AMConfig.AI_GUARDIAN.get() && tierSupplier.getAsInt() >= 2 - && guardian.getTarget() != null - && guardian.getTarget().isAlive() + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(guardian.getTarget()) && --cooldown <= 0; } @@ -42,7 +41,7 @@ public class AdaptiveGuardianTacticsGoal extends Goal { boolean elder = guardian instanceof ElderGuardian; cooldown = elder ? 90 + guardian.getRandom().nextInt(80) : AdaptiveAIGoalUtils.nextCooldown(guardian); LivingEntity target = guardian.getTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); @@ -62,7 +61,7 @@ public class AdaptiveGuardianTacticsGoal extends Goal { private void retargetLowHealthGuardianTarget(LivingEntity current) { List candidates = guardian.level().getEntitiesOfClass(LivingEntity.class, guardian.getBoundingBox().inflate(12.0D), - entity -> entity != guardian && entity.isAlive() && guardian.canAttack(entity) + entity -> entity != guardian && AdaptiveAIGoalUtils.isValidAdaptiveTarget(entity) && guardian.canAttack(entity) && guardian.hasLineOfSight(entity) && AdaptiveAIGoalUtils.healthFraction(entity) < AdaptiveAIGoalUtils.healthFraction(current)); if (!candidates.isEmpty()) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveRavagerTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveRavagerTacticsGoal.java index 4d54c7c..1084c78 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveRavagerTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveRavagerTacticsGoal.java @@ -40,7 +40,7 @@ public class AdaptiveRavagerTacticsGoal extends Goal { public void start() { cooldown = 55 + ravager.getRandom().nextInt(65); LivingEntity target = currentTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } rememberedTarget = target; @@ -65,10 +65,12 @@ public class AdaptiveRavagerTacticsGoal extends Goal { private LivingEntity currentTarget() { LivingEntity target = ravager.getTarget(); - if (target != null && target.isAlive()) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return target; } - if (rememberedTarget != null && rememberedTarget.isAlive() && ravager.distanceToSqr(rememberedTarget) < 20.0D * 20.0D) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(rememberedTarget) + && ravager.distanceToSqr(rememberedTarget) < 20.0D * 20.0D + && ravager.canAttack(rememberedTarget)) { return rememberedTarget; } rememberedTarget = null; diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSharedTargetGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSharedTargetGoal.java index 19720f5..5d03cfb 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSharedTargetGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSharedTargetGoal.java @@ -28,17 +28,19 @@ public class AdaptiveSharedTargetGoal extends Goal { @Override public boolean canUse() { - return tierSupplier.getAsInt() >= minTier && mob.getTarget() != null && --cooldown <= 0; + return tierSupplier.getAsInt() >= minTier && AdaptiveAIGoalUtils.isValidAdaptiveTarget(mob.getTarget()) + && --cooldown <= 0; } @Override public void start() { cooldown = AdaptiveAIGoalUtils.nextCooldown(mob); LivingEntity target = mob.getTarget(); - if (target == null || !target.isAlive()) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } - List allies = AdaptiveTargetingUtils.nearbyHostiles(mob, radius, other -> other.getTarget() == null); + List allies = AdaptiveTargetingUtils.nearbyHostiles(mob, radius, + other -> other.getTarget() == null && other.canAttack(target)); int shared = 0; for (Mob ally : allies) { if (shared++ >= 4) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveShulkerTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveShulkerTacticsGoal.java index 85e9b0a..4eefebb 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveShulkerTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveShulkerTacticsGoal.java @@ -30,8 +30,7 @@ public class AdaptiveShulkerTacticsGoal extends Goal { && AMConfig.ENABLE_SHULKER_ADVANCED_AI.get() && AMConfig.AI_SHULKER.get() && tierSupplier.getAsInt() >= 2 - && shulker.getTarget() != null - && shulker.getTarget().isAlive() + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(shulker.getTarget()) && --cooldown <= 0; } @@ -42,7 +41,7 @@ public class AdaptiveShulkerTacticsGoal extends Goal { cooldown = inEnd && tier >= 5 ? 30 + shulker.getRandom().nextInt(35) : 50 + shulker.getRandom().nextInt(tier >= 5 ? 45 : 70); LivingEntity target = shulker.getTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } if (!shulker.hasLineOfSight(target)) { @@ -58,7 +57,9 @@ public class AdaptiveShulkerTacticsGoal extends Goal { if (tier >= 4) { double radius = inEnd && tier >= 5 ? 18.0D : 12.0D; AdaptiveTargetingUtils.nearbyHostiles(shulker, radius, - other -> other instanceof Shulker && (other.getTarget() == null || other.getTarget() == target)) + other -> other instanceof Shulker + && (other.getTarget() == null || other.getTarget() == target) + && other.canAttack(target)) .forEach(other -> other.setTarget(target)); AdaptiveAIGoalUtils.debug(shulker, tierSupplier, "shulker coordinated target"); } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSpiderTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSpiderTacticsGoal.java index 146fe39..66ea27d 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSpiderTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSpiderTacticsGoal.java @@ -2,6 +2,7 @@ package net.fretux.adaptivemobs.ai.goals; import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils; +import net.minecraft.core.BlockPos; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.PathfinderMob; import net.minecraft.world.entity.ai.goal.Goal; @@ -16,6 +17,8 @@ public class AdaptiveSpiderTacticsGoal extends Goal { private final IntSupplier tierSupplier; private final boolean caveSpider; private int cooldown; + private LivingEntity climbTarget; + private int climbTicks; public AdaptiveSpiderTacticsGoal(PathfinderMob mob, IntSupplier tierSupplier, boolean caveSpider) { this.mob = mob; @@ -28,18 +31,40 @@ public class AdaptiveSpiderTacticsGoal extends Goal { public boolean canUse() { int tier = tierSupplier.getAsInt(); int min = caveSpider ? 2 : 3; - return tier >= min && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0; + return tier >= min && AdaptiveAIGoalUtils.isValidAdaptiveTarget(mob.getTarget()) && --cooldown <= 0; + } + + @Override + public boolean canContinueToUse() { + return climbTicks > 0 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(climbTarget) && isPillaring(climbTarget); + } + + @Override + public void tick() { + if (climbTarget == null || climbTicks-- <= 0) { + return; + } + climbPillar(climbTarget); } @Override public void start() { cooldown = caveSpider ? 30 + mob.getRandom().nextInt(40) : AdaptiveAIGoalUtils.nextCooldown(mob); LivingEntity target = mob.getTarget(); - if (target == null) { + climbTarget = null; + climbTicks = 0; + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); double distance = Math.sqrt(mob.distanceToSqr(target)); + if (tier >= 3 && isPillaring(target)) { + climbTarget = target; + climbTicks = caveSpider ? 45 : 35; + climbPillar(target); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "spider pillar climb"); + return; + } if (tier >= 4) { Vec3 elevated = AdaptivePositioningUtils.elevatedPosition(mob, target, 3.0D); mob.getNavigation().moveTo(elevated.x, elevated.y, elevated.z, caveSpider ? 1.2D : 1.1D); @@ -54,4 +79,37 @@ public class AdaptiveSpiderTacticsGoal extends Goal { AdaptiveAIGoalUtils.debug(mob, tierSupplier, "spider lunge"); } } + + private boolean isPillaring(LivingEntity target) { + double vertical = target.getY() - mob.getY(); + double horizontal = Math.sqrt((target.getX() - mob.getX()) * (target.getX() - mob.getX()) + + (target.getZ() - mob.getZ()) * (target.getZ() - mob.getZ())); + return vertical > 2.2D && vertical <= 10.0D && horizontal < 6.5D; + } + + private void climbPillar(LivingEntity target) { + Vec3 side = pillarSidePosition(target); + mob.getLookControl().setLookAt(target, 35.0F, 35.0F); + mob.getNavigation().moveTo(side.x, mob.getY(), side.z, caveSpider ? 1.25D : 1.15D); + + double horizontal = Math.sqrt((side.x - mob.getX()) * (side.x - mob.getX()) + (side.z - mob.getZ()) * (side.z - mob.getZ())); + if (horizontal < 1.4D || mob.horizontalCollision) { + Vec3 toward = new Vec3(target.getX() - mob.getX(), 0.0D, target.getZ() - mob.getZ()); + double len = Math.max(0.001D, Math.sqrt(toward.x * toward.x + toward.z * toward.z)); + mob.setDeltaMovement(mob.getDeltaMovement().add(toward.x / len * 0.08D, caveSpider ? 0.26D : 0.22D, + toward.z / len * 0.08D)); + mob.hurtMarked = true; + } + } + + private Vec3 pillarSidePosition(LivingEntity target) { + BlockPos targetBase = BlockPos.containing(target.getX(), mob.getY(), target.getZ()); + Vec3 away = mob.position().subtract(Vec3.atCenterOf(targetBase)); + if (Math.abs(away.x) > Math.abs(away.z)) { + targetBase = targetBase.offset(away.x >= 0.0D ? 1 : -1, 0, 0); + } else { + targetBase = targetBase.offset(0, 0, away.z >= 0.0D ? 1 : -1); + } + return Vec3.atCenterOf(targetBase); + } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetPriorityGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetPriorityGoal.java index 25c1411..3103eb3 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetPriorityGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetPriorityGoal.java @@ -34,8 +34,8 @@ public class AdaptiveTargetPriorityGoal extends Goal { cooldown = 80 + mob.getRandom().nextInt(80); double radius = AdaptiveAIGoalUtils.followRange(mob, 16.0D); List players = mob.level().getEntitiesOfClass(Player.class, mob.getBoundingBox().inflate(radius), - player -> player.isAlive() && !player.isCreative() && !player.isSpectator() - && mob.canAttack(player) && mob.hasLineOfSight(player)); + player -> AdaptiveAIGoalUtils.isValidAdaptiveTarget(player) && mob.canAttack(player) + && mob.hasLineOfSight(player)); Player best = null; double bestScore = currentScore(); for (Player player : players) { @@ -52,7 +52,7 @@ public class AdaptiveTargetPriorityGoal extends Goal { } private double currentScore() { - if (mob.getTarget() instanceof Player player) { + if (mob.getTarget() instanceof Player player && AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { return AdaptiveAIGoalUtils.playerPriorityScore(mob, player); } return 0.0D; diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetSanitizerGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetSanitizerGoal.java new file mode 100644 index 0000000..74132fa --- /dev/null +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveTargetSanitizerGoal.java @@ -0,0 +1,66 @@ +package net.fretux.adaptivemobs.ai.goals; + +import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.monster.Zombie; + +import java.util.EnumSet; + +public class AdaptiveTargetSanitizerGoal extends Goal { + + private final Mob mob; + + public AdaptiveTargetSanitizerGoal(Mob mob) { + this.mob = mob; + setFlags(EnumSet.noneOf(Flag.class)); + } + + @Override + public boolean canUse() { + LivingEntity target = mob.getTarget(); + return (target != null && !AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) || shouldReleaseZombieStack(target); + } + + @Override + public void start() { + LivingEntity target = mob.getTarget(); + if (target != null && !AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { + mob.setTarget(null); + } + if (shouldReleaseZombieStack(target)) { + mob.stopRiding(); + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + } + } + + private boolean shouldReleaseZombieStack(LivingEntity target) { + if (!(mob instanceof Zombie) || !(mob.getVehicle() instanceof Zombie)) { + return false; + } + if (!AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) { + return true; + } + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { + return true; + } + if (mob.distanceToSqr(target) > 8.0D * 8.0D) { + return true; + } + Entity base = stackBase(mob); + double verticalFromBase = target.getY() - base.getY(); + double horizontalFromBase = Math.sqrt((target.getX() - base.getX()) * (target.getX() - base.getX()) + + (target.getZ() - base.getZ()) * (target.getZ() - base.getZ())); + return verticalFromBase <= 2.5D || horizontalFromBase >= 5.5D; + } + + private Entity stackBase(Entity entity) { + Entity cursor = entity; + while (cursor.getVehicle() instanceof Mob) { + cursor = cursor.getVehicle(); + } + return cursor; + } +} diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveThreatMemoryGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveThreatMemoryGoal.java index a49d6a8..6de15d1 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveThreatMemoryGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveThreatMemoryGoal.java @@ -35,6 +35,9 @@ public class AdaptiveThreatMemoryGoal extends Goal { return; } Player player = remembered.get(); + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(player) || !mob.canAttack(player)) { + return; + } if (mob.getTarget() == null || !mob.getTarget().isAlive()) { mob.setTarget(player); } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveWardenTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveWardenTacticsGoal.java index c8ca15b..e134abd 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveWardenTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveWardenTacticsGoal.java @@ -37,18 +37,21 @@ public class AdaptiveWardenTacticsGoal extends Goal { public void start() { cooldown = 80 + warden.getRandom().nextInt(80); LivingEntity target = warden.getTarget(); - if (target != null && target.isAlive()) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { rememberedTarget = target; warden.getLookControl().setLookAt(target, 25.0F, 25.0F); return; } - if (rememberedTarget != null && rememberedTarget.isAlive() && warden.distanceToSqr(rememberedTarget) < 24.0D * 24.0D) { + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(rememberedTarget) + && warden.distanceToSqr(rememberedTarget) < 24.0D * 24.0D + && warden.canAttack(rememberedTarget)) { warden.setAttackTarget(rememberedTarget); AdaptiveAIGoalUtils.debug(warden, tierSupplier, "warden conservative target persistence"); } LivingEntity hurtBy = warden.getLastHurtByMob(); - if (tierSupplier.getAsInt() >= 5 && hurtBy != null && hurtBy.isAlive() && AdaptiveAIGoalUtils.isUsingRangedWeapon(hurtBy)) { + if (tierSupplier.getAsInt() >= 5 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(hurtBy) + && warden.canAttack(hurtBy) && AdaptiveAIGoalUtils.isUsingRangedWeapon(hurtBy)) { repeatedRangedTicks = hurtBy == lastHurtBy ? repeatedRangedTicks + 1 : 1; lastHurtBy = hurtBy; if (repeatedRangedTicks >= 2) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/SharedTargetGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/SharedTargetGoal.java index 1f0e1b2..51593cd 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/SharedTargetGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/SharedTargetGoal.java @@ -1,5 +1,6 @@ package net.fretux.adaptivemobs.ai.goals; +import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.goal.Goal; @@ -28,19 +29,20 @@ public class SharedTargetGoal extends Goal { @Override public boolean canUse() { - return tierSupplier.getAsInt() >= minTier && mob.getTarget() != null && --cooldown <= 0; + return tierSupplier.getAsInt() >= minTier && AdaptiveAIGoalUtils.isValidAdaptiveTarget(mob.getTarget()) + && --cooldown <= 0; } @Override public void start() { cooldown = 40 + mob.getRandom().nextInt(40); LivingEntity target = mob.getTarget(); - if (target == null || !target.isAlive()) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } AABB box = mob.getBoundingBox().inflate(radius); List allies = mob.level().getEntitiesOfClass(Mob.class, box, - other -> other != mob && other instanceof Enemy && other.getTarget() == null); + other -> other != mob && other instanceof Enemy && other.getTarget() == null && other.canAttack(target)); int shared = 0; for (Mob ally : allies) { if (shared++ >= 4) { diff --git a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java index 63709c2..7a040c8 100644 --- a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java +++ b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java @@ -1,6 +1,7 @@ package net.fretux.adaptivemobs.event; import net.fretux.adaptivemobs.ai.AdaptiveGoalInjector; +import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; import net.fretux.adaptivemobs.ai.AdaptiveMemory; import net.fretux.adaptivemobs.ai.AdaptiveMobHooks; import net.fretux.adaptivemobs.ai.AdaptiveSpecialAbilities; @@ -61,6 +62,10 @@ public class AdaptiveMobsEvents { public void onBlockBreak(BlockEvent.BreakEvent event) { if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get() && event.getLevel() instanceof ServerLevel level) { + if (event.getPlayer() instanceof ServerPlayer player + && !AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { + return; + } AdaptiveMemory.rememberSound(level, event.getPos(), AdaptiveMemory.SoundKind.BLOCK_BREAK, 20 * 8); } } @@ -68,7 +73,8 @@ public class AdaptiveMobsEvents { @SubscribeEvent public void onItemUseFinish(LivingEntityUseItemEvent.Finish event) { if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get() - && event.getEntity() instanceof ServerPlayer player && event.getItem().isEdible()) { + && event.getEntity() instanceof ServerPlayer player && event.getItem().isEdible() + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { AdaptiveMemory.rememberSound(player.serverLevel(), player.blockPosition(), AdaptiveMemory.SoundKind.EATING, 20 * 6); } } @@ -102,7 +108,8 @@ public class AdaptiveMobsEvents { AdaptiveGoalInjector.inject(mob, level, AdaptiveDifficultyManager.getMobTier(level, mob.getType())); } if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get() - && entity instanceof Projectile projectile && projectile.getOwner() instanceof ServerPlayer player) { + && entity instanceof Projectile projectile && projectile.getOwner() instanceof ServerPlayer player + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { AdaptiveMemory.rememberSound(level, player.blockPosition(), AdaptiveMemory.SoundKind.BOW_SHOT, 20 * 6); } } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index c01954b..e665299 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -3,3 +3,6 @@ # without resorting to mixins. public net.minecraft.world.entity.Mob f_21345_ # goalSelector public net.minecraft.world.entity.Mob f_21346_ # targetSelector + +# Allow adaptive tiers to shorten the Creeper's vanilla 30-tick fuse. +public net.minecraft.world.entity.monster.Creeper f_32271_ # maxSwell