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 6a32c4f..77c0aa9 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java @@ -12,9 +12,12 @@ 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.level.Level; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.event.ForgeEventFactory; import java.util.EnumSet; @@ -24,6 +27,9 @@ 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 float MAX_MOVABLE_HARDNESS = 5.0F; + private static final int BLOCK_SEARCH_RADIUS = 4; + private static final double BUILD_REACH_SQR = 3.5D * 3.5D; private final EnderMan enderman; private final IntSupplier tierSupplier; private int cooldown; @@ -31,6 +37,8 @@ public class AdaptiveEndermanTacticsGoal extends Goal { private Direction rampApproach; private int rampRise; private int rampBaseY; + private LivingEntity activeRampTarget; + private int activeRampTicks; public AdaptiveEndermanTacticsGoal(EnderMan enderman, IntSupplier tierSupplier) { this.enderman = enderman; @@ -68,11 +76,22 @@ public class AdaptiveEndermanTacticsGoal extends Goal { .forEach(other -> other.setTarget(target)); AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman coordinated target"); } - if (tier >= 4 && AMConfig.ENABLE_ANTI_CHEESE_AI.get() && isToweredTarget(target)) { + boolean toweredTarget = tier >= 4 && AMConfig.ENABLE_ANTI_CHEESE_AI.get() && isToweredTarget(target); + if (toweredTarget) { cooldown = ANTI_PILLAR_BUILD_COOLDOWN_MIN + enderman.getRandom().nextInt(ANTI_PILLAR_BUILD_COOLDOWN_RANDOM); - if (tryPlaceMobStep(target, inEnd)) { - return; - } + activeRampTarget = target; + activeRampTicks = 160; + // Always plan a new segment from the Enderman's current height. This matters when + // it has just climbed a completed segment but still cannot reach the player. + rampTargetFeet = null; + tryPlaceMobStep(target, inEnd); + // A tower is not a low-ceiling shelter. Never fall through into shelter dismantling + // when a ramp is temporarily blocked or already complete. + return; + } + if (tier >= 4 && AMConfig.ENABLE_ANTI_CHEESE_AI.get() && tryDismantleLowShelter(target)) { + cooldown = 6 + enderman.getRandom().nextInt(6); + return; } if (tier >= 3 || waterNearby || strongMelee) { Vec3 side = AdaptivePositioningUtils.sidePosition(enderman, target, strongMelee ? 8.0D : (inEnd && tier >= 5 ? 5.0D : 3.0D), @@ -91,6 +110,49 @@ public class AdaptiveEndermanTacticsGoal extends Goal { } } + @Override + public boolean canContinueToUse() { + return activeRampTicks > 0 + && AMConfig.AI_ENABLED.get() && AMConfig.ENABLE_ADVANCED_AI.get() + && AMConfig.ENABLE_ANTI_CHEESE_AI.get() + && tierSupplier.getAsInt() >= 4 + && AdaptiveAIGoalUtils.isValidAdaptiveTarget(activeRampTarget) + && activeRampTarget.level() == enderman.level() + && enderman.distanceToSqr(activeRampTarget) <= 16.0D * 16.0D; + } + + @Override + public void tick() { + if (activeRampTicks-- <= 0 || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(activeRampTarget)) { + return; + } + enderman.getLookControl().setLookAt(activeRampTarget, 30.0F, 30.0F); + if (isCurrentRampComplete()) { + if (isToweredTarget(activeRampTarget)) { + // The blocks exist but the Enderman has not successfully climbed them yet. + // Keep approaching the top instead of starting another identical ramp plan. + if (enderman.level() instanceof ServerLevel level) { + approachHighestRampStep(level, rampTargetFeet); + } + return; + } else { + // The staircase has done its job. Release MOVE promptly so melee AI can attack + // instead of holding an idle construction goal for the remainder of the timer. + activeRampTicks = 0; + cooldown = 5; + enderman.getNavigation().moveTo(activeRampTarget, 1.2D); + return; + } + } + tryPlaceMobStep(activeRampTarget, enderman.level().dimension() == Level.END); + } + + @Override + public void stop() { + activeRampTarget = null; + activeRampTicks = 0; + } + private boolean isToweredTarget(LivingEntity target) { double vertical = target.getY() - enderman.getY(); double horizontal = Math.sqrt((target.getX() - enderman.getX()) * (target.getX() - enderman.getX()) @@ -107,12 +169,12 @@ public class AdaptiveEndermanTacticsGoal extends Goal { } BlockState scaffoldState = enderman.getCarriedBlock(); if (!isUsefulScaffoldBlock(scaffoldState)) { - return false; + return tryAcquireNearbyBlock(level, target); } BlockPos targetFeet = BlockPos.containing(target.getX(), target.getY(), target.getZ()); if (!targetFeet.equals(rampTargetFeet)) { rampTargetFeet = targetFeet; - rampApproach = sharedRampDirection(targetFeet); + rampApproach = nearestRampDirection(targetFeet); rampRise = Math.min(8, Math.max(1, (int) Math.floor(target.getY() - enderman.getY()))); rampBaseY = targetFeet.getY() - rampRise; } @@ -120,25 +182,208 @@ public class AdaptiveEndermanTacticsGoal extends Goal { 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)) { + if (enderman.distanceToSqr(Vec3.atCenterOf(pos)) > BUILD_REACH_SQR) { + moveToBuildPosition(pos); + return true; + } + BlockSnapshot replaced = BlockSnapshot.create(level.dimension(), level, pos); + if (ForgeEventFactory.onBlockPlace(enderman, replaced, Direction.UP)) { + continue; + } level.setBlock(pos, scaffoldState, 3); + level.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(enderman, scaffoldState)); 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 approachHighestRampStep(level, targetFeet); + } + + private boolean approachHighestRampStep(ServerLevel level, BlockPos targetFeet) { + for (int distance = 1; distance <= rampRise; distance++) { + BlockPos step = new BlockPos(targetFeet.getX(), targetFeet.getY() - distance, targetFeet.getZ()) + .relative(rampApproach, distance); + if (!level.getBlockState(step).getCollisionShape(level, step).isEmpty()) { + moveToBuildPosition(step); + return true; + } + } return false; } + private boolean isCurrentRampComplete() { + if (!(enderman.level() instanceof ServerLevel level) || rampTargetFeet == null + || rampApproach == null || rampRise <= 0) { + return false; + } + for (int distance = 1; distance <= rampRise; distance++) { + BlockPos step = new BlockPos(rampTargetFeet.getX(), rampTargetFeet.getY() - distance, + rampTargetFeet.getZ()).relative(rampApproach, distance); + if (level.getBlockState(step).isAir()) { + return false; + } + } + return true; + } + + private void moveToBuildPosition(BlockPos step) { + BlockPos standing = step.relative(rampApproach); + Vec3 destination = Vec3.atBottomCenterOf(standing); + if (!AdaptivePositioningUtils.isReasonableGround(enderman, destination)) { + destination = Vec3.atBottomCenterOf(step); + } + enderman.getNavigation().moveTo(destination.x, destination.y, destination.z, 1.18D); + AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman approaching staircase work area"); + } + private boolean isUsefulScaffoldBlock(BlockState state) { return state != null && !state.liquid() && !state.getCollisionShape(enderman.level(), enderman.blockPosition()).isEmpty(); } - 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 tryAcquireNearbyBlock(ServerLevel level, LivingEntity target) { + BlockPos origin = enderman.blockPosition(); + BlockPos best = null; + double bestDistance = Double.MAX_VALUE; + for (int y = -2; y <= 2; y++) { + for (int x = -BLOCK_SEARCH_RADIUS; x <= BLOCK_SEARCH_RADIUS; x++) { + for (int z = -BLOCK_SEARCH_RADIUS; z <= BLOCK_SEARCH_RADIUS; z++) { + BlockPos candidate = origin.offset(x, y, z); + if (Math.abs(candidate.getX() - origin.getX()) <= 1 + && Math.abs(candidate.getZ() - origin.getZ()) <= 1 + && candidate.getY() <= origin.getY()) { + continue; + } + if (Math.abs(candidate.getX() - target.getBlockX()) <= 1 + && Math.abs(candidate.getZ() - target.getBlockZ()) <= 1) { + continue; + } + if (isReservedRampBlock(candidate, target.blockPosition())) { + continue; + } + if (!isMovableBlock(level, candidate)) { + continue; + } + double distance = candidate.distSqr(origin); + if (distance < bestDistance) { + best = candidate.immutable(); + bestDistance = distance; + } + } + } + } + if (best == null) { + return false; + } + return pickUpBlock(level, best, "enderman acquired scaffold block"); + } + + private boolean isReservedRampBlock(BlockPos candidate, BlockPos targetFeet) { + int dx = candidate.getX() - targetFeet.getX(); + int dz = candidate.getZ() - targetFeet.getZ(); + int distance = Math.abs(dx) + Math.abs(dz); + boolean cardinal = (dx == 0) != (dz == 0); + return cardinal && distance >= 1 && distance <= 8 + && candidate.getY() == targetFeet.getY() - distance; + } + + private boolean tryDismantleLowShelter(LivingEntity target) { + if (!(enderman.level() instanceof ServerLevel level) + || !ForgeEventFactory.getMobGriefingEvent(level, enderman) + || enderman.distanceToSqr(target) > 8.0D * 8.0D) { + return false; + } + BlockPos feet = target.blockPosition(); + // A player standing in a two-block-high opening has a solid ceiling two blocks above + // their feet. Check that ceiling and its four neighbours so Endermen can open a route + // without indiscriminately tearing through the rest of the structure. + Direction[] offsets = {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + for (Direction offset : offsets) { + BlockPos candidate = offset == Direction.DOWN ? feet.above(2) : feet.above(2).relative(offset); + if (!isMovableBlock(level, candidate)) { + continue; + } + if (enderman.getCarriedBlock() != null && !tryDepositCarriedBlock(level)) { + return false; + } + if (pickUpBlock(level, candidate, "enderman dismantled low shelter")) { + enderman.randomTeleport(candidate.getX() + 0.5D, feet.getY(), candidate.getZ() + 0.5D, true); + return true; + } + } + return false; + } + + private boolean tryDepositCarriedBlock(ServerLevel level) { + BlockState carried = enderman.getCarriedBlock(); + if (!isUsefulScaffoldBlock(carried)) { + enderman.setCarriedBlock(null); + return true; + } + BlockPos origin = enderman.blockPosition(); + for (Direction direction : Direction.Plane.HORIZONTAL) { + BlockPos pos = origin.relative(direction, 2); + if (!canPlaceRampStep(level, pos, direction.getOpposite(), true)) { + continue; + } + BlockSnapshot replaced = BlockSnapshot.create(level.dimension(), level, pos); + if (ForgeEventFactory.onBlockPlace(enderman, replaced, Direction.UP)) { + continue; + } + level.setBlock(pos, carried, 3); + level.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(enderman, carried)); + enderman.setCarriedBlock(null); + return true; + } + return false; + } + + private boolean pickUpBlock(ServerLevel level, BlockPos pos, String debugMessage) { + if (enderman.getCarriedBlock() != null || !isMovableBlock(level, pos)) { + return false; + } + BlockState state = level.getBlockState(pos); + if (!level.removeBlock(pos, false)) { + return false; + } + level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(enderman, state)); + enderman.setCarriedBlock(state); + AdaptiveAIGoalUtils.debug(enderman, tierSupplier, debugMessage); + return true; + } + + private boolean isMovableBlock(ServerLevel level, BlockPos pos) { + if (!level.hasChunkAt(pos) || !level.getWorldBorder().isWithinBounds(pos) + || level.getBlockEntity(pos) != null) { + return false; + } + BlockState state = level.getBlockState(pos); + float hardness = state.getDestroySpeed(level, pos); + return !state.isAir() && !state.liquid() + && hardness >= 0.0F && hardness <= MAX_MOVABLE_HARDNESS + && state.isCollisionShapeFullBlock(level, pos) + && !isProtectedBlock(state.getBlock()); + } + + private boolean isProtectedBlock(Block block) { + return block == Blocks.BEDROCK || block == Blocks.OBSIDIAN || block == Blocks.CRYING_OBSIDIAN + || block == Blocks.NETHERITE_BLOCK || block == Blocks.REINFORCED_DEEPSLATE + || block == Blocks.END_PORTAL_FRAME || block == Blocks.END_PORTAL + || block == Blocks.NETHER_PORTAL || block == Blocks.BARRIER + || block == Blocks.COMMAND_BLOCK || block == Blocks.CHAIN_COMMAND_BLOCK + || block == Blocks.REPEATING_COMMAND_BLOCK || block == Blocks.STRUCTURE_BLOCK + || block == Blocks.JIGSAW || block == Blocks.MOVING_PISTON; + } + + private Direction nearestRampDirection(BlockPos targetFeet) { + double dx = enderman.getX() - (targetFeet.getX() + 0.5D); + double dz = enderman.getZ() - (targetFeet.getZ() + 0.5D); + if (Math.abs(dx) > Math.abs(dz)) { + return dx >= 0.0D ? Direction.EAST : Direction.WEST; + } + return dz >= 0.0D ? Direction.SOUTH : Direction.NORTH; } private boolean canPlaceRampStep(ServerLevel level, BlockPos pos, Direction approach, boolean firstStep) {