AttackPlayerGoal.java 6.65 KB
Newer Older
1 2
package com.owlmaddie.goals;

3
import net.minecraft.entity.LivingEntity;
4 5
import net.minecraft.entity.ai.RangedAttackMob;
import net.minecraft.entity.mob.Angerable;
6
import java.util.concurrent.ThreadLocalRandom;
7 8
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity;
9
import net.minecraft.entity.passive.GolemEntity;
10
import net.minecraft.server.world.ServerWorld;
11
import net.minecraft.sound.SoundEvents;
12 13 14 15
import net.minecraft.util.math.Vec3d;

import java.util.EnumSet;

16 17
import static com.owlmaddie.network.ServerPackets.ATTACK_PARTICLE;

18
/**
19 20
 * The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards a target Entity.
 * For passive entities like chickens (or hostile entities in creative mode), damage is simulated with particles.
21
 */
22
public class AttackPlayerGoal extends PlayerBaseGoal {
23 24 25 26 27
    protected final MobEntity attackerEntity;
    protected final double speed;
    protected enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING }
    protected EntityState currentState = EntityState.IDLE;
    protected int cooldownTimer = 0;
28
    protected final int CHARGE_TIME = 12; // Time before leaping / attacking
29 30 31 32 33
    protected final double MOVE_DISTANCE = 200D; // 20 blocks away
    protected final double CHARGE_DISTANCE = 25D; // 5 blocks away
    protected final double ATTACK_DISTANCE = 4D; // 2 blocks away

    public AttackPlayerGoal(LivingEntity targetEntity, MobEntity attackerEntity, double speed) {
34
        super(targetEntity);
35
        this.attackerEntity = attackerEntity;
36
        this.speed = speed;
37
        this.setControls(EnumSet.of(Control.MOVE, Control.LOOK, Control.TARGET));
38 39 40 41
    }

    @Override
    public boolean canStart() {
42
        return super.canStart() && isGoalActive();
43 44 45 46
    }

    @Override
    public boolean shouldContinue() {
47
        return super.canStart() && isGoalActive();
48 49 50 51
    }

    @Override
    public void stop() {
52 53
    }

54 55 56
    private boolean isGoalActive() {
        if (this.targetEntity == null || (this.targetEntity != null && !this.targetEntity.isAlive())) {
            return false;
57
        }
58

59 60 61 62 63
        // Set the attack target (if not self)
        if (!this.attackerEntity.equals(this.targetEntity)) {
            this.attackerEntity.setTarget(this.targetEntity);
        }

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
        // Is nearby to target
        boolean isNearby = this.attackerEntity.squaredDistanceTo(this.targetEntity) < MOVE_DISTANCE;

        // Check if the attacker is nearby and no native attacks
        boolean isNearbyAndNoNativeAttacks = isNearby && !hasNativeAttacks();

        // Check if it has native attacks but can't target (e.g., creative mode)
        LivingEntity livingAttackerEntity = this.attackerEntity;
        boolean hasNativeAttacksButCannotTarget = isNearby && hasNativeAttacks() && !livingAttackerEntity.canTarget(this.targetEntity);

        // Return true if either condition is met
        return isNearbyAndNoNativeAttacks || hasNativeAttacksButCannotTarget;
    }

    private boolean hasNativeAttacks() {
        // Does this entity have native attacks
80 81 82 83
        return this.attackerEntity instanceof HostileEntity ||
                this.attackerEntity instanceof Angerable ||
                this.attackerEntity instanceof RangedAttackMob ||
                this.attackerEntity instanceof GolemEntity;
84 85 86
    }

    private void performAttack() {
87 88 89 90 91
        // Track the attacker (needed for protect to work)
        if (!this.attackerEntity.equals(this.targetEntity)) {
            this.targetEntity.setAttacker(this.attackerEntity);
        }

92 93 94 95 96 97 98
        // For passive entities (or hostile in creative mode), apply minimal damage to simulate a 'leap' / 'melee' attack
        this.targetEntity.damage(this.attackerEntity.getDamageSources().generic(), 1.0F);

        // Play damage sound
        this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);

        // Spawn red particles to simulate 'injury'
99
        int numParticles = ThreadLocalRandom.current().nextInt(2, 7);  // Random number between 2 (inclusive) and 7 (exclusive)
100
        ((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE,
101 102
                this.targetEntity.getX(), this.targetEntity.getBodyY(0.5D), this.targetEntity.getZ(),
                numParticles, 0.5, 0.5, 0.1, 0.4);
103 104 105 106
    }

    @Override
    public void tick() {
107 108
        double squaredDistanceToPlayer = this.attackerEntity.squaredDistanceTo(this.targetEntity);
        this.attackerEntity.getLookControl().lookAt(this.targetEntity, 30.0F, 30.0F);
109

110 111 112 113 114 115 116 117 118 119 120 121
        // State transitions and actions
        switch (currentState) {
            case IDLE:
                cooldownTimer = CHARGE_TIME;
                if (squaredDistanceToPlayer < ATTACK_DISTANCE) {
                    currentState = EntityState.ATTACKING;
                } else if (squaredDistanceToPlayer < CHARGE_DISTANCE) {
                    currentState = EntityState.CHARGING;
                } else if (squaredDistanceToPlayer < MOVE_DISTANCE) {
                    currentState = EntityState.MOVING_TOWARDS_PLAYER;
                }
                break;
122

123
            case MOVING_TOWARDS_PLAYER:
124
                this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed);
125 126
                if (squaredDistanceToPlayer < CHARGE_DISTANCE) {
                    currentState = EntityState.CHARGING;
127
                } else {
128
                    currentState = EntityState.IDLE;
129
                }
130 131 132
                break;

            case CHARGING:
133
                this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D);
134 135 136 137 138 139 140
                if (cooldownTimer <= 0) {
                    currentState = EntityState.LEAPING;
                }
                break;

            case LEAPING:
                // Leap towards the player
141 142 143
                Vec3d leapDirection = new Vec3d(this.targetEntity.getX() - this.attackerEntity.getX(), 0.1D, this.targetEntity.getZ() - this.attackerEntity.getZ()).normalize().multiply(1.0);
                this.attackerEntity.setVelocity(leapDirection);
                this.attackerEntity.velocityModified = true;
144 145 146 147 148 149

                currentState = EntityState.ATTACKING;
                break;

            case ATTACKING:
                // Attack player
150
                this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D);
151 152 153 154 155 156 157
                if (squaredDistanceToPlayer < ATTACK_DISTANCE && cooldownTimer <= 0) {
                    this.performAttack();
                    currentState = EntityState.IDLE;
                } else if (cooldownTimer <= 0) {
                    currentState = EntityState.IDLE;
                }
                break;
158
        }
159 160 161

        // decrement cool down
        cooldownTimer--;
162 163 164
    }

}