mojira.dev
MC-313195

Ender Dragon fireballs can collide with the dragon in midair, leaving floating breath clouds

While testing the Ender Dragon fight in survival mode after the recent AI fixes in 26.4 Snapshot 8, I noticed that the dragon's fireballs often collide with the dragon itself immediately after being fired.

Instead of flying toward the player, the fireball explodes right in midair on the dragon's hitbox. As a result, the boss's attack is wasted, and the purple dragon's breath cloud remains floating high in the sky, obstructing visibility during the fight.

Steps to Reproduce:

  1. Go to the End in Survival mode on a clean vanilla 26.4 Snapshot 8 client.

  2. Let the Ender Dragon fly around and shoot fireballs at you while maneuvering.

  3. Watch the projectile right as it leaves the dragon's mouth.

Observed Behavior:

The fireball explodes immediately upon hitting the dragon's own hitbox, leaving a floating area effect cloud high in the air.

Expected Behavior:

The dragon's fireball should pass through the dragon that fired it and travel toward the player.

Attachments

Comments 6

Thank you for helping us improve Minecraft! We saved your files:

[media][media]

Added a second video using /tick freeze right after hearing the shoot sound. There is no visible fireball projectile in the air: the purple area effect cloud appears immediately beneath the flying dragon.

Code Analysis:

In DragonStrafePlayerPhase#doServerTick, the DragonFireball is spawned slightly behind the dragon's head (in the neck/throat area) so it appears to be fired from the dragon's mouth:

Vec3 viewVector = this.dragon.getViewVector(1.0f);
double startingX = this.dragon.head.getX() - viewVector.x * 1.0;
double startingY = this.dragon.head.getY(0.5) + 0.5;
double startingZ = this.dragon.head.getZ() - viewVector.z * 1.0;

To allow the projectile to safely pass through the dragon when fired, DragonFireball#onHit includes an owner check:

if (hitResult.getType() == HitResult.Type.ENTITY && this.ownedBy(((EntityHitResult)hitResult).getEntity())) {
    return;
}

However, because the Ender Dragon uses multi-part hitboxes (EnderDragonPart for its head, neck, wings, etc.), ((EntityHitResult)hitResult).getEntity() returns an EnderDragonPart instead of the parent EnderDragon (this.getOwner()). As a result, this.ownedBy(...) returns false, the early return is skipped, and the fireball explodes immediately inside the dragon's own neck or wings.

Additionally, the LivingEntity search right below in DragonFireball#onHit detects the parent EnderDragon nearby and snaps the newly created AreaEffectCloud directly to the dragon's coordinates (cloud.setPos(entity.getX(), entity.getY(), entity.getZ())), leaving the purple cloud floating right beneath the flying dragon.

Suggested Fix:

Elsewhere in the codebase (for example, in Player#itemAttackInteraction), multi-part dragon hitboxes are properly unwrapped to parentMob:

Entity hurtTarget = entity;
if (entity instanceof EnderDragonPart) {
    EnderDragonPart enderDragonPart = (EnderDragonPart)entity;
    hurtTarget = enderDragonPart.parentMob;
}

Unwrapping EnderDragonPart to parentMob before calling this.ownedBy(...) in DragonFireball#onHit fixes the issue:

if (hitResult.getType() == HitResult.Type.ENTITY) {
    Entity otherEntity = ((EntityHitResult)hitResult).getEntity();
    if (otherEntity instanceof EnderDragonPart dragonPart) {
        otherEntity = dragonPart.parentMob;
    }
    if (this.ownedBy(otherEntity)) {
        return;
    }
}

Proof of Fix:

To confirm that unwrapping EnderDragonPart to parentMob in DragonFireball#onHit completely resolves the issue, I tested the fix via a small Mixin (@WrapOperation on DragonFireball#ownedBy call):

@Mixin(DragonFireball.class)
public abstract class DragonFireballMixin {

    @WrapOperation(
        method = "onHit(Lnet/minecraft/world/phys/HitResult;)V",
        at = @At(
            value = "INVOKE",
            target = "Lnet/minecraft/world/entity/projectile/hurtingprojectile/DragonFireball;ownedBy(Lnet/minecraft/world/entity/Entity;)Z"
        )
    )
    private boolean fixPartOwnership(DragonFireball fireball, Entity target, Operation<Boolean> original) {
        if (target instanceof EnderDragonPart part) {
            target = part.parentMob;
            if (fireball.level() instanceof ServerLevel level) {
                logFix(level, part, target);
            }
        }
        return original.call(fireball, target);
    }

    @Unique
    private static void logFix(ServerLevel level, EnderDragonPart part, Entity owner) {
        String text = "[MC-313195] Ignored self-hit on '%s' -> owner %s".formatted(part.name, owner.getName().getString());
        level.getServer().getPlayerList().broadcastSystemMessage(Component.literal("§d" + text), false);
    }
}

As shown in the attached video, the fireball now properly recognizes the dragon's body parts, unwraps them to the parent EnderDragon, ignores the self-collision, and travels cleanly toward the player instead of exploding in midair.

Note: This video demonstrates the patched behavior via Fabric + Mixin and is NOT vanilla behavior (see F3).

diskria

(Unassigned)

Unconfirmed

26.4 Snapshot 8

Retrieved