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);
}
}
@@ -0,0 +1,5 @@
# Adaptive Mobs access transformers
# Expose the goal/target selectors on Mob so we can inject and remove AI goals
# without resorting to mixins.
public net.minecraft.world.entity.Mob f_21345_ # goalSelector
public net.minecraft.world.entity.Mob f_21346_ # targetSelector
+73
View File
@@ -0,0 +1,73 @@
# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="${mod_license}"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
# If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or Open to LAN),
# set this to true, and Forge will set the correct displayTest for you and skip loading your mod on dedicated servers.
#clientSideOnly=true #optional - defaults to false if absent
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="${mod_id}" #mandatory
# The version number of the mod
version="${mod_version}" #mandatory
# A display name for the mod
displayName="${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
#logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
# A text field displayed in the mod UI
authors="${mod_authors}" #optional
# Display Test controls the display for your mod in the server connection screen
# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
#displayTest="MATCH_VERSION" # if nothing is specified, MATCH_VERSION is the default when clientSideOnly=false, otherwise IGNORE_ALL_VERSION when clientSideOnly=true (#optional)
# The description text for the mod (multi line!) (#mandatory)
description='''${mod_description}'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${mod_id}]] #optional
# the modid of the dependency
modId="forge" #mandatory
# Does this dependency have to exist - if not, ordering below must be specified
mandatory=true #mandatory
# The version range of the dependency
versionRange="${forge_version_range}" #mandatory
# An ordering relationship for the dependency - BEFORE or AFTER required if the dependency is not mandatory
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side="BOTH"
# Here's another dependency
[[dependencies.${mod_id}]]
modId="minecraft"
mandatory=true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="${minecraft_version_range}"
ordering="NONE"
side="BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features.${mod_id}]
#openGLVersion="[3.2,)"
+8
View File
@@ -0,0 +1,8 @@
{
"pack": {
"description": {
"text": "${mod_id} resources"
},
"pack_format": 15
}
}