minecraft-src/net/minecraft/world/entity/monster/Creeper.java
2025-07-04 02:00:41 +03:00

292 lines
9.8 KiB
Java

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<Integer> DATA_SWELL_DIR = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Boolean> DATA_IS_POWERED = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Boolean> 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<? extends Creeper> 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 compound) {
super.addAdditionalSaveData(compound);
if (this.entityData.get(DATA_IS_POWERED)) {
compound.putBoolean("powered", true);
}
compound.putShort("Fuse", (short)this.maxSwell);
compound.putByte("ExplosionRadius", (byte)this.explosionRadius);
compound.putBoolean("ignited", this.isIgnited());
}
@Override
public void readAdditionalSaveData(CompoundTag compound) {
super.readAdditionalSaveData(compound);
this.entityData.set(DATA_IS_POWERED, compound.getBoolean("powered"));
if (compound.contains("Fuse", 99)) {
this.maxSwell = compound.getShort("Fuse");
}
if (compound.contains("ExplosionRadius", 99)) {
this.explosionRadius = compound.getByte("ExplosionRadius");
}
if (compound.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 serverLevel, Entity entity) {
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<MobEffectInstance> 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++;
}
}