package com.owlmaddie.goals; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.ai.RangedAttackMob; import net.minecraft.entity.mob.Angerable; import java.util.concurrent.ThreadLocalRandom; import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.passive.GolemEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.sound.SoundEvents; import net.minecraft.util.math.Vec3d; import java.util.EnumSet; import static com.owlmaddie.network.ServerPackets.ATTACK_PARTICLE; /** * 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. */ public class AttackPlayerGoal extends PlayerBaseGoal { 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; protected final int CHARGE_TIME = 12; // Time before leaping / attacking 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) { super(targetEntity); this.attackerEntity = attackerEntity; this.speed = speed; this.setControls(EnumSet.of(Control.MOVE, Control.LOOK, Control.TARGET)); } @Override public boolean canStart() { return super.canStart() && isGoalActive(); } @Override public boolean shouldContinue() { return super.canStart() && isGoalActive(); } @Override public void stop() { } private boolean isGoalActive() { if (this.targetEntity == null || (this.targetEntity != null && !this.targetEntity.isAlive())) { return false; } // Set the attack target (if not self) if (!this.attackerEntity.equals(this.targetEntity)) { this.attackerEntity.setTarget(this.targetEntity); } // 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 return this.attackerEntity instanceof HostileEntity || this.attackerEntity instanceof Angerable || this.attackerEntity instanceof RangedAttackMob || this.attackerEntity instanceof GolemEntity; } private void performAttack() { // Track the attacker (needed for protect to work) if (!this.attackerEntity.equals(this.targetEntity)) { this.targetEntity.setAttacker(this.attackerEntity); } // 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' int numParticles = ThreadLocalRandom.current().nextInt(2, 7); // Random number between 2 (inclusive) and 7 (exclusive) ((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE, this.targetEntity.getX(), this.targetEntity.getBodyY(0.5D), this.targetEntity.getZ(), numParticles, 0.5, 0.5, 0.1, 0.4); } @Override public void tick() { double squaredDistanceToPlayer = this.attackerEntity.squaredDistanceTo(this.targetEntity); this.attackerEntity.getLookControl().lookAt(this.targetEntity, 30.0F, 30.0F); // 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; case MOVING_TOWARDS_PLAYER: this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed); if (squaredDistanceToPlayer < CHARGE_DISTANCE) { currentState = EntityState.CHARGING; } else { currentState = EntityState.IDLE; } break; case CHARGING: this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D); if (cooldownTimer <= 0) { currentState = EntityState.LEAPING; } break; case LEAPING: // Leap towards the player 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; currentState = EntityState.ATTACKING; break; case ATTACKING: // Attack player this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D); if (squaredDistanceToPlayer < ATTACK_DISTANCE && cooldownTimer <= 0) { this.performAttack(); currentState = EntityState.IDLE; } else if (cooldownTimer <= 0) { currentState = EntityState.IDLE; } break; } // decrement cool down cooldownTimer--; } }