From afd98209f1349859d09a5e702d82505c96497955 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 20 Jul 2026 22:41:41 +0200 Subject: [PATCH] fix various issues --- build.gradle | 3 +- .../ai/AdaptiveSpecialAbilities.java | 25 ++++- .../ai/goals/AdaptiveAntiCheeseGoal.java | 103 ++++++++++++++---- .../event/AdaptiveMobsEvents.java | 13 +++ .../AdaptiveServerBehaviorGameTests.java | 9 +- 5 files changed, 122 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index f0fb5a7..2e510cc 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,8 @@ minecraft { // By default, the server will crash when no gametests are provided. // The gametest system is also enabled by default for other run configs under the /test command. gameTestServer { - property 'forge.enabledGameTestNamespaces', mod_id + // The tests use a small vanilla structure template, so its namespace must also be enabled. + property 'forge.enabledGameTestNamespaces', "${mod_id},minecraft" } data { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java index 5f2fead..67a6244 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java @@ -27,6 +27,7 @@ import net.minecraft.world.entity.monster.EnderMan; import net.minecraft.world.entity.monster.Endermite; import net.minecraft.world.entity.monster.Evoker; import net.minecraft.world.entity.monster.Guardian; +import net.minecraft.world.entity.monster.MagmaCube; import net.minecraft.world.entity.monster.Ghast; import net.minecraft.world.entity.monster.Phantom; import net.minecraft.world.entity.monster.Pillager; @@ -55,6 +56,7 @@ import net.minecraft.world.phys.Vec3; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; +import net.minecraftforge.event.level.ExplosionEvent; import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.event.ForgeEventFactory; @@ -67,6 +69,7 @@ public final class AdaptiveSpecialAbilities { private static final String COOLDOWN_PREFIX = "am_special_cd_"; private static final String SPLIT_FIREBALL_KEY = "am_split_fireball"; + private static final double SPLIT_FIREBALL_SPACING = 1.35D; private static final List DELAYED_REAPPEARS = new ArrayList<>(); private AdaptiveSpecialAbilities() { @@ -241,6 +244,10 @@ public final class AdaptiveSpecialAbilities { if (tier <= 0 || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) { return; } + if (mob instanceof MagmaCube && tier >= 3 && mob.getRandom().nextFloat() < 0.50F) { + player.setSecondsOnFire(4); + AdaptiveAIGoalUtils.debug(mob, () -> tier, "magma cube ignited target"); + } if (tier >= 4 && mob instanceof Drowned && mob.isInWater() && player.isInWater() && ready(mob, "dragging_grip", 160)) { player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 60, 1)); player.setDeltaMovement(player.getDeltaMovement().add(0.0D, -0.22D, 0.0D)); @@ -363,6 +370,7 @@ public final class AdaptiveSpecialAbilities { if (original.getPersistentData().getBoolean(SPLIT_FIREBALL_KEY)) { return; } + original.getPersistentData().putBoolean(SPLIT_FIREBALL_KEY, true); Vec3 direction = original.getDeltaMovement(); if (direction.lengthSqr() < 1.0E-4D && AdaptiveAIGoalUtils.isValidAdaptiveTarget(ghast.getTarget())) { direction = ghast.getTarget().getEyePosition().subtract(original.position()); @@ -377,19 +385,28 @@ public final class AdaptiveSpecialAbilities { } else { side = side.normalize(); } - spawnSplitFireball(level, ghast, original, direction.add(side.scale(0.18D)).normalize()); - spawnSplitFireball(level, ghast, original, direction.subtract(side.scale(0.18D)).normalize()); + spawnSplitFireball(level, ghast, original, direction.add(side.scale(0.24D)).normalize(), + side.scale(SPLIT_FIREBALL_SPACING)); + spawnSplitFireball(level, ghast, original, direction.subtract(side.scale(0.24D)).normalize(), + side.scale(-SPLIT_FIREBALL_SPACING)); AdaptiveAIGoalUtils.debug(ghast, () -> 5, "ghast splitting fireball"); } - private static void spawnSplitFireball(ServerLevel level, Ghast ghast, LargeFireball original, Vec3 direction) { + private static void spawnSplitFireball(ServerLevel level, Ghast ghast, LargeFireball original, + Vec3 direction, Vec3 offset) { LargeFireball split = new LargeFireball(level, ghast, direction.x, direction.y, direction.z, ghast.getExplosionPower()); split.getPersistentData().putBoolean(SPLIT_FIREBALL_KEY, true); - split.setPos(original.getX(), original.getY(), original.getZ()); + Vec3 position = original.position().add(offset).add(direction.scale(0.35D)); + split.setPos(position.x, position.y, position.z); level.addFreshEntity(split); } + public static void protectSplitFireballVolley(ExplosionEvent.Detonate event) { + event.getAffectedEntities().removeIf(entity -> entity instanceof LargeFireball fireball + && fireball.getPersistentData().getBoolean(SPLIT_FIREBALL_KEY)); + } + public static void invalidateTemporaryBlock(ServerLevel level, BlockPos pos) { TemporaryBlockSavedData.get(level).remove(level, pos); } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java index d457345..a39c2eb 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java @@ -35,6 +35,14 @@ import java.util.function.IntSupplier; public class AdaptiveAntiCheeseGoal extends Goal { private static final double TRAP_VEHICLE_BREAK_DISTANCE_SQR = 9.0D; + private static final double ZOMBIE_TOWER_COORDINATION_RADIUS = 12.0D; + // A tight radius lets body collision create a permanent crowd around the selected tower. + // Four blocks is still local, but allows the mount handoff before the crowd deadlocks. + private static final double ZOMBIE_MOUNT_DISTANCE_SQR = 16.0D; + private static final double ESTABLISHED_TOWER_MOUNT_DISTANCE_SQR = + ZOMBIE_TOWER_COORDINATION_RADIUS * ZOMBIE_TOWER_COORDINATION_RADIUS; + private static final String ZOMBIE_CLIMBER_UNTIL_KEY = "am_zombie_climber_until"; + private static final String LADDER_CLIMBER_UNTIL_KEY = "am_ladder_climber_until"; private final PathfinderMob mob; private final IntSupplier tierSupplier; @@ -58,6 +66,13 @@ public class AdaptiveAntiCheeseGoal extends Goal { if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return false; } + if (mob instanceof Zombie && mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) { + return true; + } + if (mob instanceof Zombie + && mob.getPersistentData().getLong(ZOMBIE_CLIMBER_UNTIL_KEY) > mob.level().getGameTime()) { + return false; + } if (isInTrapVehicle() || nearestTrapBlock() != null || trappedAllyVehicle() != null) { return true; } @@ -92,18 +107,24 @@ public class AdaptiveAntiCheeseGoal extends Goal { 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 && mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) { + activePillarTarget = target; + activePillarTicks = 40; + if (shouldUnstack(target)) { + mob.stopRiding(); + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie ladder released after target fell"); + } else { + tryCorpseLadderPressure(target); + } + return; + } + if (mob instanceof Zombie && isVerticalObstacleCheese(target)) { activePillarTarget = target; activePillarTicks = 24 + mob.getRandom().nextInt(16); @@ -181,7 +202,8 @@ public class AdaptiveAntiCheeseGoal extends Goal { return activePillarTicks > 0 && mob instanceof Zombie && AdaptiveAIGoalUtils.isValidAdaptiveTarget(activePillarTarget) - && isVerticalObstacleCheese(activePillarTarget); + && ((mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) + || isVerticalObstacleCheese(activePillarTarget)); } @Override @@ -289,25 +311,42 @@ public class AdaptiveAntiCheeseGoal extends Goal { if (mob.isPassenger() || mob.isVehicle()) { return false; } - List carriers = mob.level().getEntitiesOfClass(Zombie.class, mob.getBoundingBox().inflate(2.0D), + List carriers = mob.level().getEntitiesOfClass(Zombie.class, + mob.getBoundingBox().inflate(ZOMBIE_TOWER_COORDINATION_RADIUS), zombie -> zombie != mob && zombie.isAlive() && !zombie.isVehicle() && zombie.getTarget() == target + && zombie.getY() < target.getY() && !isInVehicleChain(zombie, mob) && Math.abs(zombie.getY() - mob.getY()) < 8.0D); Zombie bestCarrier = null; - double best = -1.0D; + int bestHeight = -1; + int bestBaseId = Integer.MAX_VALUE; + double bestDistance = Double.MAX_VALUE; for (Zombie carrier : carriers) { - double score = stackHeight(carrier) * 10.0D - mob.distanceToSqr(carrier); - if (score > best) { - best = score; + int height = stackHeight(carrier); + int baseId = stackBase(carrier).getId(); + double distance = mob.distanceToSqr(carrier); + if (height > bestHeight + || (height == bestHeight && baseId < bestBaseId) + || (height == bestHeight && baseId == bestBaseId && distance < bestDistance)) { + bestHeight = height; + bestBaseId = baseId; + bestDistance = distance; bestCarrier = carrier; } } if (bestCarrier == null) { return false; } + double mountDistanceSqr = bestHeight > 1 + ? ESTABLISHED_TOWER_MOUNT_DISTANCE_SQR : ZOMBIE_MOUNT_DISTANCE_SQR; + if (bestDistance > mountDistanceSqr) { + mob.getNavigation().moveTo(bestCarrier, 1.1D); + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie converging on existing tower"); + return true; + } boolean mounted = mob.startRiding(bestCarrier, true); if (mounted) { AdaptiveAIGoalUtils.markAdaptiveStackedZombie(mob); @@ -329,7 +368,28 @@ public class AdaptiveAntiCheeseGoal extends Goal { 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 (horizontal > 3.2D || vertical < 0.25D || vertical > 5.5D) { + if (mob.isVehicle()) { + return true; + } + if (horizontal <= 3.2D && vertical <= 1.0D) { + long now = mob.level().getGameTime(); + if (base.getPersistentData().getLong(LADDER_CLIMBER_UNTIL_KEY) > now) { + return true; + } + base.getPersistentData().putLong(LADDER_CLIMBER_UNTIL_KEY, now + 60L); + mob.stopRiding(); + AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); + mob.getPersistentData().putLong(ZOMBIE_CLIMBER_UNTIL_KEY, now + 80L); + 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(toward.x / len * 0.30D, 0.34D, toward.z / len * 0.30D); + mob.hurtMarked = true; + mob.getNavigation().moveTo(target, 1.2D); + activePillarTicks = 0; + AdaptiveAIGoalUtils.debug(mob, tierSupplier, "top zombie launched from ladder"); + return true; + } + if (horizontal > 3.2D || vertical > 5.5D) { return true; } @@ -339,10 +399,6 @@ public class AdaptiveAntiCheeseGoal extends Goal { mob.setDeltaMovement(mob.getDeltaMovement().add(toward.x / len * 0.18D, lift, toward.z / len * 0.18D)); mob.hurtMarked = true; - if (vertical <= 1.8D && horizontal <= 2.4D) { - mob.stopRiding(); - AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); - } AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie corpse ladder pressure"); return true; } @@ -368,19 +424,18 @@ public class AdaptiveAntiCheeseGoal extends Goal { 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) { + // Preserve the ladder while its base approaches an elevated target. Rider-to-target + // distance includes tower height and caused tall, otherwise valid ladders to oscillate. + if (verticalFromBase > 2.5D + && horizontalFromBase <= ZOMBIE_TOWER_COORDINATION_RADIUS + 4.0D) { return false; } AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob); - return !isVerticalObstacleCheese(target); + return true; } private int stackHeight(Entity entity) { diff --git a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java index 9607e5a..60ed41d 100644 --- a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java +++ b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java @@ -18,6 +18,7 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.monster.Enemy; import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.tags.DamageTypeTags; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent; import net.minecraftforge.event.entity.EntityJoinLevelEvent; @@ -57,6 +58,17 @@ public class AdaptiveMobsEvents { @SubscribeEvent public void onLivingAttack(LivingAttackEvent event) { + if (event.getEntity() instanceof ServerPlayer player + && event.getSource().getEntity() instanceof Mob explosiveMob + && event.getSource().is(DamageTypeTags.IS_EXPLOSION) + && player.level() instanceof ServerLevel level + && AMConfig.enabled && AMConfig.AI_ENABLED.get() + && AdaptiveGoalInjector.isAIEnabledFor(explosiveMob) + && AdaptiveDifficultyManager.getMobTier(level, explosiveMob.getType()) >= 5 + && AdaptiveAIGoalUtils.isBlockingShield(player)) { + player.disableShield(true); + AdaptiveAIGoalUtils.debug(explosiveMob, () -> 5, "mob explosion disabled shield"); + } if (!AMConfig.enabled || !AMConfig.AI_ENABLED.get() || !(event.getEntity() instanceof Mob victim) || !(victim instanceof Enemy) || !(event.getSource().getDirectEntity() instanceof Projectile) @@ -182,6 +194,7 @@ public class AdaptiveMobsEvents { @SubscribeEvent public void onExplosion(ExplosionEvent.Detonate event) { + AdaptiveSpecialAbilities.protectSplitFireballVolley(event); if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get() && event.getLevel() instanceof ServerLevel level) { AdaptiveMemory.rememberSound(level, event.getExplosion().getPosition(), AdaptiveMemory.SoundKind.EXPLOSION, 20 * 10); diff --git a/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java b/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java index 181c8f4..ec59df1 100644 --- a/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java +++ b/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java @@ -9,15 +9,17 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.monster.Slime; import net.minecraftforge.gametest.GameTestHolder; +import net.minecraftforge.gametest.PrefixGameTestTemplate; import java.util.Iterator; @GameTestHolder("adaptivemobs") +@PrefixGameTestTemplate(false) public final class AdaptiveServerBehaviorGameTests { private AdaptiveServerBehaviorGameTests() { } - @GameTest(template = "empty") + @GameTest(templateNamespace = "minecraft", template = "igloo/top") public static void temporaryBlocksSurviveSavedDataReload(GameTestHelper helper) { TemporaryBlockSavedData original = new TemporaryBlockSavedData(); BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); @@ -35,12 +37,15 @@ public final class AdaptiveServerBehaviorGameTests { helper.succeed(); } - @GameTest(template = "empty") + @GameTest(templateNamespace = "minecraft", template = "igloo/top") public static void slimeRecombinationCancelsWhenStuck(GameTestHelper helper) { Slime slime = helper.spawn(EntityType.SLIME, new BlockPos(1, 2, 1)); Slime partner = helper.spawn(EntityType.SLIME, new BlockPos(8, 2, 1)); slime.setSize(1, true); partner.setSize(1, true); + long now = helper.getLevel().getGameTime(); + slime.getPersistentData().putLong("am_recombine_ready_at", now); + partner.getPersistentData().putLong("am_recombine_ready_at", now); AdaptiveSlimeTacticsGoal goal = new AdaptiveSlimeTacticsGoal(slime, () -> 5); helper.assertTrue(goal.canUse(), "slime did not select its eligible partner");