make endermen less dumb

This commit is contained in:
2026-07-20 02:36:43 +02:00
parent 36bb227fd3
commit d93ea10bf8
@@ -12,9 +12,12 @@ import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.EnderMan; import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.level.Level; 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.Blocks;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.util.BlockSnapshot;
import net.minecraftforge.event.ForgeEventFactory; import net.minecraftforge.event.ForgeEventFactory;
import java.util.EnumSet; 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_MIN = 4;
private static final int ANTI_PILLAR_BUILD_COOLDOWN_RANDOM = 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 EnderMan enderman;
private final IntSupplier tierSupplier; private final IntSupplier tierSupplier;
private int cooldown; private int cooldown;
@@ -31,6 +37,8 @@ public class AdaptiveEndermanTacticsGoal extends Goal {
private Direction rampApproach; private Direction rampApproach;
private int rampRise; private int rampRise;
private int rampBaseY; private int rampBaseY;
private LivingEntity activeRampTarget;
private int activeRampTicks;
public AdaptiveEndermanTacticsGoal(EnderMan enderman, IntSupplier tierSupplier) { public AdaptiveEndermanTacticsGoal(EnderMan enderman, IntSupplier tierSupplier) {
this.enderman = enderman; this.enderman = enderman;
@@ -68,11 +76,22 @@ public class AdaptiveEndermanTacticsGoal extends Goal {
.forEach(other -> other.setTarget(target)); .forEach(other -> other.setTarget(target));
AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman coordinated 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); cooldown = ANTI_PILLAR_BUILD_COOLDOWN_MIN + enderman.getRandom().nextInt(ANTI_PILLAR_BUILD_COOLDOWN_RANDOM);
if (tryPlaceMobStep(target, inEnd)) { activeRampTarget = target;
return; 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) { if (tier >= 3 || waterNearby || strongMelee) {
Vec3 side = AdaptivePositioningUtils.sidePosition(enderman, target, strongMelee ? 8.0D : (inEnd && tier >= 5 ? 5.0D : 3.0D), 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) { private boolean isToweredTarget(LivingEntity target) {
double vertical = target.getY() - enderman.getY(); double vertical = target.getY() - enderman.getY();
double horizontal = Math.sqrt((target.getX() - enderman.getX()) * (target.getX() - enderman.getX()) 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(); BlockState scaffoldState = enderman.getCarriedBlock();
if (!isUsefulScaffoldBlock(scaffoldState)) { if (!isUsefulScaffoldBlock(scaffoldState)) {
return false; return tryAcquireNearbyBlock(level, target);
} }
BlockPos targetFeet = BlockPos.containing(target.getX(), target.getY(), target.getZ()); BlockPos targetFeet = BlockPos.containing(target.getX(), target.getY(), target.getZ());
if (!targetFeet.equals(rampTargetFeet)) { if (!targetFeet.equals(rampTargetFeet)) {
rampTargetFeet = targetFeet; rampTargetFeet = targetFeet;
rampApproach = sharedRampDirection(targetFeet); rampApproach = nearestRampDirection(targetFeet);
rampRise = Math.min(8, Math.max(1, (int) Math.floor(target.getY() - enderman.getY()))); rampRise = Math.min(8, Math.max(1, (int) Math.floor(target.getY() - enderman.getY())));
rampBaseY = targetFeet.getY() - rampRise; rampBaseY = targetFeet.getY() - rampRise;
} }
@@ -120,25 +182,208 @@ public class AdaptiveEndermanTacticsGoal extends Goal {
int distance = Math.max(1, rampRise - step); int distance = Math.max(1, rampRise - step);
BlockPos pos = new BlockPos(targetFeet.getX(), rampBaseY + step, targetFeet.getZ()).relative(rampApproach, distance); BlockPos pos = new BlockPos(targetFeet.getX(), rampBaseY + step, targetFeet.getZ()).relative(rampApproach, distance);
if (canPlaceRampStep(level, pos, rampApproach, step == 0)) { 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.setBlock(pos, scaffoldState, 3);
level.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(enderman, scaffoldState));
enderman.setCarriedBlock(null); enderman.setCarriedBlock(null);
enderman.randomTeleport(pos.getX() + 0.5D, pos.getY() + 1.0D, pos.getZ() + 0.5D, true); enderman.randomTeleport(pos.getX() + 0.5D, pos.getY() + 1.0D, pos.getZ() + 0.5D, true);
AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman placed anti-pillar scaffold"); AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman placed anti-pillar scaffold");
return true; 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; 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) { private boolean isUsefulScaffoldBlock(BlockState state) {
return state != null && !state.liquid() return state != null && !state.liquid()
&& !state.getCollisionShape(enderman.level(), enderman.blockPosition()).isEmpty(); && !state.getCollisionShape(enderman.level(), enderman.blockPosition()).isEmpty();
} }
private Direction sharedRampDirection(BlockPos targetFeet) { private boolean tryAcquireNearbyBlock(ServerLevel level, LivingEntity target) {
Direction[] directions = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; BlockPos origin = enderman.blockPosition();
int index = Math.floorMod(targetFeet.getX() * 31 + targetFeet.getZ() * 17, directions.length); BlockPos best = null;
return directions[index]; 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) { private boolean canPlaceRampStep(ServerLevel level, BlockPos pos, Direction approach, boolean firstStep) {