From 55bdc63514dc24afcd04123f9084134345e6191f Mon Sep 17 00:00:00 2001 From: fretuks Date: Sun, 19 Jul 2026 21:06:04 +0200 Subject: [PATCH] properly implement 2 mechanics --- MOB_ADAPTATIONS.md | 9 ++- .../ai/AdaptiveSpecialAbilities.java | 47 +++++++++--- .../ai/goals/AdaptiveSlimeTacticsGoal.java | 76 +++++++++++++++++-- 3 files changed, 112 insertions(+), 20 deletions(-) diff --git a/MOB_ADAPTATIONS.md b/MOB_ADAPTATIONS.md index 6cdbe36..b0f83e9 100644 --- a/MOB_ADAPTATIONS.md +++ b/MOB_ADAPTATIONS.md @@ -103,13 +103,13 @@ Several hostile families also have identity-specific pressure tools. These use v - Guardians - Focused Beam: tier 4+ guardian beam damage increases slightly when multiple guardians are already focused on the same player. - Elder guardians - Drowning Pressure: elder guardian beam hits add brief Slowness and a sinking pull. - Shulkers - Gravity Chain: tier 4+ shulker hits against an already levitating player extend Levitation and apply brief Slowness. -- Slimes - Recombine: tier 5 small slimes gain short damage and jump pressure when they successfully hit, helping split slimes remain relevant. -- Magma cubes - Molten Recombine: tier 5 small magma cubes use the same short damage and jump pressure as slimes, and briefly ignite the player on hit. +- Slimes - Recombine: tier 5 equal-size Slimes that survive for 10-15 seconds seek one another within 12 blocks. On contact, one absorbs the other and doubles in size from 1 to 2 or from 2 to 4. +- Magma cubes - Molten Recombine: tier 5 Magma Cubes use the same delayed merge behavior, but only combine with equal-size Magma Cubes and never mix with ordinary Slimes. - Piglins - Gold Frenzy: piglin-family Nether pressure is stronger at tier 5, while gold still remains valid counterplay rather than a hard immunity bypass. - Piglin brutes - Guard Break: tier 5 brute hits add a small armor-piercing damage component and briefly disable shields. - Hoglins - Gore Toss: tier 4+ hoglin hits can toss the player upward and backward on cooldown. - Zoglins - Frenzied Rampage: tier 5 low-health zoglins gain a short Speed and Resistance burst. -- Ghasts - Splitting Fireball: treated as future work; current tier 5 ghasts already use stronger target prediction through airborne positioning and projectile aim adjustment. +- Ghasts - Splitting Fireball: every tier 5 Ghast fireball launches two additional fireballs at shallow side angles while preserving the original center shot. Split projectiles are marked so they cannot recursively split again. - Blazes - Overheat Volley: tier 5 blazes that take ranged damage briefly gain Speed and Strength pressure to punish static bow farming. - Phantoms - Bleeding Dive: tier 5 phantom hits apply brief Hunger as a vanilla bleeding-style damage-over-time pressure. - Vexes - Phase Cut: tier 5 vexes that hit while phasing gain bonus hit pressure and apply brief Weakness. @@ -363,6 +363,7 @@ AI changes: - Tier 2+ angled jump pressure instead of only direct movement. - Side positioning based on entity id so repeated slimes naturally spread. - Larger tier 5 slimes move and leap more aggressively. +- Tier 5 equal-size Slimes become eligible to recombine after surviving for 10-15 seconds, seek a compatible partner within 12 blocks, and merge on contact. Two size-1 Slimes become size 2; two size-2 Slimes become size 4. Size-4 Slimes do not grow further. - Adaptive melee positioning. - Tier 4+ shared targeting. @@ -376,6 +377,7 @@ Magma cubes receive shared scaling and spawn pressure through the generic hostil AI changes: - The same angled jump pressure as slimes, controlled by magma-cube advanced AI. +- Tier 5 Magma Cubes use the same 10-15 second recombination cycle and 1→2→4 size progression, but only with other Magma Cubes of exactly the same size. - Adaptive melee positioning. - Nether biome tactics make them bolder in the Nether. - Tier 4+ shared targeting. @@ -407,6 +409,7 @@ AI changes: - Airborne repositioning around targets. - Larger vertical offset than other airborne mobs. - Tier 5 target prediction for airborne positioning. +- Tier 5 Splitting Fireball adds two shallow-angle side projectiles to each normal center fireball. The side shots retain the Ghast as owner and use its normal explosion power. - Nether biome tactics identify them as Nether mobs where applicable. - Tier 4+ shared targeting. diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java index f24b1b9..c816038 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java @@ -25,13 +25,12 @@ 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; import net.minecraft.world.entity.monster.Ravager; import net.minecraft.world.entity.monster.Shulker; import net.minecraft.world.entity.monster.Silverfish; -import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.entity.monster.Spider; import net.minecraft.world.entity.monster.Stray; import net.minecraft.world.entity.monster.Vex; @@ -44,6 +43,7 @@ import net.minecraft.world.entity.monster.piglin.PiglinBrute; import net.minecraft.world.entity.monster.warden.Warden; import net.minecraft.world.entity.projectile.AbstractArrow; import net.minecraft.world.entity.projectile.Arrow; +import net.minecraft.world.entity.projectile.LargeFireball; import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.entity.projectile.ShulkerBullet; import net.minecraft.world.entity.player.Player; @@ -60,6 +60,7 @@ import java.util.List; 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 List TEMPORARY_BLOCKS = new ArrayList<>(); private static final List DELAYED_REAPPEARS = new ArrayList<>(); @@ -75,6 +76,10 @@ public final class AdaptiveSpecialAbilities { return; } int tier = tier(level, mob); + if (tier >= 5 && mob instanceof Ghast ghast && projectile instanceof LargeFireball fireball) { + splitGhastFireball(level, ghast, fireball); + return; + } if (tier < 4 || !(projectile instanceof Arrow arrow)) { return; } @@ -267,13 +272,6 @@ public final class AdaptiveSpecialAbilities { player.addEffect(new MobEffectInstance(MobEffects.LEVITATION, 80, 0)); player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 50, 0)); } - if ((mob instanceof Slime || mob instanceof MagmaCube) && tier >= 5 && mob.getBbWidth() <= 1.5F) { - mob.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, 80, 0)); - mob.addEffect(new MobEffectInstance(MobEffects.JUMP, 80, 1)); - if (mob instanceof MagmaCube) { - player.setSecondsOnFire(2); - } - } if (mob instanceof Phantom && tier >= 5) { player.addEffect(new MobEffectInstance(MobEffects.HUNGER, 100, 0)); } @@ -338,6 +336,37 @@ public final class AdaptiveSpecialAbilities { return directEntity instanceof Projectile; } + private static void splitGhastFireball(ServerLevel level, Ghast ghast, LargeFireball original) { + if (original.getPersistentData().getBoolean(SPLIT_FIREBALL_KEY)) { + return; + } + Vec3 direction = original.getDeltaMovement(); + if (direction.lengthSqr() < 1.0E-4D && AdaptiveAIGoalUtils.isValidAdaptiveTarget(ghast.getTarget())) { + direction = ghast.getTarget().getEyePosition().subtract(original.position()); + } + if (direction.lengthSqr() < 1.0E-4D) { + return; + } + direction = direction.normalize(); + Vec3 side = new Vec3(-direction.z, 0.0D, direction.x); + if (side.lengthSqr() < 1.0E-4D) { + side = new Vec3(1.0D, 0.0D, 0.0D); + } 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()); + AdaptiveAIGoalUtils.debug(ghast, () -> 5, "ghast splitting fireball"); + } + + private static void spawnSplitFireball(ServerLevel level, Ghast ghast, LargeFireball original, Vec3 direction) { + 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()); + level.addFreshEntity(split); + } + private record TemporaryBlock(net.minecraft.resources.ResourceKey dimension, BlockPos pos, long expireTick) { } 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 18b756c..c928877 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java @@ -9,39 +9,63 @@ import net.minecraft.world.entity.monster.MagmaCube; import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.phys.Vec3; +import java.util.Comparator; import java.util.EnumSet; +import java.util.List; import java.util.function.IntSupplier; public class AdaptiveSlimeTacticsGoal extends Goal { + private static final String RECOMBINE_READY_AT_KEY = "am_recombine_ready_at"; + private static final int MAX_RECOMBINE_SIZE = 4; + private static final double RECOMBINE_SEARCH_RADIUS = 12.0D; + private static final double RECOMBINE_DISTANCE_SQR = 3.24D; + private final Slime slime; private final IntSupplier tierSupplier; private int cooldown; + private int recombineCheckCooldown; + private Slime recombinePartner; public AdaptiveSlimeTacticsGoal(Slime slime, IntSupplier tierSupplier) { this.slime = slime; this.tierSupplier = tierSupplier; setFlags(EnumSet.of(Flag.MOVE, Flag.JUMP, Flag.LOOK)); + scheduleRecombine(200 + slime.getRandom().nextInt(101)); } @Override public boolean canUse() { boolean magma = slime instanceof MagmaCube; - return AMConfig.ENABLE_MELEE_AI.get() - && (magma ? AMConfig.ENABLE_MAGMA_CUBE_ADVANCED_AI.get() : AMConfig.ENABLE_SLIME_ADVANCED_AI.get()) - && AMConfig.AI_SLIME.get() - && tierSupplier.getAsInt() >= 2 - && slime.getTarget() != null - && slime.getTarget().isAlive() - && --cooldown <= 0; + if (!AMConfig.ENABLE_MELEE_AI.get() + || !(magma ? AMConfig.ENABLE_MAGMA_CUBE_ADVANCED_AI.get() : AMConfig.ENABLE_SLIME_ADVANCED_AI.get()) + || !AMConfig.AI_SLIME.get() || tierSupplier.getAsInt() < 2) { + return false; + } + recombinePartner = null; + if (tierSupplier.getAsInt() >= 5 && slime.getSize() < MAX_RECOMBINE_SIZE + && slime.level().getGameTime() >= slime.getPersistentData().getLong(RECOMBINE_READY_AT_KEY) + && --recombineCheckCooldown <= 0) { + recombineCheckCooldown = 10; + recombinePartner = findRecombinePartner(); + if (recombinePartner != null) { + return true; + } + } + return AdaptiveAIGoalUtils.isValidAdaptiveTarget(slime.getTarget()) && --cooldown <= 0; } @Override public void start() { int tier = tierSupplier.getAsInt(); + if (recombinePartner != null && recombinePartner.isAlive()) { + tryRecombine(recombinePartner); + recombinePartner = null; + return; + } cooldown = Math.max(16, 52 - tier * 5) + slime.getRandom().nextInt(25); LivingEntity target = slime.getTarget(); - if (target == null) { + if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) { return; } double distance = Math.sqrt(slime.distanceToSqr(target)); @@ -59,4 +83,40 @@ public class AdaptiveSlimeTacticsGoal extends Goal { } AdaptiveAIGoalUtils.debug(slime, tierSupplier, "slime angled jump pressure"); } + + private Slime findRecombinePartner() { + boolean magma = slime instanceof MagmaCube; + List candidates = slime.level().getEntitiesOfClass(Slime.class, + slime.getBoundingBox().inflate(RECOMBINE_SEARCH_RADIUS), + other -> other != slime && other.isAlive() && other.getSize() == slime.getSize() + && (other instanceof MagmaCube) == magma + && other.level().getGameTime() >= other.getPersistentData().getLong(RECOMBINE_READY_AT_KEY)); + return candidates.stream().min(Comparator.comparingDouble(slime::distanceToSqr)).orElse(null); + } + + private void 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; + } + LivingEntity inheritedTarget = AdaptiveAIGoalUtils.isValidAdaptiveTarget(slime.getTarget()) + ? slime.getTarget() : partner.getTarget(); + partner.discard(); + slime.setSize(Math.min(MAX_RECOMBINE_SIZE, slime.getSize() * 2), true); + if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(inheritedTarget)) { + slime.setTarget(inheritedTarget); + } + scheduleRecombine(200 + slime.getRandom().nextInt(101)); + AdaptiveAIGoalUtils.debug(slime, tierSupplier, + slime instanceof MagmaCube ? "magma cube recombined" : "slime recombined"); + } + + private void scheduleRecombine(int delayTicks) { + if (slime.getPersistentData().getLong(RECOMBINE_READY_AT_KEY) == 0L) { + slime.getPersistentData().putLong(RECOMBINE_READY_AT_KEY, slime.level().getGameTime() + delayTicks); + } else if (slime.getSize() > 1) { + slime.getPersistentData().putLong(RECOMBINE_READY_AT_KEY, slime.level().getGameTime() + delayTicks); + } + } }