package net.fretux.adaptivemobs.ai.goals; import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils; 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; 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; this.tierSupplier = tierSupplier; setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); } @Override public boolean canUse() { return AMConfig.ENABLE_ENDERMAN_AI.get() && tierSupplier.getAsInt() >= 2 && AdaptiveAIGoalUtils.isValidAdaptiveTarget(enderman.getTarget()) && --cooldown <= 0; } @Override public void start() { LivingEntity target = enderman.getTarget(); if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } int tier = tierSupplier.getAsInt(); boolean inEnd = enderman.level().dimension() == Level.END; cooldown = inEnd && tier >= 5 ? 35 + enderman.getRandom().nextInt(45) : 60 + enderman.getRandom().nextInt(80); double dist = Math.sqrt(enderman.distanceToSqr(target)); boolean waterNearby = enderman.level().getFluidState(enderman.blockPosition()).is(FluidTags.WATER); boolean strongMelee = tier >= 5 && dist < 4.0D && !AdaptiveAIGoalUtils.isUsingRangedWeapon(target); 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.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); int attempts = inEnd && tier >= 5 ? 6 : 4; for (int i = 0; i < attempts; i++) { Vec3 candidate = side.add(enderman.getRandom().nextInt(5) - 2, 0.0D, enderman.getRandom().nextInt(5) - 2); BlockPos pos = BlockPos.containing(candidate); if (!enderman.level().getFluidState(pos).is(FluidTags.WATER) && AdaptivePositioningUtils.isReasonableGround(enderman, candidate) && enderman.randomTeleport(candidate.x, candidate.y, candidate.z, true)) { AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman angular teleport"); return; } } } } 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(); } }