package net.minecraft.world.entity.monster; import java.util.Collection; import net.minecraft.nbt.CompoundTag; 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.tags.ItemTags; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.AreaEffectCloud; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder; import net.minecraft.world.entity.ai.goal.AvoidEntityGoal; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; import net.minecraft.world.entity.ai.goal.SwellGoal; import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; import net.minecraft.world.entity.animal.Cat; import net.minecraft.world.entity.animal.Ocelot; import net.minecraft.world.entity.animal.goat.Goat; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.level.gameevent.GameEvent; import org.jetbrains.annotations.Nullable; public class Creeper extends Monster { private static final EntityDataAccessor DATA_SWELL_DIR = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_IS_POWERED = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_IS_IGNITED = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.BOOLEAN); private int oldSwell; private int swell; private int maxSwell = 30; private int explosionRadius = 3; private int droppedSkulls; public Creeper(EntityType entityType, Level level) { super(entityType, level); } @Override protected void registerGoals() { this.goalSelector.addGoal(1, new FloatGoal(this)); this.goalSelector.addGoal(2, new SwellGoal(this)); this.goalSelector.addGoal(3, new AvoidEntityGoal(this, Ocelot.class, 6.0F, 1.0, 1.2)); this.goalSelector.addGoal(3, new AvoidEntityGoal(this, Cat.class, 6.0F, 1.0, 1.2)); this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0, false)); this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8)); this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(6, new RandomLookAroundGoal(this)); this.targetSelector.addGoal(1, new NearestAttackableTargetGoal(this, Player.class, true)); this.targetSelector.addGoal(2, new HurtByTargetGoal(this)); } public static Builder createAttributes() { return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, 0.25); } @Override public int getMaxFallDistance() { return this.getTarget() == null ? this.getComfortableFallDistance(0.0F) : this.getComfortableFallDistance(this.getHealth() - 1.0F); } @Override public boolean causeFallDamage(float fallDistance, float multiplier, DamageSource source) { boolean bl = super.causeFallDamage(fallDistance, multiplier, source); this.swell += (int)(fallDistance * 1.5F); if (this.swell > this.maxSwell - 5) { this.swell = this.maxSwell - 5; } return bl; } @Override protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(DATA_SWELL_DIR, -1); builder.define(DATA_IS_POWERED, false); builder.define(DATA_IS_IGNITED, false); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); if (this.entityData.get(DATA_IS_POWERED)) { tag.putBoolean("powered", true); } tag.putShort("Fuse", (short)this.maxSwell); tag.putByte("ExplosionRadius", (byte)this.explosionRadius); tag.putBoolean("ignited", this.isIgnited()); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); this.entityData.set(DATA_IS_POWERED, tag.getBoolean("powered")); if (tag.contains("Fuse", 99)) { this.maxSwell = tag.getShort("Fuse"); } if (tag.contains("ExplosionRadius", 99)) { this.explosionRadius = tag.getByte("ExplosionRadius"); } if (tag.getBoolean("ignited")) { this.ignite(); } } @Override public void tick() { if (this.isAlive()) { this.oldSwell = this.swell; if (this.isIgnited()) { this.setSwellDir(1); } int i = this.getSwellDir(); if (i > 0 && this.swell == 0) { this.playSound(SoundEvents.CREEPER_PRIMED, 1.0F, 0.5F); this.gameEvent(GameEvent.PRIME_FUSE); } this.swell += i; if (this.swell < 0) { this.swell = 0; } if (this.swell >= this.maxSwell) { this.swell = this.maxSwell; this.explodeCreeper(); } } super.tick(); } @Override public void setTarget(@Nullable LivingEntity livingEntity) { if (!(livingEntity instanceof Goat)) { super.setTarget(livingEntity); } } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.CREEPER_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.CREEPER_DEATH; } @Override protected void dropCustomDeathLoot(ServerLevel level, DamageSource damageSource, boolean recentlyHit) { super.dropCustomDeathLoot(level, damageSource, recentlyHit); Entity entity = damageSource.getEntity(); if (entity != this && entity instanceof Creeper creeper && creeper.canDropMobsSkull()) { creeper.increaseDroppedSkulls(); this.spawnAtLocation(level, Items.CREEPER_HEAD); } } @Override public boolean doHurtTarget(ServerLevel level, Entity source) { return true; } public boolean isPowered() { return this.entityData.get(DATA_IS_POWERED); } /** * Params: (Float)Render tick. Returns the intensity of the creeper's flash when it is ignited. */ public float getSwelling(float partialTicks) { return Mth.lerp(partialTicks, (float)this.oldSwell, (float)this.swell) / (this.maxSwell - 2); } /** * Returns the current state of creeper, -1 is idle, 1 is 'in fuse' */ public int getSwellDir() { return this.entityData.get(DATA_SWELL_DIR); } /** * Sets the state of creeper, -1 to idle and 1 to be 'in fuse' */ public void setSwellDir(int state) { this.entityData.set(DATA_SWELL_DIR, state); } @Override public void thunderHit(ServerLevel level, LightningBolt lightning) { super.thunderHit(level, lightning); this.entityData.set(DATA_IS_POWERED, true); } @Override protected InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); if (itemStack.is(ItemTags.CREEPER_IGNITERS)) { SoundEvent soundEvent = itemStack.is(Items.FIRE_CHARGE) ? SoundEvents.FIRECHARGE_USE : SoundEvents.FLINTANDSTEEL_USE; this.level().playSound(player, this.getX(), this.getY(), this.getZ(), soundEvent, this.getSoundSource(), 1.0F, this.random.nextFloat() * 0.4F + 0.8F); if (!this.level().isClientSide) { this.ignite(); if (!itemStack.isDamageableItem()) { itemStack.shrink(1); } else { itemStack.hurtAndBreak(1, player, getSlotForHand(hand)); } } return InteractionResult.SUCCESS; } else { return super.mobInteract(player, hand); } } /** * Creates an explosion as determined by this creeper's power and explosion radius. */ private void explodeCreeper() { if (this.level() instanceof ServerLevel serverLevel) { float f = this.isPowered() ? 2.0F : 1.0F; this.dead = true; serverLevel.explode(this, this.getX(), this.getY(), this.getZ(), this.explosionRadius * f, Level.ExplosionInteraction.MOB); this.spawnLingeringCloud(); this.triggerOnDeathMobEffects(serverLevel, Entity.RemovalReason.KILLED); this.discard(); } } private void spawnLingeringCloud() { Collection collection = this.getActiveEffects(); if (!collection.isEmpty()) { AreaEffectCloud areaEffectCloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ()); areaEffectCloud.setRadius(2.5F); areaEffectCloud.setRadiusOnUse(-0.5F); areaEffectCloud.setWaitTime(10); areaEffectCloud.setDuration(areaEffectCloud.getDuration() / 2); areaEffectCloud.setRadiusPerTick(-areaEffectCloud.getRadius() / areaEffectCloud.getDuration()); for (MobEffectInstance mobEffectInstance : collection) { areaEffectCloud.addEffect(new MobEffectInstance(mobEffectInstance)); } this.level().addFreshEntity(areaEffectCloud); } } public boolean isIgnited() { return this.entityData.get(DATA_IS_IGNITED); } public void ignite() { this.entityData.set(DATA_IS_IGNITED, true); } /** * Returns {@code true} if an entity is able to drop its skull due to being blown up by this creeper. * * Does not test if this creeper is charged, the caller must do that. However, does test the doMobLoot gamerule. */ public boolean canDropMobsSkull() { return this.isPowered() && this.droppedSkulls < 1; } public void increaseDroppedSkulls() { this.droppedSkulls++; } }