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