Initial commit

This commit is contained in:
2026-06-29 15:59:48 +02:00
commit 7fe5ab4e24
67 changed files with 7661 additions and 0 deletions
@@ -0,0 +1,22 @@
package net.fretux.adaptivemobs;
import com.mojang.logging.LogUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.event.AdaptiveMobsEvents;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import org.slf4j.Logger;
@Mod(AdaptiveMobs.MODID)
public class AdaptiveMobs {
public static final String MODID = "adaptivemobs";
public static final Logger LOGGER = LogUtils.getLogger();
public AdaptiveMobs() {
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, AMConfig.SPEC);
MinecraftForge.EVENT_BUS.register(new AdaptiveMobsEvents());
}
}
@@ -0,0 +1,117 @@
package net.fretux.adaptivemobs.ai;
import net.fretux.adaptivemobs.AdaptiveMobs;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.function.IntSupplier;
public final class AdaptiveAIGoalUtils {
private AdaptiveAIGoalUtils() {
}
public static boolean advancedEnabled(int tier) {
return AMConfig.AI_ENABLED.get() && AMConfig.ENABLE_ADVANCED_AI.get() && tier > 0;
}
public static boolean isBlockingShield(LivingEntity entity) {
if (entity == null || !entity.isUsingItem()) {
return false;
}
ItemStack stack = entity.getUseItem();
return !stack.isEmpty() && stack.is(Items.SHIELD);
}
public static boolean isUsingRangedWeapon(LivingEntity entity) {
if (entity == null || !entity.isUsingItem()) {
return false;
}
ItemStack stack = entity.getUseItem();
return stack.is(Items.BOW) || stack.is(Items.CROSSBOW) || stack.is(Items.TRIDENT);
}
public static boolean isEating(LivingEntity entity) {
if (entity == null || !entity.isUsingItem()) {
return false;
}
ItemStack stack = entity.getUseItem();
return stack.isEdible();
}
public static boolean isLikelyMeleeWeapon(ItemStack stack) {
return stack.is(Items.WOODEN_SWORD) || stack.is(Items.STONE_SWORD) || stack.is(Items.IRON_SWORD)
|| stack.is(Items.GOLDEN_SWORD) || stack.is(Items.DIAMOND_SWORD) || stack.is(Items.NETHERITE_SWORD)
|| stack.is(Items.WOODEN_AXE) || stack.is(Items.STONE_AXE) || stack.is(Items.IRON_AXE)
|| stack.is(Items.GOLDEN_AXE) || stack.is(Items.DIAMOND_AXE) || stack.is(Items.NETHERITE_AXE);
}
public static int armorPieces(LivingEntity entity) {
int pieces = 0;
for (ItemStack stack : entity.getArmorSlots()) {
if (!stack.isEmpty()) {
pieces++;
}
}
return pieces;
}
public static double playerPriorityScore(Mob mob, Player player) {
double score = 0.0D;
score += (1.0D - healthFraction(player)) * 5.0D;
if (isEating(player)) {
score += 2.0D;
}
if (isUsingRangedWeapon(player)) {
score += 1.5D;
}
score += Math.max(0, 4 - armorPieces(player)) * 0.7D;
if (mob instanceof AbstractSkeleton && !isBlockingShield(player)) {
score += 1.2D;
}
if (!(mob instanceof AbstractSkeleton) && armorPieces(player) <= 2) {
score += 0.8D;
}
if (player.getLastHurtMob() instanceof Mob && player.getLastHurtMob() != mob) {
score += 1.0D;
}
score -= Math.sqrt(mob.distanceToSqr(player)) * 0.08D;
return score;
}
public static double healthFraction(LivingEntity entity) {
if (entity == null || entity.getMaxHealth() <= 0.0F) {
return 1.0D;
}
return entity.getHealth() / entity.getMaxHealth();
}
public static double followRange(Mob mob, double fallback) {
if (mob.getAttribute(Attributes.FOLLOW_RANGE) == null) {
return fallback;
}
return mob.getAttributeValue(Attributes.FOLLOW_RANGE);
}
public static int nextCooldown(Mob mob) {
return AMConfig.aiScanCooldown(mob.getRandom());
}
public static boolean hasServerLevel(Mob mob) {
return mob.level() instanceof ServerLevel;
}
public static void debug(Mob mob, IntSupplier tier, String message) {
if (!AMConfig.DEBUG_AI_LOGGING.get()) {
return;
}
AdaptiveMobs.LOGGER.info("[AdaptiveMobs AI] {} tier {}: {}", mob.getType(), tier.getAsInt(), message);
}
}
@@ -0,0 +1,347 @@
package net.fretux.adaptivemobs.ai;
import net.fretux.adaptivemobs.ai.goals.AdaptiveCreeperPressureGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveAirbornePositioningGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveAntiCheeseGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveBiomeTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveCreeperTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveEndermanTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveEndermiteTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveEvokerTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveEscapeDenialGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveFlankGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveGuardianTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveLowHealthRetreatGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveMeleePositioningGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveMoraleGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveRavagerTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveRangedAttackGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveRangedCoverGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveRetreatAndReengageGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSkeletonShieldGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSharedTargetGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveShulkerTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSlimeTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSoundInvestigationGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSpiderDisengageGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSpiderTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveSupportGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveTargetPriorityGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveThreatMemoryGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveWardenTacticsGoal;
import net.fretux.adaptivemobs.ai.goals.AdaptiveWitchTacticsGoal;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Blaze;
import net.minecraft.world.entity.monster.CaveSpider;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.entity.monster.Drowned;
import net.minecraft.world.entity.monster.ElderGuardian;
import net.minecraft.world.entity.monster.Endermite;
import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.monster.Evoker;
import net.minecraft.world.entity.monster.Ghast;
import net.minecraft.world.entity.monster.Guardian;
import net.minecraft.world.entity.monster.Husk;
import net.minecraft.world.entity.monster.MagmaCube;
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;
import net.minecraft.world.entity.monster.Vindicator;
import net.minecraft.world.entity.monster.WitherSkeleton;
import net.minecraft.world.entity.monster.Witch;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.entity.monster.ZombieVillager;
import net.minecraft.world.entity.monster.ZombifiedPiglin;
import net.minecraft.world.entity.monster.hoglin.Hoglin;
import net.minecraft.world.entity.monster.piglin.Piglin;
import net.minecraft.world.entity.monster.piglin.PiglinBrute;
import net.minecraft.world.entity.monster.warden.Warden;
import net.minecraft.world.entity.monster.Zoglin;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.IntSupplier;
public final class AdaptiveGoalInjector {
private static final Set<Mob> INJECTED = Collections.newSetFromMap(new WeakHashMap<>());
private AdaptiveGoalInjector() {
}
public static void inject(Mob mob, ServerLevel level, int initialTier) {
if (!AMConfig.AI_ENABLED.get() || initialTier <= 0 || isBoss(mob)) {
return;
}
if (!INJECTED.add(mob)) {
return;
}
PathfinderMob pathfinder = mob instanceof PathfinderMob pf ? pf : null;
IntSupplier tier = () -> AdaptiveDifficultyManager.getMobTier(level, mob.getType());
if (tier.getAsInt() >= 4) {
mob.targetSelector.addGoal(2, new AdaptiveSharedTargetGoal(mob, tier, 4, 12.0D));
}
if (pathfinder != null && mob instanceof AbstractSkeleton && AMConfig.AI_SKELETON.get()) {
mob.goalSelector.addGoal(3, new AdaptiveSkeletonShieldGoal(pathfinder, tier));
mob.goalSelector.addGoal(4, new AdaptiveFlankGoal(pathfinder, tier, 2, 8.0D, 1.0D, true));
} else if (pathfinder != null && mob instanceof Zombie && AMConfig.AI_ZOMBIE.get()) {
mob.goalSelector.addGoal(3, new AdaptiveFlankGoal(pathfinder, tier, 2, 2.5D, 1.05D, true));
} else if (pathfinder != null && mob instanceof Creeper creeper && AMConfig.AI_CREEPER.get()) {
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 && AMConfig.AI_SPIDER.get()) {
mob.goalSelector.addGoal(4, new AdaptiveFlankGoal(pathfinder, tier, 2, 2.0D, 1.15D, false));
} else if (pathfinder != null && ((mob instanceof Witch && AMConfig.AI_WITCH.get())
|| (mob instanceof Pillager && AMConfig.AI_PILLAGER.get()))) {
mob.goalSelector.addGoal(4, new AdaptiveFlankGoal(pathfinder, tier, 2, 7.0D, 1.0D, false));
}
if (AMConfig.ENABLE_ADVANCED_AI.get()) {
injectAdvanced(mob, pathfinder, tier);
AdaptiveAIGoalUtils.debug(mob, tier, "injected adaptive goals");
}
}
private static void injectAdvanced(Mob mob, PathfinderMob pathfinder, IntSupplier tier) {
if (AMConfig.ENABLE_TARGET_PRIORITY_AI.get()) {
mob.targetSelector.addGoal(3, new AdaptiveTargetPriorityGoal(mob, tier));
}
if (pathfinder != null && AMConfig.ENABLE_MORALE_AI.get() && !(mob instanceof Warden)) {
mob.goalSelector.addGoal(8, new AdaptiveMoraleGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_THREAT_MEMORY_AI.get()) {
mob.goalSelector.addGoal(2, new AdaptiveThreatMemoryGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get()) {
mob.goalSelector.addGoal(9, new AdaptiveSoundInvestigationGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_SUPPORT_AI.get() && !(mob instanceof Warden)) {
mob.goalSelector.addGoal(8, new AdaptiveSupportGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_ESCAPE_DENIAL_AI.get()
&& (mob instanceof Zombie || mob instanceof Spider)) {
mob.goalSelector.addGoal(6, new AdaptiveEscapeDenialGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_BIOME_TACTICS_AI.get()) {
mob.goalSelector.addGoal(8, new AdaptiveBiomeTacticsGoal(pathfinder, tier));
}
if (pathfinder != null && AMConfig.ENABLE_ANTI_CHEESE_AI.get()) {
mob.goalSelector.addGoal(7, new AdaptiveAntiCheeseGoal(pathfinder, tier));
}
if (pathfinder != null && isRangedMob(mob) && rangedEnabled(mob)) {
mob.goalSelector.addGoal(3, new AdaptiveRangedAttackGoal(pathfinder, tier, rangedOptimalRange(mob), 1.0D));
if (AMConfig.ENABLE_RANGED_COVER_AI.get()
&& (mob instanceof AbstractSkeleton || mob instanceof Pillager || mob instanceof Witch || mob instanceof Evoker)) {
mob.goalSelector.addGoal(6, new AdaptiveRangedCoverGoal(pathfinder, tier));
}
}
if (pathfinder != null && isMeleeMob(mob) && meleeEnabled(mob)) {
mob.goalSelector.addGoal(5, new AdaptiveMeleePositioningGoal(pathfinder, tier, meleeSpeed(mob), conservativeMelee(mob)));
}
if (mob instanceof Creeper creeper && AMConfig.ENABLE_CREEPER_AI.get() && AMConfig.AI_CREEPER.get()) {
mob.goalSelector.addGoal(3, new AdaptiveCreeperTacticsGoal(creeper, tier));
}
if (pathfinder != null && mob instanceof Spider && AMConfig.ENABLE_SPIDER_AI.get() && AMConfig.AI_SPIDER.get()) {
mob.goalSelector.addGoal(3, new AdaptiveSpiderTacticsGoal(pathfinder, tier, mob instanceof CaveSpider));
mob.goalSelector.addGoal(6, new AdaptiveSpiderDisengageGoal(pathfinder, tier, mob instanceof CaveSpider ? 4 : 5, 0.25D, 1.2D));
mob.goalSelector.addGoal(7, new AdaptiveLowHealthRetreatGoal(pathfinder, tier, 4, 0.28D, 1.15D));
}
if (mob instanceof Witch witch && AMConfig.ENABLE_WITCH_AI.get() && AMConfig.AI_WITCH.get()) {
mob.goalSelector.addGoal(3, new AdaptiveWitchTacticsGoal(witch, tier));
mob.goalSelector.addGoal(7, new AdaptiveLowHealthRetreatGoal(witch, tier, 3, 0.35D, 1.0D));
}
if (mob instanceof EnderMan enderman && AMConfig.ENABLE_ENDERMAN_AI.get() && AMConfig.AI_ENDERMAN.get()) {
mob.goalSelector.addGoal(2, new AdaptiveEndermanTacticsGoal(enderman, tier));
}
if (pathfinder != null && mob instanceof Endermite && AMConfig.ENABLE_ENDERMITE_AI.get() && AMConfig.AI_SMALL_SWARM.get()) {
mob.goalSelector.addGoal(4, new AdaptiveEndermiteTacticsGoal(pathfinder, tier));
}
if (mob instanceof Evoker evoker && AMConfig.ENABLE_RAID_MOB_AI.get()
&& AMConfig.ENABLE_EVOKER_AI.get() && AMConfig.AI_EVOKER.get()) {
mob.goalSelector.addGoal(3, new AdaptiveEvokerTacticsGoal(evoker, tier));
}
if (mob instanceof Guardian guardian && AMConfig.ENABLE_AQUATIC_HOSTILE_AI.get()
&& AMConfig.ENABLE_GUARDIAN_ADVANCED_AI.get() && AMConfig.AI_GUARDIAN.get()) {
mob.goalSelector.addGoal(4, new AdaptiveGuardianTacticsGoal(guardian, tier));
}
if (mob instanceof Shulker shulker && AMConfig.ENABLE_END_MOB_AI.get()
&& AMConfig.ENABLE_SHULKER_ADVANCED_AI.get() && AMConfig.AI_SHULKER.get()) {
mob.goalSelector.addGoal(4, new AdaptiveShulkerTacticsGoal(shulker, tier));
}
if (mob instanceof Slime slime && AMConfig.AI_SLIME.get()
&& ((mob instanceof MagmaCube && AMConfig.ENABLE_MAGMA_CUBE_ADVANCED_AI.get())
|| (!(mob instanceof MagmaCube) && AMConfig.ENABLE_SLIME_ADVANCED_AI.get()))) {
mob.goalSelector.addGoal(4, new AdaptiveSlimeTacticsGoal(slime, tier));
}
if (mob instanceof Ravager ravager && AMConfig.ENABLE_RAID_MOB_AI.get()
&& AMConfig.ENABLE_RAVAGER_ADVANCED_AI.get() && AMConfig.AI_RAVAGER.get()) {
mob.goalSelector.addGoal(4, new AdaptiveRavagerTacticsGoal(ravager, tier));
}
if (isAirborneMob(mob) && airborneEnabled(mob)) {
mob.goalSelector.addGoal(4, new AdaptiveAirbornePositioningGoal(mob, tier, airborneRange(mob), airborneVerticalOffset(mob)));
}
if (pathfinder != null && mob instanceof Vex && AMConfig.ENABLE_RAID_MOB_AI.get() && AMConfig.AI_VEX.get()) {
mob.goalSelector.addGoal(4, new AdaptiveRetreatAndReengageGoal(pathfinder, tier, 5, 0.10D, 1.2D));
mob.goalSelector.addGoal(7, new AdaptiveLowHealthRetreatGoal(pathfinder, tier, 4, 0.30D, 1.2D));
}
if (pathfinder != null && mob instanceof AbstractSkeleton && AMConfig.AI_SKELETON.get()) {
mob.goalSelector.addGoal(7, new AdaptiveLowHealthRetreatGoal(pathfinder, tier, 4, 0.25D, 1.0D));
}
if (pathfinder != null && mob instanceof Pillager && AMConfig.AI_PILLAGER.get()) {
mob.goalSelector.addGoal(7, new AdaptiveLowHealthRetreatGoal(pathfinder, tier, 4, 0.28D, 1.0D));
}
if (pathfinder != null && mob instanceof Warden && AMConfig.ENABLE_WARDEN_AI.get() && AMConfig.AI_WARDEN.get()) {
mob.goalSelector.addGoal(7, new AdaptiveMeleePositioningGoal(pathfinder, tier, 0.9D, true));
if (AMConfig.ENABLE_WARDEN_ADVANCED_AI.get()) {
mob.goalSelector.addGoal(8, new AdaptiveWardenTacticsGoal((Warden) mob, tier));
}
}
}
private static boolean isBoss(Mob mob) {
return !AMConfig.ENABLE_BOSS_AI.get()
&& (mob.getType() == EntityType.ENDER_DRAGON || mob.getType() == EntityType.WITHER);
}
private static boolean isRangedMob(Mob mob) {
return mob instanceof AbstractSkeleton || mob instanceof Pillager || mob instanceof Piglin
|| mob instanceof Witch || mob instanceof Ghast || mob instanceof Blaze
|| mob instanceof Guardian || mob instanceof Shulker;
}
private static boolean rangedEnabled(Mob mob) {
if (!AMConfig.ENABLE_RANGED_AI.get()) {
return false;
}
if (mob instanceof Pillager) {
return AMConfig.ENABLE_PILLAGER_AI.get() && AMConfig.AI_PILLAGER.get();
}
if (mob instanceof AbstractSkeleton) {
return AMConfig.AI_SKELETON.get();
}
if (mob instanceof Witch) {
return AMConfig.ENABLE_WITCH_AI.get() && AMConfig.AI_WITCH.get();
}
if (mob instanceof Guardian) {
return AMConfig.ENABLE_AQUATIC_HOSTILE_AI.get() && AMConfig.AI_GUARDIAN.get();
}
if (mob instanceof Shulker) {
return AMConfig.ENABLE_END_MOB_AI.get() && AMConfig.AI_SHULKER.get();
}
return !(mob instanceof Piglin) || (AMConfig.ENABLE_NETHER_MOB_AI.get() && AMConfig.AI_PIGLIN_FAMILY.get());
}
private static double rangedOptimalRange(Mob mob) {
if (mob instanceof Pillager || mob instanceof Piglin) {
return 10.0D;
}
if (mob instanceof Guardian || mob instanceof Shulker) {
return 12.0D;
}
if (mob instanceof Ghast) {
return 24.0D;
}
return 8.0D;
}
private static boolean isMeleeMob(Mob mob) {
return mob instanceof Zombie || mob instanceof Husk || mob instanceof Drowned
|| mob instanceof ZombieVillager || mob instanceof ZombifiedPiglin
|| mob instanceof Piglin || mob instanceof PiglinBrute || mob instanceof Vindicator
|| mob instanceof WitherSkeleton || mob instanceof Hoglin || mob instanceof Zoglin
|| mob instanceof Silverfish || mob instanceof Endermite || mob instanceof Slime || mob instanceof MagmaCube
|| mob instanceof Ravager || mob instanceof Warden;
}
private static boolean meleeEnabled(Mob mob) {
if (!AMConfig.ENABLE_MELEE_AI.get()) {
return false;
}
if (mob instanceof Piglin || mob instanceof PiglinBrute || mob instanceof ZombifiedPiglin
|| mob instanceof Hoglin || mob instanceof Zoglin) {
return AMConfig.ENABLE_NETHER_MOB_AI.get() && AMConfig.AI_PIGLIN_FAMILY.get();
}
if (mob instanceof Vindicator || mob instanceof Ravager) {
return AMConfig.ENABLE_RAID_MOB_AI.get() && AMConfig.AI_RAVAGER.get();
}
if (mob instanceof Silverfish) {
return AMConfig.AI_SMALL_SWARM.get();
}
if (mob instanceof Endermite) {
return AMConfig.ENABLE_ENDERMITE_AI.get() && AMConfig.AI_SMALL_SWARM.get();
}
if (mob instanceof Warden) {
return AMConfig.ENABLE_WARDEN_AI.get() && AMConfig.AI_WARDEN.get();
}
return AMConfig.AI_ZOMBIE.get() || mob instanceof WitherSkeleton || mob instanceof Slime || mob instanceof MagmaCube;
}
private static double meleeSpeed(Mob mob) {
if (mob instanceof Silverfish) {
return 1.15D;
}
if (mob instanceof Ravager || mob instanceof Warden) {
return 0.9D;
}
return 1.05D;
}
private static boolean conservativeMelee(Mob mob) {
return mob instanceof PiglinBrute || mob instanceof Ravager || mob instanceof Warden || mob instanceof ElderGuardian;
}
private static boolean isAirborneMob(Mob mob) {
return mob instanceof Ghast || mob instanceof Blaze || mob instanceof Phantom || mob instanceof Vex;
}
private static boolean airborneEnabled(Mob mob) {
if (mob instanceof Ghast) {
return AMConfig.ENABLE_NETHER_MOB_AI.get() && AMConfig.AI_GHAST.get();
}
if (mob instanceof Blaze) {
return AMConfig.ENABLE_NETHER_MOB_AI.get() && AMConfig.AI_BLAZE.get();
}
if (mob instanceof Phantom) {
return AMConfig.AI_PHANTOM.get();
}
if (mob instanceof Vex) {
return AMConfig.ENABLE_RAID_MOB_AI.get() && AMConfig.AI_VEX.get();
}
return true;
}
private static double airborneRange(Mob mob) {
if (mob instanceof Ghast) {
return 22.0D;
}
if (mob instanceof Phantom) {
return 8.0D;
}
return 7.0D;
}
private static double airborneVerticalOffset(Mob mob) {
if (mob instanceof Ghast) {
return 6.0D;
}
if (mob instanceof Phantom) {
return 5.0D;
}
return 2.0D;
}
}
@@ -0,0 +1,105 @@
package net.fretux.adaptivemobs.ai;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.WeakHashMap;
public final class AdaptiveMemory {
private static final int MAX_SOUND_ENTRIES_PER_LEVEL = 256;
private static final Map<Mob, ThreatEntry> THREATS = new WeakHashMap<>();
private static final Map<ServerLevel, List<SoundEntry>> SOUNDS = new WeakHashMap<>();
private AdaptiveMemory() {
}
public static void rememberThreat(Mob mob, ServerPlayer player, int ticks) {
if (!(mob.level() instanceof ServerLevel level)) {
return;
}
THREATS.put(mob, new ThreatEntry(player.getUUID(), level.getGameTime() + ticks));
}
public static Optional<ServerPlayer> rememberedThreat(Mob mob) {
if (!(mob.level() instanceof ServerLevel level)) {
return Optional.empty();
}
ThreatEntry entry = THREATS.get(mob);
if (entry == null || entry.expiresAt < level.getGameTime()) {
THREATS.remove(mob);
return Optional.empty();
}
ServerPlayer player = level.getServer().getPlayerList().getPlayer(entry.playerId);
return player != null && player.isAlive() ? Optional.of(player) : Optional.empty();
}
public static void rememberSound(ServerLevel level, BlockPos pos, SoundKind kind, int ticks) {
rememberSound(level, Vec3.atCenterOf(pos), kind, ticks);
}
public static void rememberSound(ServerLevel level, Vec3 pos, SoundKind kind, int ticks) {
List<SoundEntry> entries = SOUNDS.computeIfAbsent(level, ignored -> new ArrayList<>());
pruneExpired(entries, level.getGameTime());
if (entries.size() >= MAX_SOUND_ENTRIES_PER_LEVEL) {
entries.remove(0);
}
entries.add(new SoundEntry(pos, kind, level.getGameTime() + ticks));
}
public static Optional<SoundEntry> nearestSound(ServerLevel level, Mob mob, double radius) {
List<SoundEntry> entries = SOUNDS.get(level);
if (entries == null || entries.isEmpty()) {
return Optional.empty();
}
long now = level.getGameTime();
double maxSqr = radius * radius;
SoundEntry best = null;
double bestSqr = Double.MAX_VALUE;
Iterator<SoundEntry> iterator = entries.iterator();
while (iterator.hasNext()) {
SoundEntry entry = iterator.next();
if (entry.expiresAt < now) {
iterator.remove();
continue;
}
double dist = mob.distanceToSqr(entry.pos);
if (dist < maxSqr && dist < bestSqr) {
best = entry;
bestSqr = dist;
}
}
return Optional.ofNullable(best);
}
public static void clear(ServerLevel level) {
SOUNDS.remove(level);
}
private static void pruneExpired(List<SoundEntry> entries, long now) {
entries.removeIf(entry -> entry.expiresAt < now);
}
public enum SoundKind {
BLOCK_BREAK,
EATING,
BOW_SHOT,
EXPLOSION
}
private record ThreatEntry(UUID playerId, long expiresAt) {
}
public record SoundEntry(Vec3 pos, SoundKind kind, long expiresAt) {
}
}
@@ -0,0 +1,71 @@
package net.fretux.adaptivemobs.ai;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Pillager;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
public final class AdaptiveMobHooks {
private AdaptiveMobHooks() {
}
public static void onProjectileJoin(EntityJoinLevelEvent event) {
if (!AMConfig.enabled || !AMConfig.AI_ENABLED.get() || !AMConfig.ENABLE_ADVANCED_AI.get()
|| !AMConfig.ENABLE_RANGED_AI.get() || !(event.getLevel() instanceof ServerLevel level)) {
return;
}
Entity entity = event.getEntity();
if (!(entity instanceof Projectile projectile) || !(projectile.getOwner() instanceof Mob mob)) {
return;
}
LivingEntity target = mob.getTarget();
if (target == null || !target.isAlive()) {
return;
}
int tier = AdaptiveDifficultyManager.getMobTier(level, mob.getType());
if (tier <= 1) {
return;
}
if (tier >= 3 && AdaptiveAIGoalUtils.isBlockingShield(target) && mob.getRandom().nextFloat() < 0.45F) {
projectile.setDeltaMovement(projectile.getDeltaMovement().scale(0.94D));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "shield-blocked shot softened");
return;
}
if (tier >= 4 && AdaptiveTargetingUtils.hasAllyBetween(mob, target, 0.8D)) {
projectile.setDeltaMovement(projectile.getDeltaMovement().scale(0.90D));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "ally in ranged line");
return;
}
double accuracy = AMConfig.tierValue(AMConfig.AI_ACCURACY_BONUS.get(), tier);
if (accuracy <= 0.0D) {
return;
}
double lead = tier >= 5 ? AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tier) : 0.0D;
Vec3 aim = AdaptivePositioningUtils.predictedPosition(target, lead)
.add(0.0D, target.getBbHeight() * aimHeight(mob, target, tier), 0.0D);
Vec3 wanted = aim.subtract(projectile.position()).normalize().scale(projectile.getDeltaMovement().length());
Vec3 blended = projectile.getDeltaMovement().scale(1.0D - accuracy).add(wanted.scale(accuracy));
projectile.setDeltaMovement(blended);
AdaptiveAIGoalUtils.debug(mob, () -> tier, "projectile aim adjusted");
}
private static double aimHeight(Mob mob, LivingEntity target, int tier) {
if ((mob instanceof AbstractSkeleton || mob instanceof Pillager) && tier >= 4) {
double chance = AMConfig.tierValue(AMConfig.AI_HEADSHOT_CHANCE.get(), tier);
if (mob.getRandom().nextDouble() < chance) {
return 0.82D;
}
}
return 0.55D;
}
}
@@ -0,0 +1,97 @@
package net.fretux.adaptivemobs.ai;
import net.minecraft.core.BlockPos;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
public final class AdaptivePositioningUtils {
private AdaptivePositioningUtils() {
}
public static Vec3 predictedPosition(LivingEntity target, double strength) {
if (target == null || strength <= 0.0D) {
return target == null ? Vec3.ZERO : target.position();
}
return target.position().add(target.getDeltaMovement().scale(8.0D * strength));
}
public static Vec3 sidePosition(Mob mob, LivingEntity target, double radius, double side, double leadStrength) {
Vec3 aim = predictedPosition(target, leadStrength);
Vec3 away = mob.position().subtract(aim);
double len = Math.sqrt(away.x * away.x + away.z * away.z);
if (len < 1.0E-3D) {
away = new Vec3(1.0D, 0.0D, 0.0D);
len = 1.0D;
}
double angle = Math.toRadians(55.0D * side);
double nx = away.x * Math.cos(angle) - away.z * Math.sin(angle);
double nz = away.x * Math.sin(angle) + away.z * Math.cos(angle);
double scale = radius / len;
return new Vec3(aim.x + nx * scale, aim.y, aim.z + nz * scale);
}
public static Vec3 darkerSidePosition(Mob mob, LivingEntity target, double radius, double leadStrength) {
Vec3 left = sidePosition(mob, target, radius, 1.0D, leadStrength);
Vec3 right = sidePosition(mob, target, radius, -1.0D, leadStrength);
if (!AMConfig.ENABLE_DARK_APPROACH_AI.get()) {
return mob.getRandom().nextBoolean() ? left : right;
}
int leftLight = localLight(mob, left);
int rightLight = localLight(mob, right);
if (leftLight == rightLight) {
return mob.getRandom().nextBoolean() ? left : right;
}
return leftLight < rightLight ? left : right;
}
public static Vec3 retreatPosition(Mob mob, LivingEntity target, double distance) {
Vec3 away = mob.position().subtract(target.position());
double len = Math.sqrt(away.x * away.x + away.z * away.z);
if (len < 1.0E-3D) {
away = new Vec3(mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D, 0.0D);
len = 1.0D;
}
Vec3 dir = new Vec3(away.x / len, 0.0D, away.z / len);
return mob.position().add(dir.scale(distance));
}
public static boolean hasLineOfSight(Mob mob, LivingEntity target) {
if (target == null) {
return false;
}
HitResult hit = mob.level().clip(new ClipContext(mob.getEyePosition(), target.getEyePosition(),
ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mob));
return hit.getType() == HitResult.Type.MISS;
}
public static boolean isReasonableGround(Mob mob, Vec3 pos) {
BlockPos block = BlockPos.containing(pos);
return mob.level().getWorldBorder().isWithinBounds(block)
&& !mob.level().getBlockState(block).liquid()
&& mob.level().getBlockState(block).getCollisionShape(mob.level(), block).isEmpty();
}
public static boolean isOpenToSky(Mob mob) {
return mob.level().canSeeSky(mob.blockPosition());
}
public static int localLight(Mob mob, Vec3 pos) {
return mob.level().getMaxLocalRawBrightness(BlockPos.containing(pos));
}
public static Vec3 elevatedPosition(Mob mob, LivingEntity target, double radius) {
Vec3 base = sidePosition(mob, target, radius, mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D);
for (int y = 2; y >= 0; y--) {
Vec3 candidate = base.add(0.0D, y, 0.0D);
if (isReasonableGround(mob, candidate)) {
return candidate;
}
}
return base;
}
}
@@ -0,0 +1,345 @@
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.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.damagesource.DamageTypes;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Blaze;
import net.minecraft.world.entity.monster.CaveSpider;
import net.minecraft.world.entity.monster.Drowned;
import net.minecraft.world.entity.monster.ElderGuardian;
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.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;
import net.minecraft.world.entity.monster.Vindicator;
import net.minecraft.world.entity.monster.WitherSkeleton;
import net.minecraft.world.entity.monster.Zoglin;
import net.minecraft.world.entity.monster.ZombifiedPiglin;
import net.minecraft.world.entity.monster.hoglin.Hoglin;
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.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.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public final class AdaptiveSpecialAbilities {
private static final String COOLDOWN_PREFIX = "am_special_cd_";
private static final List<TemporaryBlock> TEMPORARY_BLOCKS = new ArrayList<>();
private static final List<DelayedReappear> DELAYED_REAPPEARS = new ArrayList<>();
private AdaptiveSpecialAbilities() {
}
public static void onProjectileJoin(EntityJoinLevelEvent event) {
if (!AMConfig.enabled || !(event.getLevel() instanceof ServerLevel level)) {
return;
}
Entity entity = event.getEntity();
if (!(entity instanceof Projectile projectile) || !(projectile.getOwner() instanceof Mob mob)) {
return;
}
int tier = tier(level, mob);
if (tier < 4 || !(projectile instanceof Arrow arrow)) {
return;
}
LivingEntity target = mob.getTarget();
if (mob instanceof Stray) {
arrow.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, tier >= 5 ? 100 : 70, tier >= 5 ? 1 : 0));
if (tier >= 5 && mob.getRandom().nextFloat() < 0.22F) {
arrow.addEffect(new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 70, 0));
}
return;
}
if (mob instanceof AbstractSkeleton) {
float poisonChance = tier >= 5 ? 0.16F : 0.09F;
if (mob.getRandom().nextFloat() < poisonChance) {
arrow.addEffect(new MobEffectInstance(MobEffects.POISON, 80 + mob.getRandom().nextInt(41), 0));
}
if (tier >= 5 && target != null && AdaptiveAIGoalUtils.isBlockingShield(target) && mob.getRandom().nextFloat() < 0.18F) {
arrow.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 60, 0));
} else if (tier >= 5 && target != null && target.isSprinting() && mob.getRandom().nextFloat() < 0.18F) {
arrow.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 60, 0));
}
return;
}
if (mob instanceof Pillager && tier >= 5 && mob.getRandom().nextFloat() < 0.18F) {
arrow.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 40, 1));
arrow.getPersistentData().putBoolean("am_pinning_bolt", true);
}
}
public static void onLivingHurt(LivingHurtEvent event) {
if (!AMConfig.enabled || !(event.getEntity().level() instanceof ServerLevel level)) {
return;
}
LivingEntity victim = event.getEntity();
Entity sourceEntity = event.getSource().getEntity();
Entity directEntity = event.getSource().getDirectEntity();
if (victim instanceof Mob damagedMob && sourceEntity instanceof LivingEntity attacker) {
onMobDamaged(level, damagedMob, attacker, directEntity);
}
if (victim instanceof ServerPlayer player && sourceEntity instanceof Mob attackerMob) {
onPlayerHurt(level, player, attackerMob, directEntity, event);
}
}
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END || event.getServer() == null) {
return;
}
long now = event.getServer().overworld().getGameTime();
Iterator<TemporaryBlock> blockIterator = TEMPORARY_BLOCKS.iterator();
while (blockIterator.hasNext()) {
TemporaryBlock block = blockIterator.next();
if (now < block.expireTick) {
continue;
}
ServerLevel level = event.getServer().getLevel(block.dimension);
if (level != null && level.getBlockState(block.pos).is(Blocks.COBWEB)) {
level.removeBlock(block.pos, false);
}
blockIterator.remove();
}
Iterator<DelayedReappear> reappearIterator = DELAYED_REAPPEARS.iterator();
while (reappearIterator.hasNext()) {
DelayedReappear pending = reappearIterator.next();
if (now < pending.tick) {
continue;
}
ServerLevel level = event.getServer().getLevel(pending.dimension);
if (level != null && pending.mob.isAlive() && pending.target.isAlive()) {
Vec3 side = AdaptivePositioningUtils.sidePosition(pending.mob, pending.target, 1.6D,
pending.mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D);
pending.mob.moveTo(side.x, side.y, side.z, pending.mob.getYRot(), pending.mob.getXRot());
pending.mob.removeEffect(MobEffects.INVISIBILITY);
pending.mob.setTarget(pending.target);
AdaptiveAIGoalUtils.debug(pending.mob, () -> pending.tier, "silverfish burrow burst");
}
reappearIterator.remove();
}
}
private static void onMobDamaged(ServerLevel level, Mob mob, LivingEntity attacker, Entity directEntity) {
int tier = tier(level, mob);
if (tier <= 0) {
return;
}
if (tier >= 5 && mob instanceof ZombifiedPiglin && ready(mob, "blood_call", 160)) {
for (Mob ally : AdaptiveTargetingUtils.nearbyHostiles(mob, 12.0D, other -> other instanceof ZombifiedPiglin)) {
ally.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 100, 1));
ally.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, 100, 0));
ally.setTarget(attacker);
}
AdaptiveAIGoalUtils.debug(mob, () -> tier, "zombified piglin blood call");
}
if (tier >= 4 && mob instanceof Spider && ready(mob, "web_retreat", 150)) {
placeTemporaryCobweb(level, 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));
if (enderman.randomTeleport(behind.x, behind.y, behind.z, true)) {
enderman.setTarget(attacker);
AdaptiveAIGoalUtils.debug(mob, () -> tier, "enderman backstep ambush");
}
}
if (tier >= 4 && mob instanceof Silverfish silverfish
&& AdaptiveAIGoalUtils.healthFraction(silverfish) < 0.45D
&& ready(mob, "burrow_burst", 220)) {
silverfish.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 35, 0));
silverfish.getNavigation().stop();
DELAYED_REAPPEARS.add(new DelayedReappear(level.dimension(), level.getGameTime() + 24, silverfish, attacker, tier));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "silverfish burrow vanish");
}
if (tier >= 4 && mob instanceof Evoker evoker && ready(mob, "vex_relay", 120)) {
for (Mob vex : AdaptiveTargetingUtils.nearbyHostiles(evoker, 14.0D, other -> other instanceof Vex)) {
vex.setTarget(attacker);
}
Vec3 retreat = AdaptivePositioningUtils.retreatPosition(evoker, attacker, 5.0D);
evoker.getNavigation().moveTo(retreat.x, retreat.y, retreat.z, 1.0D);
AdaptiveAIGoalUtils.debug(mob, () -> tier, "evoker vex relay");
}
if (tier >= 5 && mob instanceof Blaze && eventWasRanged(directEntity) && ready(mob, "overheat", 120)) {
mob.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, 80, 0));
mob.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 80, 0));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "blaze overheat volley");
}
if (tier >= 5 && mob instanceof Zoglin && AdaptiveAIGoalUtils.healthFraction(mob) < 0.50D && ready(mob, "rampage", 180)) {
mob.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 100, 1));
mob.addEffect(new MobEffectInstance(MobEffects.DAMAGE_RESISTANCE, 100, 0));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "zoglin frenzied rampage");
}
if (tier >= 5 && mob instanceof Warden && eventWasRanged(directEntity) && attacker instanceof ServerPlayer player) {
AdaptiveMemory.rememberThreat(mob, player, 20 * 30);
mob.getPersistentData().putLong(COOLDOWN_PREFIX + "echo_mark", level.getGameTime() + 20 * 30L);
AdaptiveAIGoalUtils.debug(mob, () -> tier, "warden echo mark");
}
}
private static void onPlayerHurt(ServerLevel level, ServerPlayer player, Mob mob, Entity directEntity, LivingHurtEvent event) {
int tier = tier(level, mob);
if (tier <= 0) {
return;
}
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));
AdaptiveAIGoalUtils.debug(mob, () -> tier, "drowned dragging grip");
}
if (mob instanceof WitherSkeleton && tier >= 4 && AdaptiveAIGoalUtils.healthFraction(player) < 0.40D) {
player.addEffect(new MobEffectInstance(MobEffects.WITHER, tier >= 5 ? 140 : 100, 0));
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 ? 100 : 140)) {
placeTemporaryCobweb(level, player.blockPosition(), level.getGameTime() + 100 + mob.getRandom().nextInt(61));
}
if (mob instanceof CaveSpider && tier >= 3
&& (AdaptiveAIGoalUtils.isUsingRangedWeapon(player) || player.isUsingItem())) {
player.addEffect(new MobEffectInstance(MobEffects.POISON, 60, 0));
}
if (mob instanceof Pillager && tier >= 5 && directEntity instanceof AbstractArrow arrow
&& arrow.getPersistentData().getBoolean("am_pinning_bolt")) {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 30, 1));
disableShield(player);
}
if ((mob instanceof Vindicator || mob instanceof PiglinBrute) && tier >= 5) {
event.setAmount(event.getAmount() + 2.0F);
disableShield(player);
}
if ((mob instanceof Ravager || mob instanceof Hoglin) && tier >= 4 && ready(mob, "crushing_charge", 120)) {
shove(player, mob.position(), mob instanceof Ravager ? 1.15D : 0.85D, 0.35D);
if (player.horizontalCollision) {
event.setAmount(event.getAmount() + 3.0F);
}
}
if (mob instanceof Guardian && tier >= 4) {
int focused = AdaptiveTargetingUtils.nearbyHostiles(mob, 12.0D,
other -> other instanceof Guardian && other.getTarget() == player).size();
if (focused >= 2) {
event.setAmount(event.getAmount() * (tier >= 5 ? 1.25F : 1.15F));
}
if (mob instanceof ElderGuardian) {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 50, 0));
player.setDeltaMovement(player.getDeltaMovement().add(0.0D, -0.12D, 0.0D));
}
}
if (mob instanceof Shulker && tier >= 4 && player.hasEffect(MobEffects.LEVITATION)) {
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));
}
if (mob instanceof Vex && tier >= 5 && mob.noPhysics) {
event.setAmount(event.getAmount() + 1.5F);
player.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 50, 0));
}
if (mob instanceof Endermite && tier >= 3) {
for (Mob ally : AdaptiveTargetingUtils.nearbyHostiles(mob, 10.0D,
other -> other instanceof Endermite || other instanceof EnderMan)) {
ally.setTarget(player);
}
}
if (mob.getPersistentData().getLong(COOLDOWN_PREFIX + "echo_mark") > level.getGameTime()
&& event.getSource().is(DamageTypes.SONIC_BOOM)) {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 80, 1));
shove(player, mob.position(), 0.65D, 0.10D);
}
}
private static int tier(ServerLevel level, Mob mob) {
if (!AMConfig.isMobEnabled(mob.getType())) {
return 0;
}
return AdaptiveDifficultyManager.getMobTier(level, mob.getType());
}
private static boolean ready(Mob mob, String key, long cooldownTicks) {
long now = mob.level().getGameTime();
String fullKey = COOLDOWN_PREFIX + key;
if (mob.getPersistentData().getLong(fullKey) > now) {
return false;
}
mob.getPersistentData().putLong(fullKey, now + cooldownTicks);
return true;
}
private static void disableShield(ServerPlayer player) {
if (AdaptiveAIGoalUtils.isBlockingShield(player)) {
player.disableShield(true);
}
}
private static void shove(LivingEntity target, Vec3 from, double horizontal, double vertical) {
Vec3 away = target.position().subtract(from);
double len = Math.max(0.001D, Math.sqrt(away.x * away.x + away.z * away.z));
target.setDeltaMovement(target.getDeltaMovement().add(away.x / len * horizontal, vertical, away.z / len * horizontal));
target.hurtMarked = true;
}
private static void placeTemporaryCobweb(ServerLevel level, BlockPos requestedPos, long expireTick) {
BlockPos pos = level.getFluidState(requestedPos).is(FluidTags.WATER) ? requestedPos.above() : requestedPos;
if (!level.getBlockState(pos).isAir() || !level.getWorldBorder().isWithinBounds(pos)) {
return;
}
level.setBlock(pos, Blocks.COBWEB.defaultBlockState(), 3);
TEMPORARY_BLOCKS.add(new TemporaryBlock(level.dimension(), pos.immutable(), expireTick));
level.playSound(null, pos, SoundEvents.SPIDER_AMBIENT, SoundSource.HOSTILE, 0.6F, 1.25F);
}
private static boolean eventWasRanged(Entity directEntity) {
return directEntity instanceof Projectile;
}
private record TemporaryBlock(net.minecraft.resources.ResourceKey<net.minecraft.world.level.Level> dimension,
BlockPos pos, long expireTick) {
}
private record DelayedReappear(net.minecraft.resources.ResourceKey<net.minecraft.world.level.Level> dimension,
long tick, Silverfish mob, LivingEntity target, int tier) {
}
}
@@ -0,0 +1,63 @@
package net.fretux.adaptivemobs.ai;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public final class AdaptiveTargetingUtils {
private AdaptiveTargetingUtils() {
}
public static List<Mob> nearbyHostiles(Mob mob, double radius, Predicate<Mob> predicate) {
AABB box = mob.getBoundingBox().inflate(radius);
List<Mob> found = mob.level().getEntitiesOfClass(Mob.class, box,
other -> other != mob && other instanceof Enemy && predicate.test(other));
int max = AMConfig.AI_MAX_NEARBY_MOBS_SCANNED.get();
if (found.size() <= max) {
return found;
}
return new ArrayList<>(found.subList(0, max));
}
public static int nearbyAttackers(Mob mob, LivingEntity target, double radius) {
if (target == null) {
return 0;
}
return nearbyHostiles(mob, radius, other -> other.getTarget() == target).size();
}
public static boolean hasAllyBetween(Mob mob, LivingEntity target, double width) {
if (target == null) {
return false;
}
Vec3 start = mob.getEyePosition();
Vec3 end = target.getEyePosition();
Vec3 line = end.subtract(start);
double lenSqr = line.lengthSqr();
if (lenSqr < 1.0E-4D) {
return false;
}
List<Mob> allies = nearbyHostiles(mob, Math.min(16.0D, Math.sqrt(lenSqr) + 2.0D),
other -> other.getTarget() == target || other.getTarget() == null);
for (Mob ally : allies) {
Vec3 toAlly = ally.position().add(0.0D, ally.getBbHeight() * 0.5D, 0.0D).subtract(start);
double t = toAlly.dot(line) / lenSqr;
if (t <= 0.05D || t >= 0.95D) {
continue;
}
Vec3 closest = start.add(line.scale(t));
if (closest.distanceTo(ally.position().add(0.0D, ally.getBbHeight() * 0.5D, 0.0D)) <= width + ally.getBbWidth()) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,52 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveAirbornePositioningGoal extends Goal {
private final Mob mob;
private final IntSupplier tierSupplier;
private final double range;
private final double verticalOffset;
private int cooldown;
public AdaptiveAirbornePositioningGoal(Mob mob, IntSupplier tierSupplier, double range, double verticalOffset) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.range = range;
this.verticalOffset = verticalOffset;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 2 && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
cooldown = AdaptiveAIGoalUtils.nextCooldown(mob);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
boolean spread = tier >= 4 && AdaptiveTargetingUtils.nearbyAttackers(mob, target, range) > 1;
double lead = tier >= 5 ? AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tier) : 0.0D;
Vec3 dest = AdaptivePositioningUtils.sidePosition(mob, target, range,
spread && mob.getRandom().nextBoolean() ? -1.0D : 1.0D, lead).add(0.0D, verticalOffset, 0.0D);
mob.getMoveControl().setWantedPosition(dest.x, dest.y, dest.z, 1.0D);
mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "airborne reposition");
}
}
@@ -0,0 +1,255 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.monster.Pillager;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.entity.vehicle.AbstractMinecart;
import net.minecraft.world.entity.vehicle.Boat;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class AdaptiveAntiCheeseGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveAntiCheeseGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_ANTI_CHEESE_AI.get()
&& tierSupplier.getAsInt() >= 3
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 55 + mob.getRandom().nextInt(80);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (avoidTrap()) {
return;
}
if (mob instanceof Zombie && isVerticalObstacleCheese(target)) {
if (tryCorpseLadderSurge(target)) {
return;
}
if (tryZombieStack(target)) {
return;
}
Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 2.5D, 0.0D);
moveIfReasonable(dest, 1.0D, "zombie pillar gather");
return;
}
if ((mob instanceof AbstractSkeleton || mob instanceof Pillager) && !mob.hasLineOfSight(target)) {
Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 7.0D, 0.0D);
moveIfReasonable(dest, 1.0D, "ranged tiny-window reposition");
return;
}
if (mob instanceof Creeper creeper && target.hasLineOfSight(creeper)
&& AdaptivePositioningUtils.isOpenToSky(mob)
&& Math.sqrt(mob.distanceToSqr(target)) > 4.0D) {
creeper.setSwellDir(-1);
Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 5.0D, 0.0D);
moveIfReasonable(dest, 1.0D, "creeper open-ground hesitation");
return;
}
if (mob instanceof EnderMan && (mob.level().getFluidState(mob.blockPosition()).is(FluidTags.WATER)
|| target.level().getFluidState(target.blockPosition()).is(FluidTags.WATER))) {
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 7.0D);
moveIfReasonable(dest, 1.0D, "enderman water disengage");
}
}
private boolean avoidTrap() {
Entity vehicle = mob.getVehicle();
if (vehicle instanceof Boat || vehicle instanceof AbstractMinecart) {
mob.stopRiding();
moveAwayFrom(vehicle.position(), 1.15D, "trap vehicle escape");
return true;
}
Entity trapVehicle = nearestTrapVehicle();
if (trapVehicle != null && mob.distanceToSqr(trapVehicle) < 2.9D) {
moveAwayFrom(trapVehicle.position(), 1.05D, "trap vehicle avoidance");
return true;
}
Vec3 trapBlock = nearestTrapBlock();
if (trapBlock != null) {
moveAwayFrom(trapBlock, 1.05D, "trap block avoidance");
return true;
}
return false;
}
private Entity nearestTrapVehicle() {
List<Entity> vehicles = mob.level().getEntitiesOfClass(Entity.class, mob.getBoundingBox().inflate(2.5D),
entity -> entity instanceof Boat || entity instanceof AbstractMinecart);
Entity nearest = null;
double best = Double.MAX_VALUE;
for (Entity entity : vehicles) {
double dist = mob.distanceToSqr(entity);
if (dist < best) {
best = dist;
nearest = entity;
}
}
return nearest;
}
private Vec3 nearestTrapBlock() {
Vec3 nearest = null;
double best = Double.MAX_VALUE;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
BlockState state = mob.level().getBlockState(mob.blockPosition().offset(x, y, z));
if (!isTrapBlock(state)) {
continue;
}
Vec3 pos = Vec3.atCenterOf(mob.blockPosition().offset(x, y, z));
double dist = mob.position().distanceToSqr(pos);
if (dist < best) {
best = dist;
nearest = pos;
}
}
}
}
return nearest;
}
private boolean isTrapBlock(BlockState state) {
return state.is(Blocks.COBWEB)
|| state.is(Blocks.POWDER_SNOW)
|| state.is(Blocks.SWEET_BERRY_BUSH);
}
private void moveAwayFrom(Vec3 threat, double speed, String debug) {
Vec3 away = mob.position().subtract(threat);
double len = Math.sqrt(away.x * away.x + away.z * away.z);
if (len < 1.0E-3D) {
away = new Vec3(mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D, 0.0D);
len = 1.0D;
}
Vec3 dest = mob.position().add(away.x / len * 4.0D, 0.0D, away.z / len * 4.0D);
moveIfReasonable(dest, speed, debug);
}
private boolean tryZombieStack(LivingEntity target) {
if (mob.isPassenger() || mob.isVehicle()) {
return false;
}
List<Zombie> carriers = mob.level().getEntitiesOfClass(Zombie.class, mob.getBoundingBox().inflate(2.0D),
zombie -> zombie != mob
&& zombie.isAlive()
&& !zombie.isVehicle()
&& zombie.getTarget() == target
&& Math.abs(zombie.getY() - mob.getY()) < 1.25D);
Zombie bestCarrier = null;
double best = Double.MAX_VALUE;
for (Zombie carrier : carriers) {
double dist = mob.distanceToSqr(carrier);
if (dist < best) {
best = dist;
bestCarrier = carrier;
}
}
if (bestCarrier == null) {
return false;
}
boolean mounted = mob.startRiding(bestCarrier, true);
if (mounted) {
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie stacking over obstacle");
}
return mounted;
}
private boolean tryCorpseLadderSurge(LivingEntity target) {
if (tierSupplier.getAsInt() < 5 || !mob.isPassenger()) {
return false;
}
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 (vertical < 0.75D || vertical > 2.25D || horizontal > 2.0D) {
return false;
}
List<Zombie> nearby = mob.level().getEntitiesOfClass(Zombie.class, mob.getBoundingBox().inflate(2.5D),
zombie -> zombie.isAlive() && zombie.getTarget() == target);
if (nearby.size() < 3) {
return false;
}
mob.setDeltaMovement(mob.getDeltaMovement().add(0.0D, 0.45D, 0.0D));
mob.hurtMarked = true;
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie corpse ladder surge");
return true;
}
private boolean isVerticalObstacleCheese(LivingEntity target) {
return isPillaring(target) || isSimpleWallBlocked(target);
}
private boolean isPillaring(LivingEntity target) {
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()));
return vertical > 2.5D && horizontal < 5.5D;
}
private boolean isSimpleWallBlocked(LivingEntity target) {
double vertical = Math.abs(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 (vertical > 2.0D || horizontal > 5.5D || mob.hasLineOfSight(target)) {
return false;
}
Vec3 direction = target.position().subtract(mob.position());
int steps = Math.max(2, (int) Math.ceil(horizontal));
for (int i = 1; i < steps; i++) {
double scale = i / (double) steps;
BlockPos base = BlockPos.containing(mob.getX() + direction.x * scale, mob.getY(), mob.getZ() + direction.z * scale);
if (isSolidObstacle(base.above()) || isSolidObstacle(base.above(2))) {
return true;
}
}
return false;
}
private boolean isSolidObstacle(BlockPos pos) {
return !mob.level().getBlockState(pos).getCollisionShape(mob.level(), pos).isEmpty();
}
private void moveIfReasonable(Vec3 dest, double speed, String debug) {
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, debug);
}
}
}
@@ -0,0 +1,83 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Blaze;
import net.minecraft.world.entity.monster.Drowned;
import net.minecraft.world.entity.monster.Spider;
import net.minecraft.world.entity.monster.Zoglin;
import net.minecraft.world.entity.monster.hoglin.Hoglin;
import net.minecraft.world.entity.monster.piglin.Piglin;
import net.minecraft.world.entity.monster.piglin.PiglinBrute;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveBiomeTacticsGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveBiomeTacticsGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 3 && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
int tier = tierSupplier.getAsInt();
cooldown = tier >= 5 ? 45 + mob.getRandom().nextInt(55) : 80 + mob.getRandom().nextInt(90);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
if (isNetherMob() && mob.level().dimension() == Level.NETHER && mob.distanceToSqr(target) < 18.0D * 18.0D) {
double speed = tier >= 5 ? 1.25D : (tier >= 4 ? 1.18D : 1.12D);
mob.getNavigation().moveTo(target, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "nether aggression");
return;
}
if (mob instanceof Drowned && mob.isInWater() && target.isInWater()) {
mob.getNavigation().moveTo(target, 1.12D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "drowned water pressure");
return;
}
if (mob instanceof Spider && !mob.level().canSeeSky(mob.blockPosition())) {
Vec3 elevated = AdaptivePositioningUtils.elevatedPosition(mob, target, 3.0D);
mob.getNavigation().moveTo(elevated.x, elevated.y, elevated.z, 1.12D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "cave vertical spider pressure");
return;
}
if (mob instanceof AbstractSkeleton && mob.level().canSeeSky(mob.blockPosition())) {
Vec3 dest = AdaptivePositioningUtils.sidePosition(mob, target, 9.0D, mob.getRandom().nextBoolean() ? 1.0D : -1.0D,
AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tierSupplier.getAsInt()) * 0.5D);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.0D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "skeleton open terrain spacing");
}
}
}
private boolean isNetherMob() {
return mob instanceof Piglin || mob instanceof PiglinBrute || mob instanceof Hoglin
|| mob instanceof Zoglin
|| mob instanceof Blaze
|| mob.getType() == EntityType.GHAST
|| mob.getType() == EntityType.MAGMA_CUBE;
}
}
@@ -0,0 +1,58 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveCreeperPressureGoal extends Goal {
private final Creeper creeper;
private final IntSupplier tierSupplier;
public AdaptiveCreeperPressureGoal(Creeper creeper, IntSupplier tierSupplier) {
this.creeper = creeper;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
LivingEntity target = creeper.getTarget();
return tierSupplier.getAsInt() >= 3 && target != null && target.isAlive();
}
@Override
public void tick() {
LivingEntity target = creeper.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double startSwell = tier >= 5 ? 5.0D : 4.0D;
double stopSwell = tier >= 4 ? 7.0D : 6.0D;
double dist = Math.sqrt(creeper.distanceToSqr(target));
creeper.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (AdaptiveAIGoalUtils.isBlockingShield(target) && dist <= stopSwell) {
creeper.setSwellDir(-1);
Vec3 dest = AdaptivePositioningUtils.sidePosition(creeper, target, Math.max(2.8D, dist),
creeper.getRandom().nextBoolean() ? 1.0D : -1.0D, tier >= 5 ? 0.25D : 0.0D);
if (AdaptivePositioningUtils.isReasonableGround(creeper, dest)) {
creeper.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.05D);
}
return;
}
if (dist <= startSwell || (tier >= 4 && target.getLastHurtMob() != null && dist <= stopSwell)) {
creeper.setSwellDir(1);
creeper.getNavigation().stop();
} else {
creeper.setSwellDir(-1);
}
}
}
@@ -0,0 +1,84 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveCreeperTacticsGoal extends Goal {
private final Creeper creeper;
private final IntSupplier tierSupplier;
private int cooldown;
private int retreatTicks;
private int blinkCooldown;
public AdaptiveCreeperTacticsGoal(Creeper creeper, IntSupplier tierSupplier) {
this.creeper = creeper;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 2 && creeper.getTarget() != null && creeper.getTarget().isAlive();
}
@Override
public void tick() {
LivingEntity target = creeper.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double dist = Math.sqrt(creeper.distanceToSqr(target));
creeper.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (AdaptiveAIGoalUtils.isBlockingShield(target) && dist <= 7.0D) {
creeper.setSwellDir(-1);
if (cooldown-- <= 0) {
cooldown = 30 + creeper.getRandom().nextInt(35);
Vec3 dest = AdaptivePositioningUtils.sidePosition(creeper, target, Math.max(3.0D, dist),
creeper.getRandom().nextBoolean() ? 1.0D : -1.0D, tier >= 5 ? 0.25D : 0.0D);
if (AdaptivePositioningUtils.isReasonableGround(creeper, dest)) {
creeper.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.05D);
AdaptiveAIGoalUtils.debug(creeper, tierSupplier, "creeper shield sidestep");
}
}
return;
}
if (tier >= 2 && dist > 4.5D && creeper.getSwellDir() > 0) {
creeper.setSwellDir(-1);
}
if (tier >= 3 && dist > 3.2D && creeper.getSwellDir() > 0) {
creeper.setSwellDir(-1);
}
if (tier >= 4 && AdaptiveTargetingUtils.nearbyAttackers(creeper, target, 7.0D) > 0 && dist <= 5.0D) {
creeper.setSwellDir(1);
}
if (tier >= 5 && dist <= 4.0D && blinkCooldown-- <= 0 && creeper.getSwellDir() <= 0) {
blinkCooldown = 180 + creeper.getRandom().nextInt(120);
creeper.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 10, 0, false, false));
creeper.level().playSound(null, creeper.blockPosition(), SoundEvents.CREEPER_PRIMED,
SoundSource.HOSTILE, 0.8F, 1.35F);
AdaptiveAIGoalUtils.debug(creeper, tierSupplier, "creeper blink shroud");
}
if (tier >= 5 && retreatTicks-- <= 0 && cooldown-- <= 0 && target.hasLineOfSight(creeper) && dist > 5.0D) {
cooldown = 120 + creeper.getRandom().nextInt(80);
retreatTicks = 25;
Vec3 dest = AdaptivePositioningUtils.retreatPosition(creeper, target, 5.0D);
creeper.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.05D);
AdaptiveAIGoalUtils.debug(creeper, tierSupplier, "creeper brief retreat");
}
}
}
@@ -0,0 +1,74 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.FluidTags;
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.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveEndermanTacticsGoal extends Goal {
private final EnderMan enderman;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveEndermanTacticsGoal(EnderMan enderman, IntSupplier tierSupplier) {
this.enderman = enderman;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_ENDERMAN_AI.get()
&& tierSupplier.getAsInt() >= 2
&& enderman.getTarget() != null
&& enderman.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
LivingEntity target = enderman.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
boolean inEnd = enderman.level().dimension() == Level.END;
cooldown = inEnd && tier >= 5 ? 35 + enderman.getRandom().nextInt(45) : 60 + enderman.getRandom().nextInt(80);
double dist = Math.sqrt(enderman.distanceToSqr(target));
boolean waterNearby = enderman.level().getFluidState(enderman.blockPosition()).is(FluidTags.WATER);
boolean strongMelee = tier >= 5 && dist < 4.0D && !AdaptiveAIGoalUtils.isUsingRangedWeapon(target);
if (inEnd && tier >= 4) {
double radius = tier >= 5 ? 18.0D : 12.0D;
AdaptiveTargetingUtils.nearbyHostiles(enderman, radius,
other -> other instanceof EnderMan && (other.getTarget() == null || other.getTarget() == target))
.forEach(other -> other.setTarget(target));
AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman coordinated target");
}
if (tier >= 3 || waterNearby || strongMelee) {
Vec3 side = AdaptivePositioningUtils.sidePosition(enderman, target, strongMelee ? 8.0D : (inEnd && tier >= 5 ? 5.0D : 3.0D),
enderman.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D);
int attempts = inEnd && tier >= 5 ? 6 : 4;
for (int i = 0; i < attempts; i++) {
Vec3 candidate = side.add(enderman.getRandom().nextInt(5) - 2, 0.0D, enderman.getRandom().nextInt(5) - 2);
BlockPos pos = BlockPos.containing(candidate);
if (!enderman.level().getFluidState(pos).is(FluidTags.WATER)
&& AdaptivePositioningUtils.isReasonableGround(enderman, candidate)
&& enderman.randomTeleport(candidate.x, candidate.y, candidate.z, true)) {
AdaptiveAIGoalUtils.debug(enderman, tierSupplier, "enderman angular teleport");
return;
}
}
}
}
}
@@ -0,0 +1,73 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveEndermiteTacticsGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
private LivingEntity rememberedTarget;
public AdaptiveEndermiteTacticsGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_MELEE_AI.get()
&& AMConfig.ENABLE_ENDERMITE_AI.get()
&& AMConfig.AI_SMALL_SWARM.get()
&& tierSupplier.getAsInt() >= 2
&& currentTarget() != null
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 18 + mob.getRandom().nextInt(28);
LivingEntity target = currentTarget();
if (target == null) {
return;
}
rememberedTarget = target;
if (mob.getTarget() == null) {
mob.setTarget(target);
}
int tier = tierSupplier.getAsInt();
double distance = Math.sqrt(mob.distanceToSqr(target));
double side = mob.getId() % 2 == 0 ? 1.0D : -1.0D;
double lead = tier >= 5 ? AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tier) : 0.0D;
Vec3 dest = AdaptivePositioningUtils.sidePosition(mob, target, Math.max(1.5D, distance * 0.45D), side, lead);
if (!AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
dest = target.position();
}
mob.getLookControl().setLookAt(target, 35.0F, 35.0F);
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, tier >= 5 ? 1.25D : 1.15D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "endermite side pursuit");
}
private LivingEntity currentTarget() {
LivingEntity target = mob.getTarget();
if (target != null && target.isAlive()) {
return target;
}
if (rememberedTarget != null && rememberedTarget.isAlive() && mob.distanceToSqr(rememberedTarget) < 16.0D * 16.0D) {
return rememberedTarget;
}
rememberedTarget = null;
return null;
}
}
@@ -0,0 +1,53 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Spider;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveEscapeDenialGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveEscapeDenialGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 3
&& (mob instanceof Zombie || mob instanceof Spider)
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 90 + mob.getRandom().nextInt(90);
LivingEntity target = mob.getTarget();
if (target == null || mob.distanceToSqr(target) > 12.0D * 12.0D) {
return;
}
Vec3 look = target.getLookAngle();
Vec3 exitBias = target.position().add(look.x * 3.0D, 0.0D, look.z * 3.0D);
if (!AdaptivePositioningUtils.isReasonableGround(mob, exitBias)) {
exitBias = AdaptivePositioningUtils.darkerSidePosition(mob, target, 3.5D, 0.0D);
}
if (AdaptivePositioningUtils.isReasonableGround(mob, exitBias)) {
mob.getNavigation().moveTo(exitBias.x, exitBias.y, exitBias.z, mob instanceof Spider ? 1.15D : 1.0D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "escape route pressure");
}
}
}
@@ -0,0 +1,80 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.AbstractIllager;
import net.minecraft.world.entity.monster.Evoker;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveEvokerTacticsGoal extends Goal {
private final Evoker evoker;
private final IntSupplier tierSupplier;
private int cooldown;
private int lastSpellTicks;
private int stillTicks;
private Vec3 lastPos = Vec3.ZERO;
public AdaptiveEvokerTacticsGoal(Evoker evoker, IntSupplier tierSupplier) {
this.evoker = evoker;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_RAID_MOB_AI.get()
&& AMConfig.ENABLE_EVOKER_AI.get()
&& AMConfig.AI_EVOKER.get()
&& tierSupplier.getAsInt() >= 2
&& evoker.getTarget() != null
&& evoker.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = evoker.isCastingSpell() ? 35 + evoker.getRandom().nextInt(35) : 55 + evoker.getRandom().nextInt(55);
LivingEntity target = evoker.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double distance = Math.sqrt(evoker.distanceToSqr(target));
boolean justCast = lastSpellTicks > 0 || evoker.isCastingSpell();
lastSpellTicks = evoker.isCastingSpell() ? 60 : Math.max(0, lastSpellTicks - cooldown);
updateStillTicks();
boolean meleeRush = distance < 7.0D && !AdaptiveAIGoalUtils.isUsingRangedWeapon(target);
boolean exposed = evoker.hasLineOfSight(target) && !AdaptiveAIGoalUtils.isBlockingShield(target);
boolean useCover = tier >= 4 && AdaptiveTargetingUtils.nearbyHostiles(evoker, 8.0D,
other -> other instanceof AbstractIllager && other != evoker && other.getTarget() == target).size() > 0;
if (meleeRush || justCast || stillTicks > 3 || (useCover && exposed)) {
Vec3 dest = useCover
? AdaptivePositioningUtils.sidePosition(evoker, target, 10.0D, evoker.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D)
: AdaptivePositioningUtils.retreatPosition(evoker, target, meleeRush ? 6.0D : 4.0D);
if (AdaptivePositioningUtils.isReasonableGround(evoker, dest)) {
evoker.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.0D);
AdaptiveAIGoalUtils.debug(evoker, tierSupplier, "evoker reposition");
}
}
}
private void updateStillTicks() {
Vec3 pos = evoker.position();
if (pos.distanceToSqr(lastPos) < 0.08D) {
stillTicks++;
} else {
stillTicks = 0;
}
lastPos = pos;
}
}
@@ -0,0 +1,129 @@
package net.fretux.adaptivemobs.ai.goals;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
/**
* A cooperative approach goal that makes a mob close on its target from an angle (flanking /
* circling) instead of marching straight in, and — at high tiers — aim at where the target is
* heading rather than where it currently is.
*
* <p>It only takes over movement when the mob is at medium range. Once the mob is close it yields
* (canContinueToUse returns false) so the vanilla melee/attack goal resumes and actually hits.</p>
*/
public class AdaptiveFlankGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double minDist;
private final double speed;
private final boolean leadAtTier5;
private int side;
private int recalcCooldown;
private Vec3 destination;
public AdaptiveFlankGoal(PathfinderMob mob, IntSupplier tierSupplier, int minTier,
double minDist, double speed, boolean leadAtTier5) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.minDist = minDist;
this.speed = speed;
this.leadAtTier5 = leadAtTier5;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
private double followRange() {
return mob.getAttributeValue(Attributes.FOLLOW_RANGE);
}
@Override
public boolean canUse() {
if (tierSupplier.getAsInt() < minTier) {
return false;
}
LivingEntity target = mob.getTarget();
if (target == null || !target.isAlive()) {
return false;
}
double distSqr = mob.distanceToSqr(target);
double follow = followRange();
return distSqr > minDist * minDist && distSqr < follow * follow;
}
@Override
public boolean canContinueToUse() {
LivingEntity target = mob.getTarget();
if (target == null || !target.isAlive() || tierSupplier.getAsInt() < minTier) {
return false;
}
double distSqr = mob.distanceToSqr(target);
// Stop a bit before contact so the melee goal can take the final approach and the hit.
double inner = (minDist * 0.7);
double follow = followRange();
return distSqr > inner * inner && distSqr < follow * follow;
}
@Override
public void start() {
// Alternate sides per-entity so a group fans out instead of stacking.
this.side = (mob.getId() % 2 == 0) ? 1 : -1;
this.recalcCooldown = 0;
}
@Override
public void stop() {
this.destination = null;
mob.getNavigation().stop();
}
@Override
public boolean requiresUpdateEveryTick() {
return false;
}
@Override
public void tick() {
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (recalcCooldown-- > 0) {
return;
}
recalcCooldown = 10; // ~0.5s, avoids per-tick pathfinding
Vec3 aim = target.position();
if (leadAtTier5 && tierSupplier.getAsInt() >= 5) {
// Intercept: aim where the target will be shortly.
aim = aim.add(target.getDeltaMovement().scale(8.0));
}
// Rotate the (target -> mob) vector toward one side to produce an angled approach.
Vec3 toMob = mob.position().subtract(aim);
double len = Math.sqrt(toMob.x * toMob.x + toMob.z * toMob.z);
if (len < 1.0E-3) {
return;
}
double angle = Math.toRadians(45.0 * side);
double nx = toMob.x * Math.cos(angle) - toMob.z * Math.sin(angle);
double nz = toMob.x * Math.sin(angle) + toMob.z * Math.cos(angle);
double radius = Math.max(minDist, len * 0.75);
double scale = radius / len;
double dx = aim.x + nx * scale;
double dz = aim.z + nz * scale;
this.destination = new Vec3(dx, aim.y, dz);
mob.getNavigation().moveTo(dx, aim.y, dz, speed);
}
}
@@ -0,0 +1,73 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.ElderGuardian;
import net.minecraft.world.entity.monster.Guardian;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class AdaptiveGuardianTacticsGoal extends Goal {
private final Guardian guardian;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveGuardianTacticsGoal(Guardian guardian, IntSupplier tierSupplier) {
this.guardian = guardian;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_AQUATIC_HOSTILE_AI.get()
&& AMConfig.ENABLE_GUARDIAN_ADVANCED_AI.get()
&& AMConfig.AI_GUARDIAN.get()
&& tierSupplier.getAsInt() >= 2
&& guardian.getTarget() != null
&& guardian.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
boolean elder = guardian instanceof ElderGuardian;
cooldown = elder ? 90 + guardian.getRandom().nextInt(80) : AdaptiveAIGoalUtils.nextCooldown(guardian);
LivingEntity target = guardian.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
boolean blocked = !guardian.hasLineOfSight(target);
boolean coordinate = tier >= 4 && !elder && AdaptiveTargetingUtils.nearbyAttackers(guardian, target, 12.0D) > 1;
if (blocked || coordinate) {
Vec3 dest = AdaptivePositioningUtils.sidePosition(guardian, target, elder ? 9.0D : 7.0D,
guardian.getRandom().nextBoolean() ? 1.0D : -1.0D, tier >= 5 ? 0.10D : 0.0D);
guardian.getMoveControl().setWantedPosition(dest.x, dest.y, dest.z, elder ? 0.65D : 0.9D);
AdaptiveAIGoalUtils.debug(guardian, tierSupplier, "guardian pressure reposition");
}
if (tier >= 5 && !elder) {
retargetLowHealthGuardianTarget(target);
}
}
private void retargetLowHealthGuardianTarget(LivingEntity current) {
List<LivingEntity> candidates = guardian.level().getEntitiesOfClass(LivingEntity.class,
guardian.getBoundingBox().inflate(12.0D),
entity -> entity != guardian && entity.isAlive() && guardian.canAttack(entity)
&& guardian.hasLineOfSight(entity)
&& AdaptiveAIGoalUtils.healthFraction(entity) < AdaptiveAIGoalUtils.healthFraction(current));
if (!candidates.isEmpty()) {
guardian.setTarget(candidates.get(0));
AdaptiveAIGoalUtils.debug(guardian, tierSupplier, "guardian exposed target pressure");
}
}
}
@@ -0,0 +1,56 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveLowHealthRetreatGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double healthThreshold;
private final double speed;
private int cooldown;
public AdaptiveLowHealthRetreatGoal(PathfinderMob mob, IntSupplier tierSupplier,
int minTier, double healthThreshold, double speed) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.healthThreshold = healthThreshold;
this.speed = speed;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.AI_RETREAT_ENABLED.get()
&& tierSupplier.getAsInt() >= minTier
&& AdaptiveAIGoalUtils.healthFraction(mob) <= healthThreshold
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 120 + mob.getRandom().nextInt(100);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 6.0D);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "low health retreat");
}
}
}
@@ -0,0 +1,72 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveMeleePositioningGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final double speed;
private final boolean conservative;
private int cooldown;
public AdaptiveMeleePositioningGoal(PathfinderMob mob, IntSupplier tierSupplier, double speed, boolean conservative) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.speed = speed;
this.conservative = conservative;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_MELEE_AI.get()
&& tierSupplier.getAsInt() >= 2
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = AdaptiveAIGoalUtils.nextCooldown(mob);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double distance = Math.sqrt(mob.distanceToSqr(target));
double follow = AdaptiveAIGoalUtils.followRange(mob, 16.0D);
if (distance > follow) {
return;
}
mob.getLookControl().setLookAt(target, 35.0F, 35.0F);
int attackers = AdaptiveTargetingUtils.nearbyAttackers(mob, target, 5.0D);
boolean crowding = attackers >= (conservative ? 3 : 2);
boolean sideApproach = tier >= 3 && attackers >= 2;
boolean intercept = tier >= 5;
if (!crowding && !sideApproach && !intercept) {
return;
}
double side = mob.getRandom().nextBoolean() ? 1.0D : -1.0D;
double lead = intercept ? AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tier) : 0.0D;
double radius = Math.max(2.0D, Math.min(5.0D, distance * 0.6D));
Vec3 dest = AdaptivePositioningUtils.sidePosition(mob, target, radius, side, lead);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "melee reposition");
}
}
}
@@ -0,0 +1,63 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveMoraleGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveMoraleGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_MORALE_AI.get()
&& tierSupplier.getAsInt() >= 3
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 70 + mob.getRandom().nextInt(80);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
int allies = AdaptiveTargetingUtils.nearbyHostiles(mob, 10.0D,
other -> other.getTarget() == target || other.getTarget() == null).size() + 1;
int players = Math.max(1, mob.level().getEntitiesOfClass(Player.class, mob.getBoundingBox().inflate(10.0D),
player -> player.isAlive() && !player.isCreative() && !player.isSpectator()).size());
if (allies > players + 1) {
Vec3 dest = AdaptivePositioningUtils.darkerSidePosition(mob, target, 2.5D + mob.getId() % 3, 0.0D);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.08D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "group morale pressure");
}
} else if (allies <= 1 && AdaptiveAIGoalUtils.healthFraction(mob) < 0.35D) {
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 5.0D);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.0D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "isolated defensive morale");
}
}
}
}
@@ -0,0 +1,68 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveRangedAttackGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final double optimalRange;
private final double speed;
private int cooldown;
private int side;
public AdaptiveRangedAttackGoal(PathfinderMob mob, IntSupplier tierSupplier, double optimalRange, double speed) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.optimalRange = optimalRange;
this.speed = speed;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_RANGED_AI.get()
&& tierSupplier.getAsInt() >= 2
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = AdaptiveAIGoalUtils.nextCooldown(mob);
side = mob.getRandom().nextBoolean() ? 1 : -1;
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
mob.getLookControl().setLookAt(target, 40.0F, 40.0F);
boolean shield = tier >= 3 && AdaptiveAIGoalUtils.isBlockingShield(target);
boolean badSight = tier >= 3 && !mob.hasLineOfSight(target);
boolean allyInLine = tier >= 4 && AdaptiveTargetingUtils.hasAllyBetween(mob, target, 0.8D);
double distance = Math.sqrt(mob.distanceToSqr(target));
if (shield || badSight || allyInLine || distance < optimalRange * 0.65D || distance > optimalRange * 1.25D) {
double radius = distance < optimalRange * 0.75D ? optimalRange : Math.max(4.0D, optimalRange * 0.85D);
double lead = tier >= 5 ? AMConfig.tierValue(AMConfig.AI_INTERCEPTION_STRENGTH.get(), tier) : 0.0D;
Vec3 dest = AdaptivePositioningUtils.sidePosition(mob, target, radius, side, lead);
if (!AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
dest = AdaptivePositioningUtils.retreatPosition(mob, target, 4.0D);
}
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "ranged reposition");
}
}
}
@@ -0,0 +1,45 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveRangedCoverGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveRangedCoverGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 3 && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
cooldown = 95 + mob.getRandom().nextInt(90);
LivingEntity target = mob.getTarget();
if (target == null || mob.distanceToSqr(target) > 16.0D * 16.0D) {
return;
}
Vec3 retreat = AdaptivePositioningUtils.retreatPosition(mob, target, 4.0D);
Vec3 side = AdaptivePositioningUtils.darkerSidePosition(mob, target, 8.0D, 0.0D);
Vec3 dest = mob.getRandom().nextBoolean() ? retreat : side;
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 0.95D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "imperfect ranged cover");
}
}
}
@@ -0,0 +1,77 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.AbstractIllager;
import net.minecraft.world.entity.monster.Ravager;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveRavagerTacticsGoal extends Goal {
private final Ravager ravager;
private final IntSupplier tierSupplier;
private int cooldown;
private LivingEntity rememberedTarget;
public AdaptiveRavagerTacticsGoal(Ravager ravager, IntSupplier tierSupplier) {
this.ravager = ravager;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_RAID_MOB_AI.get()
&& AMConfig.ENABLE_RAVAGER_ADVANCED_AI.get()
&& AMConfig.AI_RAVAGER.get()
&& tierSupplier.getAsInt() >= 3
&& currentTarget() != null
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 55 + ravager.getRandom().nextInt(65);
LivingEntity target = currentTarget();
if (target == null) {
return;
}
rememberedTarget = target;
if (ravager.getTarget() == null) {
ravager.setTarget(target);
}
int tier = tierSupplier.getAsInt();
double distance = Math.sqrt(ravager.distanceToSqr(target));
boolean tooClose = distance < 3.5D;
boolean coordinated = tier >= 4 && AdaptiveTargetingUtils.nearbyHostiles(ravager, 10.0D,
other -> other instanceof AbstractIllager && other.getTarget() == target).size() > 0;
if (tooClose || coordinated || (tier >= 5 && distance < 10.0D && ravager.hasLineOfSight(target))) {
Vec3 dest = tooClose
? AdaptivePositioningUtils.retreatPosition(ravager, target, 3.0D)
: AdaptivePositioningUtils.sidePosition(ravager, target, 5.0D, ravager.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.08D);
if (AdaptivePositioningUtils.isReasonableGround(ravager, dest)) {
ravager.getNavigation().moveTo(dest.x, dest.y, dest.z, tooClose ? 0.85D : 1.05D);
AdaptiveAIGoalUtils.debug(ravager, tierSupplier, "ravager deliberate pressure");
}
}
}
private LivingEntity currentTarget() {
LivingEntity target = ravager.getTarget();
if (target != null && target.isAlive()) {
return target;
}
if (rememberedTarget != null && rememberedTarget.isAlive() && ravager.distanceToSqr(rememberedTarget) < 20.0D * 20.0D) {
return rememberedTarget;
}
rememberedTarget = null;
return null;
}
}
@@ -0,0 +1,59 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveRetreatAndReengageGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double triggerHealthLoss;
private final double speed;
private int cooldown;
private float lastHealth;
public AdaptiveRetreatAndReengageGoal(PathfinderMob mob, IntSupplier tierSupplier,
int minTier, double triggerHealthLoss, double speed) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.triggerHealthLoss = triggerHealthLoss;
this.speed = speed;
this.lastHealth = mob.getHealth();
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
float current = mob.getHealth();
float lost = lastHealth - current;
lastHealth = current;
return AMConfig.AI_RETREAT_ENABLED.get()
&& tierSupplier.getAsInt() >= minTier
&& lost >= mob.getMaxHealth() * triggerHealthLoss
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& cooldown-- <= 0;
}
@Override
public void start() {
cooldown = 80 + mob.getRandom().nextInt(80);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 5.0D);
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "retreat and re-engage");
}
}
@@ -0,0 +1,53 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class AdaptiveSharedTargetGoal extends Goal {
private final Mob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double radius;
private int cooldown;
public AdaptiveSharedTargetGoal(Mob mob, IntSupplier tierSupplier, int minTier, double radius) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.radius = radius;
setFlags(EnumSet.noneOf(Flag.class));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= minTier && mob.getTarget() != null && --cooldown <= 0;
}
@Override
public void start() {
cooldown = AdaptiveAIGoalUtils.nextCooldown(mob);
LivingEntity target = mob.getTarget();
if (target == null || !target.isAlive()) {
return;
}
List<Mob> allies = AdaptiveTargetingUtils.nearbyHostiles(mob, radius, other -> other.getTarget() == null);
int shared = 0;
for (Mob ally : allies) {
if (shared++ >= 4) {
break;
}
ally.setTarget(target);
}
if (shared > 0) {
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "shared target with " + shared + " allies");
}
}
}
@@ -0,0 +1,66 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Shulker;
import net.minecraft.world.level.Level;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveShulkerTacticsGoal extends Goal {
private final Shulker shulker;
private final IntSupplier tierSupplier;
private int cooldown;
private int blockedTicks;
public AdaptiveShulkerTacticsGoal(Shulker shulker, IntSupplier tierSupplier) {
this.shulker = shulker;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_END_MOB_AI.get()
&& AMConfig.ENABLE_SHULKER_ADVANCED_AI.get()
&& AMConfig.AI_SHULKER.get()
&& tierSupplier.getAsInt() >= 2
&& shulker.getTarget() != null
&& shulker.getTarget().isAlive()
&& --cooldown <= 0;
}
@Override
public void start() {
int tier = tierSupplier.getAsInt();
boolean inEnd = shulker.level().dimension() == Level.END;
cooldown = inEnd && tier >= 5 ? 30 + shulker.getRandom().nextInt(35)
: 50 + shulker.getRandom().nextInt(tier >= 5 ? 45 : 70);
LivingEntity target = shulker.getTarget();
if (target == null) {
return;
}
if (!shulker.hasLineOfSight(target)) {
blockedTicks++;
if (blockedTicks >= 2) {
shulker.setTarget(null);
blockedTicks = 0;
AdaptiveAIGoalUtils.debug(shulker, tierSupplier, "shulker held fire without line of sight");
}
return;
}
blockedTicks = 0;
if (tier >= 4) {
double radius = inEnd && tier >= 5 ? 18.0D : 12.0D;
AdaptiveTargetingUtils.nearbyHostiles(shulker, radius,
other -> other instanceof Shulker && (other.getTarget() == null || other.getTarget() == target))
.forEach(other -> other.setTarget(target));
AdaptiveAIGoalUtils.debug(shulker, tierSupplier, "shulker coordinated target");
}
}
}
@@ -0,0 +1,55 @@
package net.fretux.adaptivemobs.ai.goals;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveSkeletonShieldGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveSkeletonShieldGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
LivingEntity target = mob.getTarget();
return tierSupplier.getAsInt() >= 3
&& target != null
&& target.isAlive()
&& target.isUsingItem()
&& isShield(target.getUseItem())
&& mob.hasLineOfSight(target)
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 30 + mob.getRandom().nextInt(30);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
Vec3 away = mob.position().subtract(target.position());
Vec3 side = new Vec3(-away.z, 0.0D, away.x).normalize()
.scale(mob.getRandom().nextBoolean() ? 5.0D : -5.0D);
Vec3 dest = mob.position().add(side).add(away.normalize().scale(2.0D));
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.0D);
}
private static boolean isShield(ItemStack stack) {
return !stack.isEmpty() && stack.is(Items.SHIELD);
}
}
@@ -0,0 +1,62 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.MagmaCube;
import net.minecraft.world.entity.monster.Slime;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveSlimeTacticsGoal extends Goal {
private final Slime slime;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveSlimeTacticsGoal(Slime slime, IntSupplier tierSupplier) {
this.slime = slime;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.JUMP, Flag.LOOK));
}
@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;
}
@Override
public void start() {
int tier = tierSupplier.getAsInt();
cooldown = Math.max(16, 52 - tier * 5) + slime.getRandom().nextInt(25);
LivingEntity target = slime.getTarget();
if (target == null) {
return;
}
double distance = Math.sqrt(slime.distanceToSqr(target));
double side = slime.getId() % 2 == 0 ? 1.0D : -1.0D;
Vec3 dest = AdaptivePositioningUtils.sidePosition(slime, target, Math.max(2.0D, distance * 0.55D), side, 0.0D);
if (!AdaptivePositioningUtils.isReasonableGround(slime, dest)) {
dest = target.position();
}
double speed = tier >= 5 && slime.getSize() >= 3 ? 1.25D : 1.0D;
slime.getLookControl().setLookAt(target, 30.0F, 30.0F);
slime.getMoveControl().setWantedPosition(dest.x, dest.y, dest.z, speed);
if (slime.onGround() && distance < 9.0D) {
Vec3 leap = target.position().subtract(slime.position()).normalize().scale(tier >= 5 && slime.getSize() >= 3 ? 0.34D : 0.24D);
slime.setDeltaMovement(slime.getDeltaMovement().add(leap.x, 0.28D, leap.z));
}
AdaptiveAIGoalUtils.debug(slime, tierSupplier, "slime angled jump pressure");
}
}
@@ -0,0 +1,44 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptiveMemory;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import java.util.EnumSet;
import java.util.Optional;
import java.util.function.IntSupplier;
public class AdaptiveSoundInvestigationGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveSoundInvestigationGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 2
&& mob.getTarget() == null
&& mob.level() instanceof ServerLevel
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 80 + mob.getRandom().nextInt(80);
ServerLevel level = (ServerLevel) mob.level();
Optional<AdaptiveMemory.SoundEntry> sound = AdaptiveMemory.nearestSound(level, mob, 14.0D);
if (sound.isEmpty()) {
return;
}
mob.getNavigation().moveTo(sound.get().pos().x, sound.get().pos().y, sound.get().pos().z, 1.0D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "investigating " + sound.get().kind());
}
}
@@ -0,0 +1,66 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveSpiderDisengageGoal extends Goal {
private static final int DISENGAGE_TICKS = 100;
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double triggerHealthLoss;
private final double speed;
private int cooldown;
private float lastHealth;
public AdaptiveSpiderDisengageGoal(PathfinderMob mob, IntSupplier tierSupplier,
int minTier, double triggerHealthLoss, double speed) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.triggerHealthLoss = triggerHealthLoss;
this.speed = speed;
this.lastHealth = mob.getHealth();
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
float current = mob.getHealth();
float lost = lastHealth - current;
lastHealth = current;
return AMConfig.AI_RETREAT_ENABLED.get()
&& tierSupplier.getAsInt() >= minTier
&& lost >= mob.getMaxHealth() * triggerHealthLoss
&& mob.getTarget() != null
&& mob.getTarget().isAlive()
&& cooldown-- <= 0;
}
@Override
public void start() {
cooldown = 110 + mob.getRandom().nextInt(90);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
mob.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, DISENGAGE_TICKS, 0));
mob.addEffect(new MobEffectInstance(MobEffects.REGENERATION, DISENGAGE_TICKS, 4));
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 5.0D);
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, speed);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "spider invisible regeneration disengage");
}
}
@@ -0,0 +1,57 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveSpiderTacticsGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private final boolean caveSpider;
private int cooldown;
public AdaptiveSpiderTacticsGoal(PathfinderMob mob, IntSupplier tierSupplier, boolean caveSpider) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.caveSpider = caveSpider;
setFlags(EnumSet.of(Flag.MOVE, Flag.JUMP, Flag.LOOK));
}
@Override
public boolean canUse() {
int tier = tierSupplier.getAsInt();
int min = caveSpider ? 2 : 3;
return tier >= min && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
cooldown = caveSpider ? 30 + mob.getRandom().nextInt(40) : AdaptiveAIGoalUtils.nextCooldown(mob);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double distance = Math.sqrt(mob.distanceToSqr(target));
if (tier >= 4) {
Vec3 elevated = AdaptivePositioningUtils.elevatedPosition(mob, target, 3.0D);
mob.getNavigation().moveTo(elevated.x, elevated.y, elevated.z, caveSpider ? 1.2D : 1.1D);
} else if (tier >= 2) {
Vec3 side = AdaptivePositioningUtils.sidePosition(mob, target, Math.max(2.0D, distance * 0.5D),
mob.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D);
mob.getNavigation().moveTo(side.x, side.y, side.z, caveSpider ? 1.2D : 1.1D);
}
if (tier >= 3 && distance < 7.0D && AdaptiveAIGoalUtils.isUsingRangedWeapon(target) && mob.onGround()) {
Vec3 leap = target.position().subtract(mob.position()).normalize().scale(caveSpider ? 0.42D : 0.36D);
mob.setDeltaMovement(mob.getDeltaMovement().add(leap.x, 0.35D, leap.z));
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "spider lunge");
}
}
}
@@ -0,0 +1,63 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Pillager;
import net.minecraft.world.entity.monster.Witch;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class AdaptiveSupportGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveSupportGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 3 && mob.getTarget() != null && mob.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
cooldown = 70 + mob.getRandom().nextInt(90);
LivingEntity target = mob.getTarget();
if (target == null) {
return;
}
if (isMeleeSupporter(mob) && target.getLastHurtMob() instanceof Mob hurt
&& (hurt instanceof AbstractSkeleton || hurt instanceof Pillager || hurt instanceof Witch)
&& mob.distanceToSqr(target) < 12.0D * 12.0D) {
mob.getNavigation().moveTo(target, 1.08D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "peeling for ranged ally");
return;
}
if ((mob instanceof AbstractSkeleton || mob instanceof Pillager || mob instanceof Witch)
&& AdaptiveTargetingUtils.nearbyAttackers(mob, target, 7.0D) > 1) {
Vec3 dest = AdaptivePositioningUtils.retreatPosition(mob, target, 3.0D);
if (AdaptivePositioningUtils.isReasonableGround(mob, dest)) {
mob.getNavigation().moveTo(dest.x, dest.y, dest.z, 0.9D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "holding behind melee pressure");
}
}
}
private static boolean isMeleeSupporter(Mob mob) {
return !(mob instanceof AbstractSkeleton) && !(mob instanceof Pillager) && !(mob instanceof Witch);
}
}
@@ -0,0 +1,60 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class AdaptiveTargetPriorityGoal extends Goal {
private final Mob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveTargetPriorityGoal(Mob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.noneOf(Flag.class));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_TARGET_PRIORITY_AI.get()
&& tierSupplier.getAsInt() >= 3
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 80 + mob.getRandom().nextInt(80);
double radius = AdaptiveAIGoalUtils.followRange(mob, 16.0D);
List<Player> players = mob.level().getEntitiesOfClass(Player.class, mob.getBoundingBox().inflate(radius),
player -> player.isAlive() && !player.isCreative() && !player.isSpectator()
&& mob.canAttack(player) && mob.hasLineOfSight(player));
Player best = null;
double bestScore = currentScore();
for (Player player : players) {
double score = AdaptiveAIGoalUtils.playerPriorityScore(mob, player);
if (score > bestScore + 1.0D) {
bestScore = score;
best = player;
}
}
if (best != null) {
mob.setTarget(best);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "priority target selected");
}
}
private double currentScore() {
if (mob.getTarget() instanceof Player player) {
return AdaptiveAIGoalUtils.playerPriorityScore(mob, player);
}
return 0.0D;
}
}
@@ -0,0 +1,46 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptiveMemory;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;
import java.util.EnumSet;
import java.util.Optional;
import java.util.function.IntSupplier;
public class AdaptiveThreatMemoryGoal extends Goal {
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
private int cooldown;
public AdaptiveThreatMemoryGoal(PathfinderMob mob, IntSupplier tierSupplier) {
this.mob = mob;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 2 && --cooldown <= 0;
}
@Override
public void start() {
cooldown = 40 + mob.getRandom().nextInt(50);
Optional<net.minecraft.server.level.ServerPlayer> remembered = AdaptiveMemory.rememberedThreat(mob);
if (remembered.isEmpty()) {
return;
}
Player player = remembered.get();
if (mob.getTarget() == null || !mob.getTarget().isAlive()) {
mob.setTarget(player);
}
if (!mob.hasLineOfSight(player) && mob.distanceToSqr(player) < 18.0D * 18.0D) {
mob.getNavigation().moveTo(player, 1.0D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "threat memory pursuit");
}
}
}
@@ -0,0 +1,60 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.warden.Warden;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveWardenTacticsGoal extends Goal {
private final Warden warden;
private final IntSupplier tierSupplier;
private int cooldown;
private LivingEntity rememberedTarget;
private LivingEntity lastHurtBy;
private int repeatedRangedTicks;
public AdaptiveWardenTacticsGoal(Warden warden, IntSupplier tierSupplier) {
this.warden = warden;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.LOOK));
}
@Override
public boolean canUse() {
return AMConfig.ENABLE_WARDEN_AI.get()
&& AMConfig.ENABLE_WARDEN_ADVANCED_AI.get()
&& AMConfig.AI_WARDEN.get()
&& tierSupplier.getAsInt() >= 4
&& --cooldown <= 0;
}
@Override
public void start() {
cooldown = 80 + warden.getRandom().nextInt(80);
LivingEntity target = warden.getTarget();
if (target != null && target.isAlive()) {
rememberedTarget = target;
warden.getLookControl().setLookAt(target, 25.0F, 25.0F);
return;
}
if (rememberedTarget != null && rememberedTarget.isAlive() && warden.distanceToSqr(rememberedTarget) < 24.0D * 24.0D) {
warden.setAttackTarget(rememberedTarget);
AdaptiveAIGoalUtils.debug(warden, tierSupplier, "warden conservative target persistence");
}
LivingEntity hurtBy = warden.getLastHurtByMob();
if (tierSupplier.getAsInt() >= 5 && hurtBy != null && hurtBy.isAlive() && AdaptiveAIGoalUtils.isUsingRangedWeapon(hurtBy)) {
repeatedRangedTicks = hurtBy == lastHurtBy ? repeatedRangedTicks + 1 : 1;
lastHurtBy = hurtBy;
if (repeatedRangedTicks >= 2) {
warden.setAttackTarget(hurtBy);
AdaptiveAIGoalUtils.debug(warden, tierSupplier, "warden repeated ranged response");
}
}
}
}
@@ -0,0 +1,150 @@
package net.fretux.adaptivemobs.ai.goals;
import net.fretux.adaptivemobs.ai.AdaptiveAIGoalUtils;
import net.fretux.adaptivemobs.ai.AdaptivePositioningUtils;
import net.fretux.adaptivemobs.ai.AdaptiveTargetingUtils;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Witch;
import net.minecraft.world.entity.projectile.ThrownPotion;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.alchemy.Potion;
import net.minecraft.world.item.alchemy.PotionUtils;
import net.minecraft.world.item.alchemy.Potions;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
import java.util.function.IntSupplier;
public class AdaptiveWitchTacticsGoal extends Goal {
private final Witch witch;
private final IntSupplier tierSupplier;
private int cooldown;
private int potionCooldown;
public AdaptiveWitchTacticsGoal(Witch witch, IntSupplier tierSupplier) {
this.witch = witch;
this.tierSupplier = tierSupplier;
setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= 2 && witch.getTarget() != null && witch.getTarget().isAlive() && --cooldown <= 0;
}
@Override
public void start() {
cooldown = 45 + witch.getRandom().nextInt(55);
potionCooldown = Math.max(0, potionCooldown - cooldown);
LivingEntity target = witch.getTarget();
if (target == null) {
return;
}
int tier = tierSupplier.getAsInt();
double dist = Math.sqrt(witch.distanceToSqr(target));
witch.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (dist < 8.0D || (tier >= 4 && AdaptiveTargetingUtils.nearbyAttackers(witch, target, 8.0D) > 0)) {
Vec3 dest = tier >= 4
? AdaptivePositioningUtils.sidePosition(witch, target, 9.0D, witch.getRandom().nextBoolean() ? 1.0D : -1.0D, 0.0D)
: AdaptivePositioningUtils.retreatPosition(witch, target, 5.0D);
witch.getNavigation().moveTo(dest.x, dest.y, dest.z, 1.0D);
AdaptiveAIGoalUtils.debug(witch, tierSupplier, "witch spacing");
}
if ((tier >= 3 && target.getDeltaMovement().horizontalDistanceSqr() > 0.05D)
|| (tier >= 4 && dist < 7.0D)
|| (tier >= 5 && AdaptiveAIGoalUtils.healthFraction(target) < 0.45D)) {
witch.performRangedAttack(target, 1.0F);
AdaptiveAIGoalUtils.debug(witch, tierSupplier, "witch extra potion opportunity");
}
if (AMConfig.ENABLE_WITCH_ADVANCED_POTION_AI.get() && potionCooldown <= 0 && !witch.isDrinkingPotion()) {
if (tryDrinkHealing(tier)) {
potionCooldown = 140 + witch.getRandom().nextInt(80);
AdaptiveAIGoalUtils.debug(witch, tierSupplier, "witch early heal");
} else if (trySupportWoundedAlly(tier)) {
potionCooldown = 120 + witch.getRandom().nextInt(80);
} else if (tryThrowSpecificPotion(target, tier, dist)) {
potionCooldown = 90 + witch.getRandom().nextInt(70);
}
}
}
private boolean tryDrinkHealing(int tier) {
double threshold = tier >= 5 ? 0.62D : tier >= 4 ? 0.52D : 0.42D;
if (AdaptiveAIGoalUtils.healthFraction(witch) > threshold) {
return false;
}
ItemStack potion = PotionUtils.setPotion(new ItemStack(Items.POTION), Potions.HEALING);
witch.setItemSlot(EquipmentSlot.MAINHAND, potion);
witch.setUsingItem(true);
return true;
}
private boolean trySupportWoundedAlly(int tier) {
if (!AMConfig.ENABLE_SUPPORT_AI.get() || tier < 3) {
return false;
}
for (Mob ally : AdaptiveTargetingUtils.nearbyHostiles(witch, 8.0D,
other -> other != witch && AdaptiveAIGoalUtils.healthFraction(other) < 0.45D)) {
if (witch.hasLineOfSight(ally)) {
throwPotion(ally, Potions.REGENERATION);
AdaptiveAIGoalUtils.debug(witch, tierSupplier, "witch support potion");
return true;
}
}
return false;
}
private boolean tryThrowSpecificPotion(LivingEntity target, int tier, double dist) {
Potion potion = null;
String debug = null;
if (tier >= 5 && AdaptiveAIGoalUtils.healthFraction(target) < 0.40D) {
potion = Potions.HARMING;
debug = "witch harming pressure";
} else if ((tier >= 4 && dist < 6.0D && isLikelyMelee(target.getMainHandItem()))
|| (tier >= 3 && dist < 7.0D && isHighDamageMelee(target.getMainHandItem()))) {
potion = Potions.WEAKNESS;
debug = "witch weakness punish";
} else if (tier >= 3 && (target.isSprinting() || target.getDeltaMovement().horizontalDistanceSqr() > 0.08D)) {
potion = Potions.SLOWNESS;
debug = "witch slowness punish";
}
if (potion == null || !witch.hasLineOfSight(target)) {
return false;
}
throwPotion(target, potion);
AdaptiveAIGoalUtils.debug(witch, tierSupplier, debug);
return true;
}
private void throwPotion(LivingEntity target, Potion potion) {
ThrownPotion thrown = new ThrownPotion(witch.level(), witch);
thrown.setItem(PotionUtils.setPotion(new ItemStack(Items.SPLASH_POTION), potion));
double dx = target.getX() + target.getDeltaMovement().x - witch.getX();
double dz = target.getZ() + target.getDeltaMovement().z - witch.getZ();
double dist = Math.sqrt(dx * dx + dz * dz);
double dy = target.getY(0.5D) - thrown.getY() + dist * 0.2D;
thrown.shoot(dx, dy, dz, 0.75F, 8.0F);
witch.level().addFreshEntity(thrown);
}
private static boolean isLikelyMelee(ItemStack stack) {
return stack.is(Items.WOODEN_SWORD) || stack.is(Items.STONE_SWORD) || stack.is(Items.IRON_SWORD)
|| stack.is(Items.GOLDEN_SWORD) || stack.is(Items.DIAMOND_SWORD) || stack.is(Items.NETHERITE_SWORD)
|| stack.is(Items.WOODEN_AXE) || stack.is(Items.STONE_AXE) || stack.is(Items.IRON_AXE)
|| stack.is(Items.GOLDEN_AXE) || stack.is(Items.DIAMOND_AXE) || stack.is(Items.NETHERITE_AXE);
}
private static boolean isHighDamageMelee(ItemStack stack) {
return stack.is(Items.DIAMOND_SWORD) || stack.is(Items.NETHERITE_SWORD)
|| stack.is(Items.IRON_AXE) || stack.is(Items.DIAMOND_AXE) || stack.is(Items.NETHERITE_AXE);
}
}
@@ -0,0 +1,52 @@
package net.fretux.adaptivemobs.ai.goals;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.phys.AABB;
import java.util.EnumSet;
import java.util.List;
import java.util.function.IntSupplier;
public class SharedTargetGoal extends Goal {
private final Mob mob;
private final IntSupplier tierSupplier;
private final int minTier;
private final double radius;
private int cooldown;
public SharedTargetGoal(Mob mob, IntSupplier tierSupplier, int minTier, double radius) {
this.mob = mob;
this.tierSupplier = tierSupplier;
this.minTier = minTier;
this.radius = radius;
setFlags(EnumSet.noneOf(Flag.class));
}
@Override
public boolean canUse() {
return tierSupplier.getAsInt() >= minTier && mob.getTarget() != null && --cooldown <= 0;
}
@Override
public void start() {
cooldown = 40 + mob.getRandom().nextInt(40);
LivingEntity target = mob.getTarget();
if (target == null || !target.isAlive()) {
return;
}
AABB box = mob.getBoundingBox().inflate(radius);
List<Mob> allies = mob.level().getEntitiesOfClass(Mob.class, box,
other -> other != mob && other instanceof Enemy && other.getTarget() == null);
int shared = 0;
for (Mob ally : allies) {
if (shared++ >= 4) {
break;
}
ally.setTarget(target);
}
}
}
@@ -0,0 +1,81 @@
package net.fretux.adaptivemobs.commands;
import com.mojang.brigadier.CommandDispatcher;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.data.AdaptiveSavedData;
import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraftforge.registries.ForgeRegistries;
import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
public final class AdaptiveMobsCommands {
private AdaptiveMobsCommands() {
}
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("adaptivemobs")
.requires(source -> source.hasPermission(2))
.then(Commands.literal("get").executes(ctx -> get(ctx.getSource())))
.then(Commands.literal("debug").executes(ctx -> debug(ctx.getSource())))
.then(Commands.literal("reset").executes(ctx -> reset(ctx.getSource())))
.then(Commands.literal("setglobaltier")
.then(Commands.argument("tier", integer(0, 5))
.executes(ctx -> setTier(ctx.getSource(), getInteger(ctx, "tier")))))
.then(Commands.literal("getmob")
.then(Commands.argument("target", EntityArgument.entity())
.executes(ctx -> getMob(ctx.getSource(), EntityArgument.getEntity(ctx, "target").getType())))));
}
private static int get(CommandSourceStack source) {
ServerLevel level = source.getLevel();
AdaptiveSavedData data = AdaptiveSavedData.get(level);
source.sendSuccess(() -> Component.literal("Adaptive Mobs: global tier "
+ AdaptiveDifficultyManager.getGlobalTier(level)
+ ", day " + AdaptiveDifficultyManager.getWorldDay(level)
+ ", hostile kills " + data.getTotalHostileKills()
+ ", manual override " + (data.getManualGlobalTier() >= 0 ? data.getManualGlobalTier() : "off")), false);
return 1;
}
private static int getMob(CommandSourceStack source, EntityType<?> type) {
AdaptiveSavedData data = AdaptiveSavedData.get(source.getLevel());
ResourceLocation key = ForgeRegistries.ENTITY_TYPES.getKey(type);
String typeKey = key == null ? type.toString() : key.toString();
source.sendSuccess(() -> Component.literal("Adaptive Mobs: " + typeKey + " kills "
+ data.getKillsForType(typeKey)), false);
return 1;
}
private static int setTier(CommandSourceStack source, int tier) {
AdaptiveSavedData.get(source.getLevel()).setManualGlobalTier(Math.min(tier, AMConfig.maxTier));
source.sendSuccess(() -> Component.literal("Adaptive Mobs: manual global tier set to " + tier), true);
return 1;
}
private static int reset(CommandSourceStack source) {
AdaptiveSavedData.get(source.getLevel()).resetProgression();
source.sendSuccess(() -> Component.literal("Adaptive Mobs: progression reset"), true);
return 1;
}
private static int debug(CommandSourceStack source) {
ServerLevel level = source.getLevel();
AdaptiveSavedData data = AdaptiveSavedData.get(level);
source.sendSuccess(() -> Component.literal("Adaptive Mobs debug: enabled=" + AMConfig.enabled
+ ", configMaxTier=" + AMConfig.maxTier
+ ", worldDay=" + AdaptiveDifficultyManager.getWorldDay(level)
+ ", totalKills=" + data.getTotalHostileKills()
+ ", storedTypes=" + data.getKillsByType().size()
+ ", dataVersion=" + data.getVersion()), false);
return 1;
}
}
@@ -0,0 +1,318 @@
package net.fretux.adaptivemobs.config;
import net.fretux.adaptivemobs.AdaptiveMobs;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.config.ModConfigEvent;
import net.minecraft.world.entity.EntityType;
import java.util.List;
/**
* Common (server-side gameplay) configuration for Adaptive Mobs.
*
* <p>All tier-indexed lists are 5 entries long and represent tiers 1..5. Tier 0 always means
* "vanilla / no change", so the accessors below return a neutral value for tier 0.</p>
*/
@Mod.EventBusSubscriber(modid = AdaptiveMobs.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
public final class AMConfig {
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
// --- General -----------------------------------------------------------
public static final ForgeConfigSpec.BooleanValue ENABLE_ADAPTIVE_MOBS = BUILDER
.comment("Master switch. When false the mod does nothing to mobs.")
.define("general.enableAdaptiveMobs", true);
public static final ForgeConfigSpec.IntValue MAX_TIER = BUILDER
.comment("Highest adaptive tier that can ever be reached (0-5).")
.defineInRange("general.maxTier", 5, 0, 5);
public static final ForgeConfigSpec.BooleanValue DEBUG_LOGGING = BUILDER
.comment("Verbose debug logging for progression, scaling and spawns.")
.define("general.debugLogging", false);
// --- Per-mob toggles ---------------------------------------------------
public static final ForgeConfigSpec.BooleanValue ENABLE_ZOMBIES = BUILDER.define("mobs.zombies", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SKELETONS = BUILDER.define("mobs.skeletons", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_CREEPERS = BUILDER.define("mobs.creepers", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SPIDERS = BUILDER.define("mobs.spiders", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_WITCHES = BUILDER.define("mobs.witches", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_PILLAGERS = BUILDER.define("mobs.pillagers", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_OTHER_HOSTILES = BUILDER
.comment("Apply generic progression/scaling/spawn pressure to hostile mobs without a specific toggle.")
.define("mobs.otherHostiles", true);
// --- Progression thresholds -------------------------------------------
public static final ForgeConfigSpec.ConfigValue<List<? extends Integer>> DAY_THRESHOLDS = BUILDER
.comment("World day required to reach tiers 1..5 (whichever of day/kills hits first wins).")
.defineList("progression.dayThresholds", List.of(5, 15, 30, 60, 100),
o -> o instanceof Integer i && i >= 0);
public static final ForgeConfigSpec.ConfigValue<List<? extends Integer>> KILL_THRESHOLDS = BUILDER
.comment("Total hostile kills required to reach tiers 1..5.")
.defineList("progression.killThresholds", List.of(25, 100, 250, 600, 1200),
o -> o instanceof Integer i && i >= 0);
public static final ForgeConfigSpec.IntValue MOB_TYPE_KILL_BONUS_THRESHOLD = BUILDER
.comment("Per-mob-type kills that grant +1 tier to that mob type (set 0 to disable per-type scaling).")
.defineInRange("progression.perTypeKillsPerTier", 150, 0, Integer.MAX_VALUE);
// --- Stat scaling ------------------------------------------------------
public static final ForgeConfigSpec.BooleanValue STAT_SCALING_ENABLED = BUILDER
.comment("Enable attribute (health/damage/speed/etc.) scaling.")
.define("scaling.enabled", true);
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> HEALTH_SCALE = BUILDER
.comment("Extra max-health fraction per tier 1..5 (0.10 = +10%).")
.defineList("scaling.healthPerTier", List.of(0.10, 0.22, 0.40, 0.60, 0.95),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> DAMAGE_SCALE = BUILDER
.comment("Extra attack-damage fraction per tier 1..5.")
.defineList("scaling.damagePerTier", List.of(0.06, 0.12, 0.24, 0.38, 0.60),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> SPEED_SCALE = BUILDER
.comment("Extra movement-speed fraction per tier 1..5 (kept small on purpose).")
.defineList("scaling.speedPerTier", List.of(0.0, 0.0, 0.01, 0.04, 0.07),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> FOLLOW_RANGE_SCALE = BUILDER
.comment("Extra follow-range fraction per tier 1..5.")
.defineList("scaling.followRangePerTier", List.of(0.0, 0.10, 0.10, 0.15, 0.25),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> ARMOR_BONUS = BUILDER
.comment("Flat armor points added per tier 1..5.")
.defineList("scaling.armorPerTier", List.of(0.0, 1.0, 2.5, 4.0, 6.0),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> KNOCKBACK_RESIST = BUILDER
.comment("Flat knockback-resistance added per tier 1..5 (0.0-1.0 scale).")
.defineList("scaling.knockbackResistPerTier", List.of(0.0, 0.0, 0.02, 0.08, 0.18),
o -> isNumberInRange(o, 0.0, Double.MAX_VALUE));
// --- Gear scaling ------------------------------------------------------
public static final ForgeConfigSpec.BooleanValue GEAR_SCALING_ENABLED = BUILDER
.comment("Enable upgrading vanilla gear on eligible mobs (zombies, skeletons, etc.).")
.define("gear.enabled", true);
public static final ForgeConfigSpec.DoubleValue GEAR_ENCHANT_CHANCE_PER_TIER = BUILDER
.comment("Base enchant chance multiplier applied per tier when gear scaling rolls (kept low).")
.defineInRange("gear.enchantChancePerTier", 0.08, 0.0, 1.0);
// --- Spawn pressure ----------------------------------------------------
public static final ForgeConfigSpec.BooleanValue SPAWN_PRESSURE_ENABLED = BUILDER
.comment("Enable occasional extra natural spawns of the same mob type.")
.define("spawnPressure.enabled", true);
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> EXTRA_SPAWN_CHANCE = BUILDER
.comment("Chance to spawn ONE extra mob on a natural spawn, per tier 1..5.")
.defineList("spawnPressure.extraChancePerTier", List.of(0.05, 0.10, 0.17, 0.25, 0.40),
o -> isNumberInRange(o, 0.0, 1.0));
public static final ForgeConfigSpec.IntValue SPAWN_DENSITY_RADIUS = BUILDER
.comment("Radius (blocks) checked for nearby mobs before adding an extra spawn.")
.defineInRange("spawnPressure.densityRadius", 12, 2, 64);
public static final ForgeConfigSpec.IntValue SPAWN_DENSITY_CAP = BUILDER
.comment("If at least this many mobs are already within the density radius, skip the extra spawn.")
.defineInRange("spawnPressure.densityCap", 6, 1, 64);
// --- AI ----------------------------------------------------------------
public static final ForgeConfigSpec.BooleanValue AI_ENABLED = BUILDER
.comment("Master switch for all adaptive AI behaviour.")
.define("ai.enabled", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_ADVANCED_AI = BUILDER
.comment("Enable expanded adaptive AI goals beyond basic flanking/pressure behaviour.")
.define("ai.enableAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_RANGED_AI = BUILDER.define("ai.enableRangedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_MELEE_AI = BUILDER.define("ai.enableMeleeAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_CREEPER_AI = BUILDER.define("ai.enableCreeperAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SPIDER_AI = BUILDER.define("ai.enableSpiderAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_WITCH_AI = BUILDER.define("ai.enableWitchAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_PILLAGER_AI = BUILDER.define("ai.enablePillagerAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_ENDERMAN_AI = BUILDER.define("ai.enableEndermanAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_NETHER_MOB_AI = BUILDER.define("ai.enableNetherMobAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_AQUATIC_HOSTILE_AI = BUILDER.define("ai.enableAquaticHostileAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_RAID_MOB_AI = BUILDER.define("ai.enableRaidMobAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_END_MOB_AI = BUILDER.define("ai.enableEndMobAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_WARDEN_AI = BUILDER.define("ai.enableWardenAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_ENDERMITE_AI = BUILDER.define("ai.enableEndermiteAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_EVOKER_AI = BUILDER.define("ai.enableEvokerAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_WITCH_ADVANCED_POTION_AI = BUILDER.define("ai.enableWitchAdvancedPotionAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_GUARDIAN_ADVANCED_AI = BUILDER.define("ai.enableGuardianAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SHULKER_ADVANCED_AI = BUILDER.define("ai.enableShulkerAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SLIME_ADVANCED_AI = BUILDER.define("ai.enableSlimeAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_MAGMA_CUBE_ADVANCED_AI = BUILDER.define("ai.enableMagmaCubeAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_RAVAGER_ADVANCED_AI = BUILDER.define("ai.enableRavagerAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_WARDEN_ADVANCED_AI = BUILDER
.comment("Very conservative Warden behaviour tweaks only; does not change default speed, damage or sonic boom frequency.")
.define("ai.enableWardenAdvancedAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_MORALE_AI = BUILDER
.comment("Allow small group morale adjustments: bolder groups, defensive low-health isolated mobs.")
.define("ai.enableMoraleAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_TARGET_PRIORITY_AI = BUILDER
.comment("Allow mobs to occasionally retarget vulnerable or committed players.")
.define("ai.enableTargetPriorityAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_ANTI_CHEESE_AI = BUILDER
.comment("Allow small tactical responses to common cheese patterns without hard counters.")
.define("ai.enableAntiCheeseAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_DARK_APPROACH_AI = BUILDER
.comment("Prefer darker approach positions when an adaptive movement goal has comparable options.")
.define("ai.enableDarkApproachAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_THREAT_MEMORY_AI = BUILDER
.comment("Remember recent player attackers briefly to reduce trivial line-of-sight aggro resets.")
.define("ai.enableThreatMemoryAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SOUND_INVESTIGATION_AI = BUILDER
.comment("Tier 2+ mobs can investigate nearby recent block breaking, eating, bow shots and explosions.")
.define("ai.enableSoundInvestigationAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_SUPPORT_AI = BUILDER
.comment("Allow small ally support behaviours such as peeling and ranged mobs holding behind melee pressure.")
.define("ai.enableSupportAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_ESCAPE_DENIAL_AI = BUILDER
.comment("Allow zombies and spiders to imperfectly bias toward likely player escape routes.")
.define("ai.enableEscapeDenialAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_BIOME_TACTICS_AI = BUILDER
.comment("Allow small dimension/terrain-specific tactics for Nether mobs, drowned, spiders and skeletons.")
.define("ai.enableBiomeTacticsAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_RANGED_COVER_AI = BUILDER
.comment("Allow ranged mobs to sometimes move toward imperfect cover after attacking.")
.define("ai.enableRangedCoverAI", true);
public static final ForgeConfigSpec.BooleanValue ENABLE_BOSS_AI = BUILDER
.comment("Future boss AI support. Ender Dragon and Wither are not altered unless this is enabled.")
.define("ai.enableBossAI", false);
public static final ForgeConfigSpec.BooleanValue AI_SKELETON = BUILDER.define("ai.skeletons", true);
public static final ForgeConfigSpec.BooleanValue AI_ZOMBIE = BUILDER.define("ai.zombies", true);
public static final ForgeConfigSpec.BooleanValue AI_CREEPER = BUILDER.define("ai.creepers", true);
public static final ForgeConfigSpec.BooleanValue AI_SPIDER = BUILDER.define("ai.spiders", true);
public static final ForgeConfigSpec.BooleanValue AI_WITCH = BUILDER.define("ai.witches", true);
public static final ForgeConfigSpec.BooleanValue AI_PILLAGER = BUILDER.define("ai.pillagers", true);
public static final ForgeConfigSpec.BooleanValue AI_ENDERMAN = BUILDER.define("ai.endermen", true);
public static final ForgeConfigSpec.BooleanValue AI_SLIME = BUILDER.define("ai.slimes", true);
public static final ForgeConfigSpec.BooleanValue AI_GHAST = BUILDER.define("ai.ghasts", true);
public static final ForgeConfigSpec.BooleanValue AI_BLAZE = BUILDER.define("ai.blazes", true);
public static final ForgeConfigSpec.BooleanValue AI_GUARDIAN = BUILDER.define("ai.guardians", true);
public static final ForgeConfigSpec.BooleanValue AI_PHANTOM = BUILDER.define("ai.phantoms", true);
public static final ForgeConfigSpec.BooleanValue AI_RAVAGER = BUILDER.define("ai.ravagers", true);
public static final ForgeConfigSpec.BooleanValue AI_EVOKER = BUILDER.define("ai.evokers", true);
public static final ForgeConfigSpec.BooleanValue AI_VEX = BUILDER.define("ai.vexes", true);
public static final ForgeConfigSpec.BooleanValue AI_PIGLIN_FAMILY = BUILDER.define("ai.piglinFamily", true);
public static final ForgeConfigSpec.BooleanValue AI_SMALL_SWARM = BUILDER.define("ai.silverfishAndEndermites", true);
public static final ForgeConfigSpec.BooleanValue AI_SHULKER = BUILDER.define("ai.shulkers", true);
public static final ForgeConfigSpec.BooleanValue AI_WARDEN = BUILDER.define("ai.warden", true);
public static final ForgeConfigSpec.IntValue AI_SCAN_COOLDOWN_MIN = BUILDER
.comment("Minimum cooldown in ticks for adaptive AI scans.")
.defineInRange("ai.scanCooldownMin", 20, 5, 200);
public static final ForgeConfigSpec.IntValue AI_SCAN_COOLDOWN_MAX = BUILDER
.comment("Maximum cooldown in ticks for adaptive AI scans.")
.defineInRange("ai.scanCooldownMax", 60, 10, 400);
public static final ForgeConfigSpec.IntValue AI_MAX_NEARBY_MOBS_SCANNED = BUILDER
.comment("Maximum nearby mobs considered by each adaptive AI scan.")
.defineInRange("ai.maxNearbyMobsScanned", 8, 1, 32);
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> AI_ACCURACY_BONUS = BUILDER
.comment("Projectile aim correction strength per tier 1..5. Small values preserve dodge counterplay.")
.defineList("ai.accuracyBonusPerTier", List.of(0.0, 0.04, 0.08, 0.12, 0.16),
o -> isNumberInRange(o, 0.0, 0.5));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> AI_HEADSHOT_CHANCE = BUILDER
.comment("Chance per tier 1..5 for skeleton-like archers to bias aim upward.")
.defineList("ai.headshotChancePerTier", List.of(0.0, 0.0, 0.05, 0.18, 0.25),
o -> isNumberInRange(o, 0.0, 1.0));
public static final ForgeConfigSpec.ConfigValue<List<? extends Number>> AI_INTERCEPTION_STRENGTH = BUILDER
.comment("Movement prediction strength per tier 1..5.")
.defineList("ai.interceptionStrengthPerTier", List.of(0.0, 0.0, 0.06, 0.14, 0.24),
o -> isNumberInRange(o, 0.0, 1.0));
public static final ForgeConfigSpec.BooleanValue AI_RETREAT_ENABLED = BUILDER
.comment("Allow brief retreat/re-engage behaviour for mobs that support it.")
.define("ai.retreatBehaviorEnabled", true);
public static final ForgeConfigSpec.BooleanValue DEBUG_AI_LOGGING = BUILDER
.comment("Verbose debug logging for adaptive AI injection and special behaviour.")
.define("ai.debugAILogging", false);
public static final ForgeConfigSpec.BooleanValue ALLOW_ZOMBIE_BLOCK_BREAK = BUILDER
.comment("If true, allow extra block-breaking behaviour for zombies (off by default; vanilla door behaviour is unaffected).")
.define("ai.allowZombieBlockBreak", false);
public static final ForgeConfigSpec SPEC = BUILDER.build();
// --- Cached values -----------------------------------------------------
public static volatile boolean enabled = true;
public static volatile int maxTier = 5;
public static volatile boolean debug = false;
private AMConfig() {
}
@SubscribeEvent
static void onLoad(final ModConfigEvent event) {
if (event.getConfig().getSpec() != SPEC) {
return;
}
enabled = ENABLE_ADAPTIVE_MOBS.get();
maxTier = MAX_TIER.get();
debug = DEBUG_LOGGING.get();
}
/** Reads tier-indexed list (tiers 1..5) returning neutral 0 for tier 0 or out-of-range. */
public static double tierValue(List<? extends Number> list, int tier) {
if (tier <= 0 || list == null || list.isEmpty()) {
return 0.0;
}
int idx = Math.min(tier, list.size()) - 1;
return list.get(idx).doubleValue();
}
public static int tierValueInt(List<? extends Integer> list, int tier1based) {
if (list == null || list.isEmpty()) {
return Integer.MAX_VALUE;
}
int idx = Math.min(tier1based, list.size()) - 1;
idx = Math.max(0, idx);
return list.get(idx);
}
public static int aiScanCooldown(net.minecraft.util.RandomSource random) {
int min = AI_SCAN_COOLDOWN_MIN.get();
int max = Math.max(min, AI_SCAN_COOLDOWN_MAX.get());
return min + random.nextInt(max - min + 1);
}
public static boolean isMobEnabled(EntityType<?> type) {
if (type == EntityType.ENDER_DRAGON || type == EntityType.WITHER) {
return ENABLE_BOSS_AI.get();
}
if (type == EntityType.ZOMBIE || type == EntityType.HUSK || type == EntityType.DROWNED
|| type == EntityType.ZOMBIE_VILLAGER || type == EntityType.ZOMBIFIED_PIGLIN) {
return ENABLE_ZOMBIES.get();
}
if (type == EntityType.SKELETON || type == EntityType.STRAY || type == EntityType.WITHER_SKELETON) {
return ENABLE_SKELETONS.get();
}
if (type == EntityType.CREEPER) {
return ENABLE_CREEPERS.get();
}
if (type == EntityType.SPIDER || type == EntityType.CAVE_SPIDER) {
return ENABLE_SPIDERS.get();
}
if (type == EntityType.WITCH) {
return ENABLE_WITCHES.get();
}
if (type == EntityType.PILLAGER) {
return ENABLE_PILLAGERS.get();
}
return ENABLE_OTHER_HOSTILES.get();
}
private static boolean isNumberInRange(Object value, double min, double max) {
if (!(value instanceof Number number)) {
return false;
}
double asDouble = number.doubleValue();
return Double.isFinite(asDouble) && asDouble >= min && asDouble <= max;
}
}
@@ -0,0 +1,103 @@
package net.fretux.adaptivemobs.data;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
import java.util.HashMap;
import java.util.Map;
/**
* World-scoped progression state. Stored on the overworld's data storage so it acts as a single
* global value for the whole server, regardless of which dimension a kill happened in.
*/
public class AdaptiveSavedData extends SavedData {
public static final String DATA_NAME = "adaptivemobs_progression";
private static final int CURRENT_VERSION = 1;
private long totalHostileKills;
private final Map<String, Integer> killsByType = new HashMap<>();
private int manualGlobalTier = -1; // -1 = no manual override
private int version = CURRENT_VERSION;
public AdaptiveSavedData() {
}
/** Resolves the single global instance, always backed by the overworld's storage. */
public static AdaptiveSavedData get(ServerLevel level) {
ServerLevel overworld = level.getServer().overworld();
return overworld.getDataStorage().computeIfAbsent(AdaptiveSavedData::load, AdaptiveSavedData::new, DATA_NAME);
}
public static AdaptiveSavedData load(CompoundTag tag) {
AdaptiveSavedData data = new AdaptiveSavedData();
data.totalHostileKills = tag.getLong("totalHostileKills");
data.manualGlobalTier = tag.contains("manualGlobalTier") ? tag.getInt("manualGlobalTier") : -1;
data.version = tag.contains("version") ? tag.getInt("version") : CURRENT_VERSION;
CompoundTag byType = tag.getCompound("killsByType");
for (String key : byType.getAllKeys()) {
data.killsByType.put(key, byType.getInt(key));
}
return data;
}
@Override
public CompoundTag save(CompoundTag tag) {
tag.putLong("totalHostileKills", totalHostileKills);
tag.putInt("manualGlobalTier", manualGlobalTier);
tag.putInt("version", version);
CompoundTag byType = new CompoundTag();
killsByType.forEach(byType::putInt);
tag.put("killsByType", byType);
return tag;
}
// --- Accessors ---------------------------------------------------------
public long getTotalHostileKills() {
return totalHostileKills;
}
public int getKillsForType(String typeKey) {
return killsByType.getOrDefault(typeKey, 0);
}
public Map<String, Integer> getKillsByType() {
return killsByType;
}
public int getManualGlobalTier() {
return manualGlobalTier;
}
public int getVersion() {
return version;
}
// --- Mutators (mark dirty) --------------------------------------------
public void addKill(String typeKey) {
totalHostileKills++;
killsByType.merge(typeKey, 1, Integer::sum);
setDirty();
}
public void setManualGlobalTier(int tier) {
this.manualGlobalTier = tier;
setDirty();
}
public void clearManualGlobalTier() {
this.manualGlobalTier = -1;
setDirty();
}
public void resetProgression() {
totalHostileKills = 0;
killsByType.clear();
manualGlobalTier = -1;
version = CURRENT_VERSION;
setDirty();
}
}
@@ -0,0 +1,110 @@
package net.fretux.adaptivemobs.difficulty;
import net.fretux.adaptivemobs.AdaptiveMobs;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.data.AdaptiveSavedData;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraftforge.registries.ForgeRegistries;
/**
* Central authority for "how hard should mobs be right now".
*
* <p>The global tier is the dominant signal (world day + total hostile kills). A per-mob-type
* bonus can nudge specific mob families up once players have killed a lot of them.</p>
*/
public final class AdaptiveDifficultyManager {
private AdaptiveDifficultyManager() {
}
/** Current world day, derived from the overworld day-time. */
public static long getWorldDay(ServerLevel level) {
return level.getServer().overworld().getDayTime() / 24000L;
}
/** Global progression tier in the range [0, maxTier]. */
public static int getGlobalTier(ServerLevel level) {
if (!AMConfig.enabled) {
return 0;
}
AdaptiveSavedData data = AdaptiveSavedData.get(level);
int max = AMConfig.maxTier;
if (data.getManualGlobalTier() >= 0) {
return clamp(data.getManualGlobalTier(), max);
}
int dayTier = tierFromDay(getWorldDay(level));
int killTier = tierFromKills(data.getTotalHostileKills());
return clamp(Math.max(dayTier, killTier), max);
}
/** Effective tier for a specific mob type (global tier plus an optional per-type bonus). */
public static int getMobTier(ServerLevel level, EntityType<?> type) {
if (!AMConfig.enabled) {
return 0;
}
int global = getGlobalTier(level);
int perTypeThreshold = AMConfig.MOB_TYPE_KILL_BONUS_THRESHOLD.get();
if (perTypeThreshold <= 0) {
return global;
}
ResourceLocation key = ForgeRegistries.ENTITY_TYPES.getKey(type);
if (key == null) {
return global;
}
int typeKills = AdaptiveSavedData.get(level).getKillsForType(key.toString());
int bonus = typeKills / perTypeThreshold;
return clamp(global + bonus, AMConfig.maxTier);
}
/** Records a player kill if the victim is a hostile mob. */
public static void recordKill(ServerPlayer player, LivingEntity killed) {
if (!AMConfig.enabled || player == null || killed == null) {
return;
}
if (!(killed instanceof Enemy)) {
return; // only count hostile mobs
}
ServerLevel level = player.serverLevel();
ResourceLocation key = ForgeRegistries.ENTITY_TYPES.getKey(killed.getType());
if (key == null) {
return;
}
AdaptiveSavedData data = AdaptiveSavedData.get(level);
data.addKill(key.toString());
if (AMConfig.debug) {
AdaptiveMobs.LOGGER.info("[AdaptiveMobs] Recorded kill of {} (total={}, type={}, globalTier={})",
key, data.getTotalHostileKills(), data.getKillsForType(key.toString()), getGlobalTier(level));
}
}
private static int tierFromDay(long day) {
int tier = 0;
for (int t = 1; t <= 5; t++) {
if (day >= AMConfig.tierValueInt(AMConfig.DAY_THRESHOLDS.get(), t)) {
tier = t;
}
}
return tier;
}
private static int tierFromKills(long kills) {
int tier = 0;
for (int t = 1; t <= 5; t++) {
if (kills >= AMConfig.tierValueInt(AMConfig.KILL_THRESHOLDS.get(), t)) {
tier = t;
}
}
return tier;
}
private static int clamp(int tier, int max) {
return Math.max(0, Math.min(tier, max));
}
}
@@ -0,0 +1,129 @@
package net.fretux.adaptivemobs.event;
import net.fretux.adaptivemobs.ai.AdaptiveGoalInjector;
import net.fretux.adaptivemobs.ai.AdaptiveMemory;
import net.fretux.adaptivemobs.ai.AdaptiveMobHooks;
import net.fretux.adaptivemobs.ai.AdaptiveSpecialAbilities;
import net.fretux.adaptivemobs.commands.AdaptiveMobsCommands;
import net.fretux.adaptivemobs.config.AMConfig;
import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager;
import net.fretux.adaptivemobs.gear.GearScaler;
import net.fretux.adaptivemobs.scaling.MobStatScaler;
import net.fretux.adaptivemobs.spawn.SpawnPressureHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
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.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.living.MobSpawnEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.event.level.ExplosionEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
public class AdaptiveMobsEvents {
@SubscribeEvent
public void onCommands(RegisterCommandsEvent event) {
AdaptiveMobsCommands.register(event.getDispatcher());
}
@SubscribeEvent
public void onLivingDeath(LivingDeathEvent event) {
LivingEntity killed = event.getEntity();
if (!(event.getSource().getEntity() instanceof ServerPlayer player)) {
return;
}
AdaptiveDifficultyManager.recordKill(player, killed);
}
@SubscribeEvent
public void onLivingHurt(LivingHurtEvent event) {
AdaptiveSpecialAbilities.onLivingHurt(event);
if (!AMConfig.enabled || !AMConfig.ENABLE_THREAT_MEMORY_AI.get()) {
return;
}
if (event.getEntity() instanceof Mob mob && mob instanceof Enemy
&& event.getSource().getEntity() instanceof ServerPlayer player) {
AdaptiveMemory.rememberThreat(mob, player, 20 * 12);
}
}
@SubscribeEvent
public void onBlockBreak(BlockEvent.BreakEvent event) {
if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get()
&& event.getLevel() instanceof ServerLevel level) {
AdaptiveMemory.rememberSound(level, event.getPos(), AdaptiveMemory.SoundKind.BLOCK_BREAK, 20 * 8);
}
}
@SubscribeEvent
public void onItemUseFinish(LivingEntityUseItemEvent.Finish event) {
if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get()
&& event.getEntity() instanceof ServerPlayer player && event.getItem().isEdible()) {
AdaptiveMemory.rememberSound(player.serverLevel(), player.blockPosition(), AdaptiveMemory.SoundKind.EATING, 20 * 6);
}
}
@SubscribeEvent
public void onFinalizeSpawn(MobSpawnEvent.FinalizeSpawn event) {
if (!AMConfig.enabled || !(event.getLevel() instanceof ServerLevel level)) {
return;
}
Mob mob = event.getEntity();
if (!(mob instanceof Enemy) || !AMConfig.isMobEnabled(mob.getType())) {
return;
}
int tier = AdaptiveDifficultyManager.getMobTier(level, mob.getType());
MobStatScaler.applyScaling(mob, tier);
GearScaler.applyGear(mob, tier);
AdaptiveGoalInjector.inject(mob, level, tier);
SpawnPressureHelper.maybeSpawnExtra(level, mob, tier, event.getSpawnType());
}
@SubscribeEvent
public void onEntityJoinLevel(EntityJoinLevelEvent event) {
AdaptiveMobHooks.onProjectileJoin(event);
AdaptiveSpecialAbilities.onProjectileJoin(event);
if (!(event.getLevel() instanceof ServerLevel level)) {
return;
}
Entity entity = event.getEntity();
if (AMConfig.enabled && entity instanceof Mob mob && mob instanceof Enemy && AMConfig.isMobEnabled(mob.getType())) {
AdaptiveGoalInjector.inject(mob, level, AdaptiveDifficultyManager.getMobTier(level, mob.getType()));
}
if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get()
&& entity instanceof Projectile projectile && projectile.getOwner() instanceof ServerPlayer player) {
AdaptiveMemory.rememberSound(level, player.blockPosition(), AdaptiveMemory.SoundKind.BOW_SHOT, 20 * 6);
}
}
@SubscribeEvent
public void onLevelUnload(LevelEvent.Unload event) {
if (event.getLevel() instanceof ServerLevel level) {
AdaptiveMemory.clear(level);
}
}
@SubscribeEvent
public void onExplosion(ExplosionEvent.Detonate 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);
}
}
@SubscribeEvent
public void onServerTick(TickEvent.ServerTickEvent event) {
AdaptiveSpecialAbilities.onServerTick(event);
}
}
@@ -0,0 +1,178 @@
package net.fretux.adaptivemobs.gear;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.AbstractSkeleton;
import net.minecraft.world.entity.monster.Pillager;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
/**
* Upgrades a mob's vanilla equipment based on its tier. Only vanilla items are used, armor is
* partial more often than full, existing/better gear is respected (never downgraded), and added
* gear is given a 0% drop chance so it stays a challenge rather than a farm.
*/
public final class GearScaler {
// Material ranks (low -> high) used both to pick new gear and to compare against existing gear.
private static final Item[] HELMETS = {Items.LEATHER_HELMET, Items.GOLDEN_HELMET, Items.CHAINMAIL_HELMET, Items.IRON_HELMET, Items.DIAMOND_HELMET};
private static final Item[] CHESTS = {Items.LEATHER_CHESTPLATE, Items.GOLDEN_CHESTPLATE, Items.CHAINMAIL_CHESTPLATE, Items.IRON_CHESTPLATE, Items.DIAMOND_CHESTPLATE};
private static final Item[] LEGS = {Items.LEATHER_LEGGINGS, Items.GOLDEN_LEGGINGS, Items.CHAINMAIL_LEGGINGS, Items.IRON_LEGGINGS, Items.DIAMOND_LEGGINGS};
private static final Item[] BOOTS = {Items.LEATHER_BOOTS, Items.GOLDEN_BOOTS, Items.CHAINMAIL_BOOTS, Items.IRON_BOOTS, Items.DIAMOND_BOOTS};
private static final Item[] SWORDS = {Items.WOODEN_SWORD, Items.GOLDEN_SWORD, Items.STONE_SWORD, Items.IRON_SWORD, Items.DIAMOND_SWORD};
private GearScaler() {
}
public static void applyGear(Mob mob, int tier) {
if (!AMConfig.GEAR_SCALING_ENABLED.get() || tier <= 0) {
return;
}
RandomSource random = mob.getRandom();
if (mob instanceof AbstractSkeleton skeleton) {
applyArmor(skeleton, tier, random);
upgradeBow(skeleton, tier, random);
if (!skeleton.getMainHandItem().is(Items.BOW)) {
upgradeMeleeWeapon(skeleton, tier, random);
}
} else if (mob instanceof Zombie zombie) {
// Covers zombies, husks, drowned and zombified piglins (all extend Zombie).
applyArmor(zombie, tier, random);
upgradeMeleeWeapon(zombie, tier, random);
} else if (mob instanceof Pillager pillager) {
upgradeCrossbow(pillager, tier, random);
}
// TODO: Piglin gear is intentionally left alone to avoid disturbing bartering behaviour.
}
// --- Armor -------------------------------------------------------------
private static void applyArmor(Mob mob, int tier, RandomSource random) {
// Partial armor more often than full: each slot rolled independently.
double perSlotChance = tier >= 5 ? 0.78D : 0.16D + 0.11D * tier; // tier1 ~0.27 .. tier4 ~0.60
tryArmorSlot(mob, EquipmentSlot.HEAD, HELMETS, tier, perSlotChance, random);
tryArmorSlot(mob, EquipmentSlot.CHEST, CHESTS, tier, perSlotChance, random);
tryArmorSlot(mob, EquipmentSlot.LEGS, LEGS, tier, perSlotChance, random);
tryArmorSlot(mob, EquipmentSlot.FEET, BOOTS, tier, perSlotChance, random);
}
private static void tryArmorSlot(Mob mob, EquipmentSlot slot, Item[] ladder, int tier,
double chance, RandomSource random) {
if (random.nextDouble() > chance) {
return;
}
int newLevel = rollMaterialLevel(tier, random);
int existingLevel = materialLevel(mob.getItemBySlot(slot).getItem(), ladder);
if (existingLevel >= newLevel) {
return; // respect existing/better gear
}
ItemStack stack = new ItemStack(ladder[newLevel]);
maybeEnchant(stack, Enchantments.ALL_DAMAGE_PROTECTION, tier, random);
equip(mob, slot, stack);
}
// --- Weapons -----------------------------------------------------------
private static void upgradeMeleeWeapon(Mob mob, int tier, RandomSource random) {
ItemStack current = mob.getMainHandItem();
int existingLevel = materialLevel(current.getItem(), SWORDS);
if (!current.isEmpty() && existingLevel < 0) {
return; // holding something we don't recognise (e.g. a trident) — leave it alone
}
// Zombies trend towards iron at high tiers; bias the ladder a little lower than armor.
int newLevel = Math.max(0, rollMaterialLevel(tier, random) - (tier >= 4 ? 0 : 1));
if (existingLevel >= newLevel && !current.isEmpty()) {
return;
}
ItemStack sword = new ItemStack(SWORDS[newLevel]);
maybeEnchant(sword, Enchantments.SHARPNESS, tier, random);
if (tier >= 5 && random.nextDouble() < 0.18D) {
sword.enchant(Enchantments.FIRE_ASPECT, 1);
}
equip(mob, EquipmentSlot.MAINHAND, sword);
}
private static void upgradeBow(AbstractSkeleton skeleton, int tier, RandomSource random) {
ItemStack bow = skeleton.getMainHandItem();
if (!bow.is(Items.BOW)) {
return; // wither skeletons hold swords etc. — handled by melee logic if desired
}
// Don't replace the bow item (no custom items); just occasionally enchant it at higher tiers.
if (tier >= 3) {
maybeEnchant(bow, Enchantments.POWER_ARROWS, tier, random);
}
if (tier >= 5 && random.nextDouble() < 0.35D) {
bow.enchant(Enchantments.PUNCH_ARROWS, 1);
}
if (tier >= 5 && random.nextDouble() < 0.22D) {
bow.enchant(Enchantments.FLAMING_ARROWS, 1);
}
}
private static void upgradeCrossbow(Pillager pillager, int tier, RandomSource random) {
ItemStack crossbow = pillager.getMainHandItem();
if (!crossbow.is(Items.CROSSBOW)) {
return;
}
if (tier >= 3) {
maybeEnchant(crossbow, Enchantments.QUICK_CHARGE, tier, random);
}
if (tier >= 5 && random.nextDouble() < 0.30D) {
crossbow.enchant(Enchantments.MULTISHOT, 1);
} else if (tier >= 5 && random.nextDouble() < 0.35D) {
crossbow.enchant(Enchantments.PIERCING, 2);
}
}
// --- Helpers -----------------------------------------------------------
/** Picks a material rank (0=leather/wood .. 4=diamond) appropriate for the tier. */
private static int rollMaterialLevel(int tier, RandomSource random) {
double r = random.nextDouble();
return switch (tier) {
case 1 -> r < 0.70 ? 0 : 1; // leather / gold
case 2 -> r < 0.40 ? 0 : (r < 0.80 ? 2 : 1); // leather / chain / gold
case 3 -> r < 0.30 ? 0 : (r < 0.65 ? 2 : 3); // leather / chain / iron
case 4 -> r < 0.12 ? 4 : (r < 0.72 ? 3 : 2); // diamond(rare) / iron / chain
case 5 -> r < 0.35 ? 4 : (r < 0.88 ? 3 : 2); // diamond / iron / chain
default -> 0;
};
}
/** Returns the rank of an item within a ladder, or -1 if it isn't in the ladder. */
private static int materialLevel(Item item, Item[] ladder) {
for (int i = 0; i < ladder.length; i++) {
if (ladder[i] == item) {
return i;
}
}
return -1;
}
private static void maybeEnchant(ItemStack stack, Enchantment enchantment, int tier, RandomSource random) {
double chance = AMConfig.GEAR_ENCHANT_CHANCE_PER_TIER.get() * tier;
if (random.nextDouble() < chance) {
int level = enchantment == Enchantments.QUICK_CHARGE && tier >= 5
? 2 + random.nextInt(2)
: (tier >= 5 && random.nextBoolean() ? 2 : 1);
int existing = EnchantmentHelper.getItemEnchantmentLevel(enchantment, stack);
if (existing >= level) {
return;
}
stack.enchant(enchantment, level);
}
}
private static void equip(Mob mob, EquipmentSlot slot, ItemStack stack) {
mob.setItemSlot(slot, stack);
mob.setDropChance(slot, 0.0F); // added gear shouldn't be farmable
}
}
@@ -0,0 +1,45 @@
package net.fretux.adaptivemobs.scaling;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.Entity;
/**
* Helper for the small chunk of persistent NBT we keep on each adapted mob (via Forge's
* {@link Entity#getPersistentData()}). Storing the applied tier lets us avoid re-applying
* attribute scaling every time the entity is loaded from disk.
*/
public final class AdaptiveMobData {
private static final String ROOT = "AdaptiveMobs";
private static final String KEY_SCALED = "scaled";
private static final String KEY_TIER = "tier";
private AdaptiveMobData() {
}
private static CompoundTag root(Entity entity) {
CompoundTag data = entity.getPersistentData();
if (!data.contains(ROOT)) {
data.put(ROOT, new CompoundTag());
}
return data.getCompound(ROOT);
}
public static boolean isScaled(Entity entity) {
return entity.getPersistentData().getCompound(ROOT).getBoolean(KEY_SCALED);
}
public static void markScaled(Entity entity, int tier) {
CompoundTag root = root(entity);
root.putBoolean(KEY_SCALED, true);
root.putInt(KEY_TIER, tier);
// re-attach in case the sub-tag was freshly created
entity.getPersistentData().put(ROOT, root);
}
/** Returns the tier stored on the mob, or -1 if it was never scaled. */
public static int getTier(Entity entity) {
CompoundTag root = entity.getPersistentData().getCompound(ROOT);
return root.contains(KEY_TIER) ? root.getInt(KEY_TIER) : -1;
}
}
@@ -0,0 +1,84 @@
package net.fretux.adaptivemobs.scaling;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import java.util.UUID;
/**
* Applies moderate, attribute-based stat scaling to a mob for a given tier. Scaling is applied
* exactly once per mob instance and recorded in {@link AdaptiveMobData} so reloads don't stack it.
*/
public final class MobStatScaler {
// Stable UUIDs so a given modifier is only ever added once per attribute instance.
private static final UUID HEALTH_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000001");
private static final UUID DAMAGE_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000002");
private static final UUID SPEED_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000003");
private static final UUID FOLLOW_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000004");
private static final UUID ARMOR_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000005");
private static final UUID KNOCKBACK_UUID = UUID.fromString("9f1d0a5e-1f2a-4c3b-8a11-000000000006");
private MobStatScaler() {
}
/**
* Applies tier scaling to the mob and heals it to its new maximum. No-op if scaling is
* disabled, the tier is 0, or the mob has already been scaled.
*/
public static void applyScaling(Mob mob, int tier) {
if (!AMConfig.STAT_SCALING_ENABLED.get() || tier <= 0) {
AdaptiveMobData.markScaled(mob, Math.max(tier, 0));
return;
}
if (AdaptiveMobData.isScaled(mob)) {
return;
}
addMultiplier(mob, Attributes.MAX_HEALTH, HEALTH_UUID, "am_health",
AMConfig.tierValue(AMConfig.HEALTH_SCALE.get(), tier));
addMultiplier(mob, Attributes.ATTACK_DAMAGE, DAMAGE_UUID, "am_damage",
AMConfig.tierValue(AMConfig.DAMAGE_SCALE.get(), tier));
addMultiplier(mob, Attributes.MOVEMENT_SPEED, SPEED_UUID, "am_speed",
AMConfig.tierValue(AMConfig.SPEED_SCALE.get(), tier));
addMultiplier(mob, Attributes.FOLLOW_RANGE, FOLLOW_UUID, "am_follow",
AMConfig.tierValue(AMConfig.FOLLOW_RANGE_SCALE.get(), tier));
addAddition(mob, Attributes.ARMOR, ARMOR_UUID, "am_armor",
AMConfig.tierValue(AMConfig.ARMOR_BONUS.get(), tier));
addAddition(mob, Attributes.KNOCKBACK_RESISTANCE, KNOCKBACK_UUID, "am_kb",
AMConfig.tierValue(AMConfig.KNOCKBACK_RESIST.get(), tier));
// Heal to the new maximum so the health bonus is actually felt.
mob.setHealth(mob.getMaxHealth());
AdaptiveMobData.markScaled(mob, tier);
}
private static void addMultiplier(Mob mob, Attribute attribute, UUID uuid, String name, double value) {
if (value <= 0.0) {
return;
}
AttributeInstance instance = mob.getAttribute(attribute);
if (instance == null || instance.getModifier(uuid) != null) {
return;
}
instance.addPermanentModifier(
new AttributeModifier(uuid, name, value, AttributeModifier.Operation.MULTIPLY_TOTAL));
}
private static void addAddition(Mob mob, Attribute attribute, UUID uuid, String name, double value) {
if (value <= 0.0) {
return;
}
AttributeInstance instance = mob.getAttribute(attribute);
if (instance == null || instance.getModifier(uuid) != null) {
return;
}
instance.addPermanentModifier(
new AttributeModifier(uuid, name, value, AttributeModifier.Operation.ADDITION));
}
}
@@ -0,0 +1,102 @@
package net.fretux.adaptivemobs.spawn;
import net.fretux.adaptivemobs.AdaptiveMobs;
import net.fretux.adaptivemobs.config.AMConfig;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.AABB;
/**
* Adds modest spawn pressure: on a natural hostile spawn there is a tier-scaled chance to spawn
* one extra mob of the same type nearby. Capped at one extra per event, gated by local density,
* and guarded against recursion so extra spawns can never trigger further extra spawns.
*/
public final class SpawnPressureHelper {
private static final ThreadLocal<Boolean> SPAWNING_EXTRA = ThreadLocal.withInitial(() -> Boolean.FALSE);
private SpawnPressureHelper() {
}
public static void maybeSpawnExtra(ServerLevel level, Mob original, int tier, MobSpawnType spawnType) {
if (!AMConfig.SPAWN_PRESSURE_ENABLED.get() || tier <= 0) {
return;
}
// Recursion guard: an extra spawn must never produce more extra spawns.
if (SPAWNING_EXTRA.get()) {
return;
}
// Natural spawns only.
if (spawnType != MobSpawnType.NATURAL && spawnType != MobSpawnType.CHUNK_GENERATION) {
return;
}
RandomSource random = level.random;
double chance = AMConfig.tierValue(AMConfig.EXTRA_SPAWN_CHANCE.get(), tier);
if (random.nextDouble() > chance) {
return;
}
// Respect local density so we don't snowball.
int radius = AMConfig.SPAWN_DENSITY_RADIUS.get();
AABB box = original.getBoundingBox().inflate(radius);
int nearby = level.getEntitiesOfClass(Monster.class, box).size();
if (nearby >= AMConfig.SPAWN_DENSITY_CAP.get()) {
return;
}
EntityType<?> type = original.getType();
Entity created = type.create(level);
if (!(created instanceof Mob extra)) {
if (created != null) {
created.discard();
}
return;
}
SPAWNING_EXTRA.set(Boolean.TRUE);
try {
for (int attempt = 0; attempt < 6; attempt++) {
double dx = (random.nextDouble() - 0.5) * radius;
double dz = (random.nextDouble() - 0.5) * radius;
BlockPos spawnPos = surfaceSpawnPos(level, original.getX() + dx, original.getZ() + dz);
if (!level.getWorldBorder().isWithinBounds(spawnPos)) {
continue;
}
extra.moveTo(spawnPos.getX() + 0.5D, spawnPos.getY(), spawnPos.getZ() + 0.5D,
random.nextFloat() * 360.0F, 0.0F);
// Honour collision, light/biome spawn rules and obstruction checks.
if (level.noCollision(extra)
&& extra.checkSpawnRules(level, MobSpawnType.NATURAL)
&& extra.checkSpawnObstruction(level)) {
extra.finalizeSpawn(level, level.getCurrentDifficultyAt(extra.blockPosition()),
MobSpawnType.NATURAL, null, null);
level.addFreshEntity(extra);
if (AMConfig.debug) {
AdaptiveMobs.LOGGER.info("[AdaptiveMobs] Extra spawn ({}) at tier {} near {}",
type, tier, original.blockPosition());
}
return;
}
}
// No valid position found.
extra.discard();
} finally {
SPAWNING_EXTRA.set(Boolean.FALSE);
}
}
private static BlockPos surfaceSpawnPos(ServerLevel level, double x, double z) {
int blockX = BlockPos.containing(x, 0.0D, z).getX();
int blockZ = BlockPos.containing(x, 0.0D, z).getZ();
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, blockX, blockZ);
return new BlockPos(blockX, y, blockZ);
}
}