diff --git a/README.md b/README.md index c06cb28..ce1c460 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ Mob-specific tiers can rise further when players repeatedly kill the same mob ty At the highest default tier, eligible hostile mobs receive: -- `+75%` max health. -- `+45%` attack damage when the mob has an attack damage attribute. -- `+5%` movement speed. +- `+95%` max health. +- `+60%` attack damage when the mob has an attack damage attribute. +- `+7%` movement speed. - `+25%` follow range. -- `+4` armor. -- `+0.10` knockback resistance. +- `+6` armor. +- `+0.18` knockback resistance. - A heal to the new maximum health after scaling is applied. -Spawn pressure is also at its highest default value: natural and chunk-generation hostile spawns have a `25%` chance to add one extra mob of the same type nearby. Extra spawns are capped at one per spawn event, obey normal spawn/collision checks, cannot recursively trigger more extras, and are skipped when the local hostile density cap is already reached. +Spawn pressure is also at its highest default value: natural and chunk-generation hostile spawns have a `40%` chance to add one extra mob of the same type nearby. Extra spawns are capped at one per spawn event, obey normal spawn/collision checks, cannot recursively trigger more extras, and are skipped when the local hostile density cap is already reached. ## Mob-Specific Tier 5 Behavior diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java index 323ddba..10b94cb 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java @@ -1,6 +1,7 @@ package net.fretux.adaptivemobs.ai; import net.fretux.adaptivemobs.ai.goals.AdaptiveCreeperPressureGoal; +import net.fretux.adaptivemobs.ai.goals.AdaptiveCreeperFuseGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveAirbornePositioningGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveAntiCheeseGoal; import net.fretux.adaptivemobs.ai.goals.AdaptiveBiomeTacticsGoal; @@ -109,7 +110,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) { - creeper.maxSwell = creeperFuseTicks(tier.getAsInt()); + mob.goalSelector.addGoal(0, new AdaptiveCreeperFuseGoal(creeper, tier)); 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) { @@ -351,10 +352,6 @@ public final class AdaptiveGoalInjector { return true; } - 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; } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java index d0810f8..223c646 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java @@ -3,6 +3,7 @@ package net.fretux.adaptivemobs.ai; import net.fretux.adaptivemobs.config.AMConfig; import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; @@ -48,10 +49,13 @@ import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.entity.projectile.ShulkerBullet; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; 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.common.util.BlockSnapshot; +import net.minecraftforge.event.ForgeEventFactory; import java.util.ArrayList; import java.util.Iterator; @@ -139,8 +143,8 @@ public final class AdaptiveSpecialAbilities { if (level == null || level.getGameTime() < block.expireTick) { continue; } - if (level.getBlockState(block.pos).is(Blocks.COBWEB)) { - level.removeBlock(block.pos, false); + if (level.getBlockState(block.pos).equals(block.placedState)) { + block.replaced.restore(true, false); } blockIterator.remove(); } @@ -186,7 +190,7 @@ public final class AdaptiveSpecialAbilities { AdaptiveAIGoalUtils.debug(mob, () -> tier, "zombified piglin blood call"); } if (tier >= 4 && mob instanceof Spider && ready(mob, "web_retreat", 600)) { - placeTemporaryCobweb(level, attacker.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61)); + placeTemporaryCobweb(level, mob, 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)) { Vec3 behind = attacker.position().subtract(attacker.getLookAngle().normalize().scale(2.2D)); @@ -244,7 +248,7 @@ public final class AdaptiveSpecialAbilities { 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 ? 400 : 600)) { - placeTemporaryCobweb(level, player.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61)); + placeTemporaryCobweb(level, mob, player.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61)); } if (mob instanceof CaveSpider && tier >= 3 && (AdaptiveAIGoalUtils.isUsingRangedWeapon(player) || player.isUsingItem())) { @@ -330,13 +334,22 @@ public final class AdaptiveSpecialAbilities { target.hurtMarked = true; } - private static void placeTemporaryCobweb(ServerLevel level, BlockPos requestedPos, long expireTick) { + private static void placeTemporaryCobweb(ServerLevel level, Mob spider, BlockPos requestedPos, long expireTick) { BlockPos pos = level.getFluidState(requestedPos).is(FluidTags.WATER) ? requestedPos.above() : requestedPos; - if (!level.getBlockState(pos).isAir() || !level.getWorldBorder().isWithinBounds(pos)) { + if (!ForgeEventFactory.getMobGriefingEvent(level, spider) + || !level.getBlockState(pos).isAir() || !level.getWorldBorder().isWithinBounds(pos)) { return; } - level.setBlock(pos, Blocks.COBWEB.defaultBlockState(), 3); - TEMPORARY_BLOCKS.add(new TemporaryBlock(level.getServer(), level.dimension(), pos.immutable(), expireTick)); + BlockSnapshot replaced = BlockSnapshot.create(level.dimension(), level, pos); + if (ForgeEventFactory.onBlockPlace(spider, replaced, Direction.UP)) { + return; + } + BlockState placedState = Blocks.COBWEB.defaultBlockState(); + if (!level.setBlock(pos, placedState, 3)) { + return; + } + TEMPORARY_BLOCKS.add(new TemporaryBlock(level.getServer(), level.dimension(), pos.immutable(), + expireTick, replaced, placedState)); level.playSound(null, pos, SoundEvents.SPIDER_AMBIENT, SoundSource.HOSTILE, 0.6F, 1.25F); } @@ -385,8 +398,8 @@ public final class AdaptiveSpecialAbilities { if (block.server != level.getServer() || block.dimension != level.dimension()) { return false; } - if (level.getBlockState(block.pos).is(Blocks.COBWEB)) { - level.removeBlock(block.pos, false); + if (level.getBlockState(block.pos).equals(block.placedState)) { + block.replaced.restore(true, false); } return true; }); @@ -399,8 +412,8 @@ public final class AdaptiveSpecialAbilities { return false; } ServerLevel level = server.getLevel(block.dimension); - if (level != null && level.getBlockState(block.pos).is(Blocks.COBWEB)) { - level.removeBlock(block.pos, false); + if (level != null && level.getBlockState(block.pos).equals(block.placedState)) { + block.replaced.restore(true, false); } return true; }); @@ -413,7 +426,7 @@ public final class AdaptiveSpecialAbilities { private record TemporaryBlock(net.minecraft.server.MinecraftServer server, net.minecraft.resources.ResourceKey dimension, - BlockPos pos, long expireTick) { + BlockPos pos, long expireTick, BlockSnapshot replaced, BlockState placedState) { } private record DelayedReappear(net.minecraft.server.MinecraftServer server, 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 439c42e..d457345 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveAntiCheeseGoal.java @@ -25,6 +25,7 @@ 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.minecraftforge.common.ForgeHooks; import net.minecraft.world.phys.Vec3; import java.util.EnumSet; @@ -260,6 +261,9 @@ public class AdaptiveAntiCheeseGoal extends Goal { if (mob instanceof Zombie && !AMConfig.ALLOW_ZOMBIE_BLOCK_BREAK.get()) { return; } + if (!ForgeHooks.canEntityDestroy(mob.level(), pos, mob)) { + return; + } if (state.is(Blocks.COBWEB)) { Block.popResource(mob.level(), pos, new ItemStack(Items.STRING)); } else if (state.is(Blocks.SWEET_BERRY_BUSH)) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java new file mode 100644 index 0000000..d661493 --- /dev/null +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java @@ -0,0 +1,38 @@ +package net.fretux.adaptivemobs.ai.goals; + +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.monster.Creeper; + +import java.util.function.IntSupplier; + +/** Keeps the creeper fuse synchronized with the current config-gated adaptive tier. */ +public final class AdaptiveCreeperFuseGoal extends Goal { + private final Creeper creeper; + private final IntSupplier tierSupplier; + private int updateCooldown; + + public AdaptiveCreeperFuseGoal(Creeper creeper, IntSupplier tierSupplier) { + this.creeper = creeper; + this.tierSupplier = tierSupplier; + } + + @Override + public boolean canUse() { + return true; + } + + @Override + public boolean canContinueToUse() { + return true; + } + + @Override + public void tick() { + if (--updateCooldown > 0) { + return; + } + updateCooldown = 20; + int tier = tierSupplier.getAsInt(); + creeper.maxSwell = tier <= 0 ? 30 : Math.max(18, 30 - Math.max(0, tier - 1) * 3); + } +} 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 77c0aa9..2266265 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveEndermanTacticsGoal.java @@ -18,6 +18,7 @@ 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.common.ForgeHooks; import net.minecraftforge.event.ForgeEventFactory; import java.util.EnumSet; @@ -341,7 +342,8 @@ public class AdaptiveEndermanTacticsGoal extends Goal { } private boolean pickUpBlock(ServerLevel level, BlockPos pos, String debugMessage) { - if (enderman.getCarriedBlock() != null || !isMovableBlock(level, pos)) { + if (enderman.getCarriedBlock() != null || !isMovableBlock(level, pos) + || !ForgeHooks.canEntityDestroy(level, pos, enderman)) { return false; } BlockState state = level.getBlockState(pos); diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java index c928877..1dd8683 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java @@ -59,8 +59,9 @@ public class AdaptiveSlimeTacticsGoal extends Goal { public void start() { int tier = tierSupplier.getAsInt(); if (recombinePartner != null && recombinePartner.isAlive()) { - tryRecombine(recombinePartner); - recombinePartner = null; + if (tryRecombine(recombinePartner)) { + recombinePartner = null; + } return; } cooldown = Math.max(16, 52 - tier * 5) + slime.getRandom().nextInt(25); @@ -84,6 +85,25 @@ public class AdaptiveSlimeTacticsGoal extends Goal { AdaptiveAIGoalUtils.debug(slime, tierSupplier, "slime angled jump pressure"); } + @Override + public boolean canContinueToUse() { + return recombinePartner != null && recombinePartner.isAlive() + && slime.isAlive() && tierSupplier.getAsInt() >= 5 + && slime.getSize() < MAX_RECOMBINE_SIZE; + } + + @Override + public void tick() { + if (recombinePartner != null && tryRecombine(recombinePartner)) { + recombinePartner = null; + } + } + + @Override + public void stop() { + recombinePartner = null; + } + private Slime findRecombinePartner() { boolean magma = slime instanceof MagmaCube; List candidates = slime.level().getEntitiesOfClass(Slime.class, @@ -94,11 +114,11 @@ public class AdaptiveSlimeTacticsGoal extends Goal { return candidates.stream().min(Comparator.comparingDouble(slime::distanceToSqr)).orElse(null); } - private void tryRecombine(Slime partner) { + private boolean tryRecombine(Slime partner) { if (slime.distanceToSqr(partner) > RECOMBINE_DISTANCE_SQR) { slime.getLookControl().setLookAt(partner, 30.0F, 30.0F); slime.getMoveControl().setWantedPosition(partner.getX(), partner.getY(), partner.getZ(), 1.2D); - return; + return false; } LivingEntity inheritedTarget = AdaptiveAIGoalUtils.isValidAdaptiveTarget(slime.getTarget()) ? slime.getTarget() : partner.getTarget(); @@ -110,6 +130,7 @@ public class AdaptiveSlimeTacticsGoal extends Goal { scheduleRecombine(200 + slime.getRandom().nextInt(101)); AdaptiveAIGoalUtils.debug(slime, tierSupplier, slime instanceof MagmaCube ? "magma cube recombined" : "slime recombined"); + return true; } private void scheduleRecombine(int delayTicks) { diff --git a/src/main/java/net/fretux/adaptivemobs/data/AdaptiveSavedData.java b/src/main/java/net/fretux/adaptivemobs/data/AdaptiveSavedData.java index 4d41a4f..5a5c272 100644 --- a/src/main/java/net/fretux/adaptivemobs/data/AdaptiveSavedData.java +++ b/src/main/java/net/fretux/adaptivemobs/data/AdaptiveSavedData.java @@ -78,8 +78,11 @@ public class AdaptiveSavedData extends SavedData { // --- Mutators (mark dirty) -------------------------------------------- public void addKill(String typeKey) { - totalHostileKills++; - killsByType.merge(typeKey, 1, Integer::sum); + if (totalHostileKills < Long.MAX_VALUE) { + totalHostileKills++; + } + killsByType.compute(typeKey, (key, current) -> + current == null ? 1 : current < Integer.MAX_VALUE ? current + 1 : Integer.MAX_VALUE); setDirty(); } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 80795f1..b0d202e 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -33,6 +33,7 @@ displayName="${mod_name}" #mandatory #credits="" #optional # A text field displayed in the mod UI authors="${mod_authors}" #optional +displayTest="IGNORE_SERVER_VERSION" # Display Test controls the display for your mod in the server connection screen # MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. # IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.