package net.minecraft.world.entity.monster; import com.google.common.collect.ImmutableList; import com.mojang.datafixers.util.Pair; import java.util.List; import java.util.Optional; import net.minecraft.core.BlockPos; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.util.valueproviders.UniformInt; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityAttachment; import net.minecraft.world.entity.EntityAttachments; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityTypes; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.ai.ActivityData; import net.minecraft.world.entity.ai.Brain; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.behavior.BehaviorUtils; import net.minecraft.world.entity.ai.behavior.DoNothing; import net.minecraft.world.entity.ai.behavior.LookAtTargetSink; import net.minecraft.world.entity.ai.behavior.MeleeAttack; import net.minecraft.world.entity.ai.behavior.MoveToTargetSink; import net.minecraft.world.entity.ai.behavior.RandomStroll; import net.minecraft.world.entity.ai.behavior.RunOne; import net.minecraft.world.entity.ai.behavior.SetEntityLookTargetSometimes; import net.minecraft.world.entity.ai.behavior.SetWalkTargetFromAttackTargetIfTargetOutOfReach; import net.minecraft.world.entity.ai.behavior.SetWalkTargetFromLookTarget; import net.minecraft.world.entity.ai.behavior.StartAttacking; import net.minecraft.world.entity.ai.behavior.StopAttackingIfTargetInvalid; import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; import net.minecraft.world.entity.ai.memory.MemoryModuleType; import net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities; import net.minecraft.world.entity.ai.sensing.Sensor; import net.minecraft.world.entity.ai.sensing.SensorType; import net.minecraft.world.entity.monster.hoglin.HoglinBase; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.level.Level; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; import org.jspecify.annotations.Nullable; public class Zoglin extends Monster implements HoglinBase { private static final EntityDataAccessor DATA_BABY_ID = SynchedEntityData.defineId(Zoglin.class, EntityDataSerializers.BOOLEAN); private static final EntityDimensions BABY_DIMENSIONS = EntityDimensions.scalable(0.75F, 0.85F) .withEyeHeight(0.625F) .withAttachments(EntityAttachments.builder().attach(EntityAttachment.PASSENGER, 0.0F, 0.875F, 0.0F)); private static final int MAX_HEALTH = 40; private static final int ATTACK_KNOCKBACK = 1; private static final float KNOCKBACK_RESISTANCE = 0.6F; private static final int ATTACK_DAMAGE = 6; private static final float BABY_ATTACK_DAMAGE = 0.5F; private static final int ATTACK_INTERVAL = 40; private static final int BABY_ATTACK_INTERVAL = 15; private static final int ATTACK_DURATION = 200; private static final float MOVEMENT_SPEED_WHEN_FIGHTING = 0.3F; private static final float SPEED_MULTIPLIER_WHEN_IDLING = 0.4F; private static final boolean DEFAULT_BABY = false; private int attackAnimationRemainingTicks; private static final Brain.Provider BRAIN_PROVIDER = Brain.provider( List.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS), var0 -> getActivities() ); public Zoglin(final EntityType type, final Level level) { super(type, level); this.xpReward = 5; } @Override protected Brain makeBrain(final Brain.Packed packedBrain) { return BRAIN_PROVIDER.makeBrain(this, packedBrain); } protected static List> getActivities() { return List.of(initCoreActivity(), initIdleActivity(), initFightActivity()); } private static ActivityData initCoreActivity() { return ActivityData.create(Activity.CORE, 0, ImmutableList.of(new LookAtTargetSink(45, 90), new MoveToTargetSink())); } private static ActivityData initIdleActivity() { return ActivityData.create( Activity.IDLE, 10, ImmutableList.of( StartAttacking.create(Zoglin::findNearestValidAttackTarget), SetEntityLookTargetSometimes.create(8.0F, UniformInt.of(30, 60)), new RunOne<>( ImmutableList.of(Pair.of(RandomStroll.stroll(0.4F), 2), Pair.of(SetWalkTargetFromLookTarget.create(0.4F, 3), 2), Pair.of(new DoNothing(30, 60), 1)) ) ) ); } @Override public EntityDimensions getDefaultDimensions(final Pose pose) { return this.isBaby() ? BABY_DIMENSIONS : super.getDefaultDimensions(pose); } private static ActivityData initFightActivity() { return ActivityData.create( Activity.FIGHT, 10, ImmutableList.of( SetWalkTargetFromAttackTargetIfTargetOutOfReach.create(1.0F), BehaviorBuilder.triggerIf(Zoglin::isAdult, MeleeAttack.create(40)), BehaviorBuilder.triggerIf(Zoglin::isBaby, MeleeAttack.create(15)), StopAttackingIfTargetInvalid.create() ), MemoryModuleType.ATTACK_TARGET ); } private static Optional findNearestValidAttackTarget(final ServerLevel level, final Mob mob) { return ((NearestVisibleLivingEntities)mob.getBrain().getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).orElse(NearestVisibleLivingEntities.empty())) .findClosest(target -> !target.is(EntityTypes.ZOGLIN) && !target.is(EntityTypes.CREEPER) && Sensor.isEntityAttackable(level, mob, target)); } @Override protected void defineSynchedData(final SynchedEntityData.Builder entityData) { super.defineSynchedData(entityData); entityData.define(DATA_BABY_ID, false); } @Override public void onSyncedDataUpdated(final EntityDataAccessor accessor) { super.onSyncedDataUpdated(accessor); if (DATA_BABY_ID.equals(accessor)) { this.refreshDimensions(); } } @Nullable @Override public SpawnGroupData finalizeSpawn( final ServerLevelAccessor level, final DifficultyInstance difficulty, final EntitySpawnReason spawnReason, @Nullable final SpawnGroupData groupData ) { if (level.getRandom().nextFloat() < 0.2F) { this.setBaby(true); } return super.finalizeSpawn(level, difficulty, spawnReason, groupData); } public static AttributeSupplier.Builder createAttributes() { return Monster.createMonsterAttributes() .add(Attributes.MAX_HEALTH, 40.0) .add(Attributes.MOVEMENT_SPEED, 0.3F) .add(Attributes.KNOCKBACK_RESISTANCE, 0.6F) .add(Attributes.ATTACK_KNOCKBACK, 1.0) .add(Attributes.ATTACK_DAMAGE, 6.0); } public boolean isAdult() { return !this.isBaby(); } @Override public boolean doHurtTarget(final ServerLevel level, final Entity target) { if (target instanceof LivingEntity entity) { this.attackAnimationRemainingTicks = 10; level.broadcastEntityEvent(this, (byte)4); this.makeSound(SoundEvents.ZOGLIN_ATTACK); return HoglinBase.hurtAndThrowTarget(level, this, entity); } else { return false; } } @Override public boolean canBeLeashed() { return true; } @Override protected void blockedByItem(final LivingEntity defender, final DamageSource source, final float damage) { if (!this.isBaby()) { HoglinBase.throwTarget(this, defender); } } @Override public boolean hurtServer(final ServerLevel level, final DamageSource source, final float damage) { boolean wasHurt = super.hurtServer(level, source, damage); if (wasHurt && source.getEntity() instanceof LivingEntity attacker) { if (this.canAttack(attacker) && !BehaviorUtils.isOtherTargetMuchFurtherAwayThanCurrentAttackTarget(this, attacker, 4.0)) { this.setAttackTarget(attacker); } return true; } else { return wasHurt; } } private void setAttackTarget(final LivingEntity target) { this.brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); this.brain.setMemoryWithExpiry(MemoryModuleType.ATTACK_TARGET, target, 200L); } @Override public Brain getBrain() { return super.getBrain(); } protected void updateActivity() { Activity oldActivity = (Activity)this.brain.getActiveNonCoreActivity().orElse(null); this.brain.setActiveActivityToFirstValid(ImmutableList.of(Activity.FIGHT, Activity.IDLE)); Activity newActivity = (Activity)this.brain.getActiveNonCoreActivity().orElse(null); if (newActivity == Activity.FIGHT && oldActivity != Activity.FIGHT) { this.playAngrySound(); } this.setAggressive(this.brain.hasMemoryValue(MemoryModuleType.ATTACK_TARGET)); } @Override protected void customServerAiStep(final ServerLevel level) { ProfilerFiller profiler = Profiler.get(); profiler.push("zoglinBrain"); this.getBrain().tick(level, this); profiler.pop(); this.updateActivity(); } @Override public void setBaby(final boolean baby) { this.getEntityData().set(DATA_BABY_ID, baby); if (!this.level().isClientSide() && baby) { this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(0.5); } } @Override public boolean isBaby() { return this.getEntityData().get(DATA_BABY_ID); } @Override public void aiStep() { if (this.attackAnimationRemainingTicks > 0) { this.attackAnimationRemainingTicks--; } super.aiStep(); } @Override public void handleEntityEvent(final byte id) { if (id == 4) { this.attackAnimationRemainingTicks = 10; this.makeSound(SoundEvents.ZOGLIN_ATTACK); } else { super.handleEntityEvent(id); } } @Override public int getAttackAnimationRemainingTicks() { return this.attackAnimationRemainingTicks; } @Override protected SoundEvent getAmbientSound() { if (this.level().isClientSide()) { return null; } else { return this.brain.hasMemoryValue(MemoryModuleType.ATTACK_TARGET) ? SoundEvents.ZOGLIN_ANGRY : SoundEvents.ZOGLIN_AMBIENT; } } @Override protected SoundEvent getHurtSound(final DamageSource source) { return SoundEvents.ZOGLIN_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.ZOGLIN_DEATH; } @Override protected void playStepSound(final BlockPos pos, final BlockState blockState) { this.playSound(SoundEvents.ZOGLIN_STEP, 0.15F, 1.0F); } protected void playAngrySound() { this.makeSound(SoundEvents.ZOGLIN_ANGRY); } @Nullable @Override public LivingEntity getTarget() { return this.getTargetFromBrain(); } @Override protected void addAdditionalSaveData(final ValueOutput output) { super.addAdditionalSaveData(output); output.putBoolean("IsBaby", this.isBaby()); } @Override protected void readAdditionalSaveData(final ValueInput input) { super.readAdditionalSaveData(input); this.setBaby(input.getBooleanOr("IsBaby", false)); } }