A possible solution is to cache the ItemStack
that causes the action in the DamageSource
instance that will be used to cause the damage and check for enchantments against this item instead of the player's currently held item. The DamageSource
will also need to be cached for projectiles and written to NBT in case the world is closed before the damage is applied.
On second thought, the DamageSource
likely doesn't need to be written to NBT for projectiles, saving the ItemStack
to the projectile should suffice. The ItemStack
can be passed to the DamageSource
when needed.
Spiders and Wolves use the class LeapAtTargetGoal
which handles the leaping at their target. This class calls Goal::setFlags
in its constructor with the values of Goal.Flag.MOVE
and Goal.Flag.LOOK
. These flags prevent other AI goals with the same flags from executing. The MeleeAttackGoal
handles the melee attack and has the same goal flags set. To fix this, remove the call to Goal::setFlags
from the LeapAtTargetGoal
to allow it to run at the same time as MeleeAttackGoal
.
I think this is related to my comment on MC-198068, but to summarize, the AI that controls them looking at their target is the same class that makes them pathfind to and attack their target, which keeps starting and resetting when they cannot find a path to the target, thus causing the mobs to briefly look at and away from the target.
Wolves are programmed to take less damage from a damage source with an associated entity that isn't a player or arrow, essentially taking a mob's damage on easy mode. My guess as to why is to make tamed wolves more useful when fighting mobs. Here's the relevant part of the code:
if (entity != null && !(entity instanceof Player) && !(entity instanceof AbstractArrow)) {
damage = (damage + 1.0F) / 2.0F;
}
This class is called net.minecraft.util.Mth
in Mojang's mappings.
It will still be possible to hit invisible entities, the name of the entity simply won't show on the debug screen.
The blaze is attacking itself. When a blaze gets hurts, it alerts other blazes within its follow range that it has been hurt and sets their attack target to the original blaze's target.
Mobs have an integer field called noActionTime
which is increased every tick so long as the mob is alive and has its AI enabled. When a mob is within 32 blocks of the closest non-spectator player, this timer is set to zero. This field is also used to determine when a mob can randomly despawn. When this timer is greater than 100 ticks (10 seconds), most mobs are programmed to not wander around. This seems to be intentional given the name of the field. My guess is this was implemented to not use up the game's resources for mobs trying to pathfind that are far from any players. This value is not saved to NBT which means noActionTime
is set to the default value of zero when the world is reloaded.
I can confirm that this is still an issue in 1.17 Release Candidate 1.
The mobs listed above all override the method LivingEntity::travel
which controls the movement logic for mobs and players alike. Horses, donkeys, mules, llamas, trader llamas, zombie horses and skeleton horses all extend the same base class called AbstractHorse
. Pigs and striders implement the interface ItemSteerable
which handles its movement when ridden while its rider is holding the needed item for steering.
In the beginning of this method, these mobs checks if it is alive, which means all motion is skipped when it is dead. This check was likely put to stop these mobs moving when dying while being ridden. This check should be removed from the travel method and instead the method LivingEntity::isImmobile
should be overridden which if true stop all AI logic for mobs. By default this method returns LivingEntity::isDeadOrDying
. It seems this was attempted in the AbstractHorse
class, however there is a simple mistake. Instead of this:
protected boolean isImmobile() {
return super.isImmobile() && this.isVehicle() && this.isSaddled() || this.isEating() || this.isStanding();
}
The logical AND should be replaced with logical OR after super.isImmobile()
, like this:
protected boolean isImmobile() {
return super.isImmobile() || this.isVehicle() && this.isSaddled() || this.isEating() || this.isStanding();
}
The above code should be applied to pigs and striders as well without the specific horse methods.
Players and mobs use a timer to count down the next time they can take damage as described here called invulnerableTime
. Which means that non-mob entities don't take more damage, but take damage every tick instead of every 10 ticks. Interestingly, the field invulnerableTime
is available in the Entity
class, so all that needs to be done is to take the logic from players/mobs and apply it to the other entities.
When any mob is attached with a lead, a field called restrictCenter
is set to the leash holder's position and another field called restrictRadius
stores the restriction distance which is set to 5 blocks when a mob is leashed. This field is tested for in various mob AI classes to check if a mob is allowed to navigate outside of the restrictRadius
from the restrictCenter
position and whether a mob should try to or continue to attack another mob if the target or the attacker is outside of this restriction. The issue is that when a mob has its lead removed, restrictRadius
is not reset to the sentinel value of -1 (which means the mob has no restriction) until the game is reloaded.
This is also the cause for MC-221754.
The cause of this issue is likely located in the BodyRotationControl
class. The yRot
field is not updated when the mob is not moving. Here's an example of a fix in the method BodyRotationControl::clientTick
. The only change is the addition of the last line.
if (this.isMoving()) {
this.mob.yBodyRot = this.mob.getYRot(); // Take the logic from here
this.rotateHeadIfNecessary();
this.lastStableYHeadRot = this.mob.yHeadRot;
this.headStableTime = 0;
} else {
if (this.notCarryingMobPassengers()) {
if (Math.abs(this.mob.yHeadRot - this.lastStableYHeadRot) > 15.0F) {
this.headStableTime = 0;
this.lastStableYHeadRot = this.mob.yHeadRot;
this.rotateBodyIfNecessary();
} else {
++this.headStableTime;
if (this.headStableTime > 10) {
this.rotateHeadTowardsFront();
}
}
this.mob.setYRot(this.mob.yBodyRot). //And apply the opposite here
}
}
The mobs that have this issue all use the MeleeAttackGoal
class or their own class that extends it. There is a field called lastCanUseCheck
, which is the first check if the goal can be used. This field is used to ensure if at least one second has passed since the last time this goal executed before any other checks are done.
For the goal to execute, the mob then must have a target that is alive and the mob must either have a path to the target, or be within melee range. While the goal is running, a field called followingTargetEvenIfNotSeen
is checked to allow the mob to pathfind toward the target even if it cannot be seen. Some mobs have this set to false, and others set to true.
The method MeleeAttackGoal::canContinueToUse
is called each tick while the goal is running to check if the goal should continue. The field followingTargetEvenIfNotSeen
is checked again in this method and if it's not true, the goal will stop if the mob does not have a path. Which means that when mobs occasionally can't find a path to the target and have followingTargetEvenIfNotSeen
set to false, the goal stops and begins the checks to start again as described in the first and second paragraph.
The field lastCanUseCheck
should be removed entirely, and the check for followingTargetEvenIfNotSeen
in the method MeleeAttackGoal::canContinueToUse
should also be removed.
This issue is directly responsible for MC-191642. The field that controls the attack timer is called ticksUntilNextAttack
. When the goal starts, this field is set to zero, which, as a result of the above description, causes the timer to reset and allow the mob to attack twice in a row. The method MeleeAttackGoal::start
should remove where the field ticksUntilNextAttack
is set to zero.
Based on Mojang's mappings, the class FollowParentGoal
controls the AI for baby animals to follow the nearest adult of the same type. This class needs to call Goal::setFlags
with the value Goal.Flag.MOVE
to prevent other goals that set this flag from executing.
Applies to slimes and magma cubes as well.
The classes
EntityDamageSource
andIndirectEntityDamageSource
override the methodDamageSource#getLocalizedDeathMessage
and check for the currently held item in the main hand of the attacker. As explained in this comment, theItemStack
responsible for the action should be stored in the projectile then passed to theDamageSource
and used in the death message instead.