implement many major AI changes.

This commit is contained in:
2026-07-19 20:48:06 +02:00
parent b732993135
commit da6ead8dab
22 changed files with 799 additions and 117 deletions
@@ -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)
@@ -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;
}
@@ -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) {
@@ -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());
@@ -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
@@ -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();
@@ -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<Entity> vehicles = mob.level().getEntitiesOfClass(Entity.class, mob.getBoundingBox().inflate(2.5D),
List<Entity> 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<Entity> 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<Zombie> 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;
}
}
@@ -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<Entity> 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;
}
}
@@ -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();
}
}
@@ -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;
@@ -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<LivingEntity> 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()) {
@@ -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;
@@ -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<Mob> allies = AdaptiveTargetingUtils.nearbyHostiles(mob, radius, other -> other.getTarget() == null);
List<Mob> allies = AdaptiveTargetingUtils.nearbyHostiles(mob, radius,
other -> other.getTarget() == null && other.canAttack(target));
int shared = 0;
for (Mob ally : allies) {
if (shared++ >= 4) {
@@ -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");
}
@@ -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);
}
}
@@ -34,8 +34,8 @@ public class AdaptiveTargetPriorityGoal extends Goal {
cooldown = 80 + mob.getRandom().nextInt(80);
double radius = AdaptiveAIGoalUtils.followRange(mob, 16.0D);
List<Player> 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;
@@ -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;
}
}
@@ -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);
}
@@ -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) {
@@ -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<Mob> 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) {
@@ -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);
}
}
@@ -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