Minestom/src/main/java/net/minestom/server/entity/LivingEntity.java

432 lines
14 KiB
Java

package net.minestom.server.entity;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.entity.property.Attribute;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.EntityFireEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.inventory.EquipmentHandler;
import net.minestom.server.item.ItemStack;
import net.minestom.server.network.packet.PacketWriter;
import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.sound.Sound;
import net.minestom.server.sound.SoundCategory;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.time.TimeUnit;
import java.util.Set;
import java.util.function.Consumer;
public abstract class LivingEntity extends Entity implements EquipmentHandler {
protected boolean canPickupItem;
protected boolean isDead;
private float health;
// Bounding box used for items' pickup (see LivingEntity#setBoundingBox)
protected BoundingBox expandedBoundingBox;
private float[] attributeValues = new float[Attribute.values().length];
private boolean isHandActive;
private boolean offHand;
private boolean riptideSpinAttack;
// The number of arrows in entity
private int arrowCount;
/**
* Time at which this entity must be extinguished
*/
private long fireExtinguishTime;
/**
* Last time the fire damage was applied
*/
private long lastFireDamageTime;
/**
* Period, in ms, between two fire damage applications
*/
private long fireDamagePeriod = 1000L;
public LivingEntity(EntityType entityType, Position spawnPosition) {
super(entityType.getId(), spawnPosition);
setupAttributes();
setGravity(0.02f);
}
public LivingEntity(EntityType entityType) {
this(entityType, new Position());
}
@Override
public void update() {
if (isOnFire()) {
if (System.currentTimeMillis() > fireExtinguishTime) {
setOnFire(false);
} else {
if (System.currentTimeMillis() - lastFireDamageTime > fireDamagePeriod) {
damage(DamageType.ON_FIRE, 1.0f);
lastFireDamageTime = System.currentTimeMillis();
}
}
}
// Items picking
if (canPickupItem()) {
Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks
Set<Entity> entities = instance.getChunkEntities(chunk);
BoundingBox livingBoundingBox = expandedBoundingBox;
for (Entity entity : entities) {
if (entity instanceof ItemEntity) {
// Do not pickup if not visible
if (this instanceof Player && !entity.isViewer((Player) this))
continue;
ItemEntity itemEntity = (ItemEntity) entity;
if (!itemEntity.isPickable())
continue;
BoundingBox itemBoundingBox = itemEntity.getBoundingBox();
if (livingBoundingBox.intersect(itemBoundingBox)) {
synchronized (itemEntity) {
if (itemEntity.shouldRemove() || itemEntity.isRemoveScheduled())
continue;
ItemStack item = itemEntity.getItemStack();
PickupItemEvent pickupItemEvent = new PickupItemEvent(item);
callCancellableEvent(PickupItemEvent.class, pickupItemEvent, () -> {
CollectItemPacket collectItemPacket = new CollectItemPacket();
collectItemPacket.collectedEntityId = itemEntity.getEntityId();
collectItemPacket.collectorEntityId = getEntityId();
collectItemPacket.pickupItemCount = item.getAmount();
sendPacketToViewersAndSelf(collectItemPacket);
entity.remove();
});
}
}
}
}
}
}
@Override
public Consumer<PacketWriter> getMetadataConsumer() {
return packet -> {
super.getMetadataConsumer().accept(packet);
fillMetadataIndex(packet, 7);
fillMetadataIndex(packet, 8);
fillMetadataIndex(packet, 11);
};
}
@Override
protected void fillMetadataIndex(PacketWriter packet, int index) {
super.fillMetadataIndex(packet, index);
if (index == 7) {
packet.writeByte((byte) 7);
packet.writeByte(METADATA_BYTE);
byte activeHandValue = 0;
if (isHandActive) {
activeHandValue += 1;
if (offHand)
activeHandValue += 2;
if (riptideSpinAttack)
activeHandValue += 4;
}
packet.writeByte(activeHandValue);
} else if (index == 8) {
packet.writeByte((byte) 8);
packet.writeByte(METADATA_FLOAT);
packet.writeFloat(health);
} else if (index == 11) {
packet.writeByte((byte) 11);
packet.writeByte(METADATA_VARINT);
packet.writeVarInt(arrowCount);
}
}
public int getArrowCount() {
return arrowCount;
}
public void setArrowCount(int arrowCount) {
this.arrowCount = arrowCount;
sendMetadataIndex(11);
}
/**
* Kill the entity, trigger the {@link EntityDeathEvent} event
*/
public void kill() {
refreshIsDead(true); // So the entity isn't killed over and over again
triggerStatus((byte) 3); // Start death animation status
setHealth(0);
// Reset velocity
velocity.zero();
// Remove passengers if any
if (hasPassenger()) {
getPassengers().forEach(entity -> removePassenger(entity));
}
EntityDeathEvent entityDeathEvent = new EntityDeathEvent(this);
callEvent(EntityDeathEvent.class, entityDeathEvent);
}
/**
* Sets fire to this entity for a given duration
*
* @param duration duration in ticks of the effect
*/
public void setFireForDuration(int duration) {
setFireForDuration(duration, TimeUnit.TICK);
}
/**
* Sets fire to this entity for a given duration
*
* @param duration duration of the effect
* @param unit unit used to express the duration
*/
public void setFireForDuration(int duration, TimeUnit unit) {
EntityFireEvent entityFireEvent = new EntityFireEvent(this, duration, unit);
// Do not start fire event if the fire needs to be removed (< 0 duration)
if (duration > 0) {
callCancellableEvent(EntityFireEvent.class, entityFireEvent, () -> {
long fireTime = entityFireEvent.getFireTime(TimeUnit.MILLISECOND);
setOnFire(true);
fireExtinguishTime = System.currentTimeMillis() + fireTime;
});
} else {
fireExtinguishTime = System.currentTimeMillis();
}
}
/**
* @param type the damage type
* @param value the amount of damage
* @return true if damage has been applied, false if it didn't
*/
public boolean damage(DamageType type, float value) {
if (isImmune(type)) {
return false;
}
EntityDamageEvent entityDamageEvent = new EntityDamageEvent(type, value);
callCancellableEvent(EntityDamageEvent.class, entityDamageEvent, () -> {
float damage = entityDamageEvent.getDamage();
EntityAnimationPacket entityAnimationPacket = new EntityAnimationPacket();
entityAnimationPacket.entityId = getEntityId();
entityAnimationPacket.animation = EntityAnimationPacket.Animation.TAKE_DAMAGE;
sendPacketToViewersAndSelf(entityAnimationPacket);
// Additional hearts support
if (this instanceof Player) {
Player player = (Player) this;
final float additionalHearts = player.getAdditionalHearts();
if (additionalHearts > 0) {
if (damage > additionalHearts) {
damage -= additionalHearts;
player.setAdditionalHearts(0);
} else {
player.setAdditionalHearts(additionalHearts - damage);
damage = 0;
}
}
}
setHealth(getHealth() - damage);
// play damage sound
Sound sound = type.getSound(this);
if (sound != null) {
SoundCategory soundCategory;
if (this instanceof Player) {
soundCategory = SoundCategory.PLAYERS;
} else {
// TODO: separate living entity categories
soundCategory = SoundCategory.HOSTILE;
}
SoundEffectPacket damageSoundPacket = SoundEffectPacket.create(soundCategory, sound, getPosition().getX(), getPosition().getY(), getPosition().getZ(), 1.0f, 1.0f);
sendPacketToViewersAndSelf(damageSoundPacket);
}
});
return !entityDamageEvent.isCancelled();
}
/**
* Is this entity immune to the given type of damage?
*
* @param type the type of damage
* @return true iff this entity is immune to the given type of damage
*/
public boolean isImmune(DamageType type) {
return false;
}
public float getHealth() {
return health;
}
public void setHealth(float health) {
health = Math.min(health, getMaxHealth());
this.health = health;
if (this.health <= 0 && !isDead) {
kill();
}
sendMetadataIndex(8); // Health metadata index
}
public float getMaxHealth() {
return getAttributeValue(Attribute.MAX_HEALTH);
}
/**
* Set the heal of the entity as its max health
* retrieved from {@link #getAttributeValue(Attribute)} with the attribute {@link Attribute#MAX_HEALTH}
*/
public void heal() {
setHealth(getAttributeValue(Attribute.MAX_HEALTH));
}
/**
* Change the specified attribute value to {@code value}
*
* @param attribute The attribute to change
* @param value the new value of the attribute
*/
public void setAttribute(Attribute attribute, float value) {
this.attributeValues[attribute.ordinal()] = value;
}
/**
* Retrieve the attribute value set by {@link #setAttribute(Attribute, float)}
*
* @param attribute the attribute value to get
* @return the attribute value
*/
public float getAttributeValue(Attribute attribute) {
return this.attributeValues[attribute.ordinal()];
}
// Equipments
public void syncEquipments() {
for (EntityEquipmentPacket.Slot slot : EntityEquipmentPacket.Slot.values()) {
syncEquipment(slot);
}
}
public void syncEquipment(EntityEquipmentPacket.Slot slot) {
EntityEquipmentPacket entityEquipmentPacket = getEquipmentPacket(slot);
if (entityEquipmentPacket == null)
return;
sendPacketToViewers(entityEquipmentPacket);
}
protected EntityEquipmentPacket getEquipmentPacket(EntityEquipmentPacket.Slot slot) {
ItemStack itemStack = getEquipment(slot);
EntityEquipmentPacket equipmentPacket = new EntityEquipmentPacket();
equipmentPacket.entityId = getEntityId();
equipmentPacket.slot = slot;
equipmentPacket.itemStack = itemStack;
return equipmentPacket;
}
/**
* @return true if the entity is dead, false otherwise
*/
public boolean isDead() {
return isDead;
}
/**
* @return true if the entity is able to pickup items
*/
public boolean canPickupItem() {
return canPickupItem;
}
/**
* When set to false, the entity will not be able to pick {@link ItemEntity} on the ground
*
* @param canPickupItem can the entity pickup item
*/
public void setCanPickupItem(boolean canPickupItem) {
this.canPickupItem = canPickupItem;
}
@Override
public void setBoundingBox(float x, float y, float z) {
super.setBoundingBox(x, y, z);
this.expandedBoundingBox = getBoundingBox().expand(1, 0.5f, 1);
}
public void refreshActiveHand(boolean isHandActive, boolean offHand, boolean riptideSpinAttack) {
this.isHandActive = isHandActive;
this.offHand = offHand;
this.riptideSpinAttack = riptideSpinAttack;
sendPacketToViewers(getMetadataPacket());
}
protected void refreshIsDead(boolean isDead) {
this.isDead = isDead;
}
protected EntityPropertiesPacket getPropertiesPacket() {
EntityPropertiesPacket propertiesPacket = new EntityPropertiesPacket();
propertiesPacket.entityId = getEntityId();
int length = Attribute.values().length;
EntityPropertiesPacket.Property[] properties = new EntityPropertiesPacket.Property[length];
for (int i = 0; i < length; i++) {
Attribute attribute = Attribute.values()[i];
EntityPropertiesPacket.Property property = new EntityPropertiesPacket.Property();
float maxValue = attribute.getMaxVanillaValue();
float value = getAttributeValue(attribute);
value = value > maxValue ? maxValue : value; // Bypass vanilla limit client-side if needed (by sending the max value allowed)
property.key = attribute.getKey();
property.value = value;
properties[i] = property;
}
propertiesPacket.properties = properties;
return propertiesPacket;
}
private void setupAttributes() {
for (Attribute attribute : Attribute.values()) {
setAttribute(attribute, attribute.getDefaultValue());
}
}
@Override
protected void handleVoid() {
// Kill if in void
if (getInstance().isInVoid(this.position)) {
damage(DamageType.VOID, 10f);
}
}
public long getFireDamagePeriod() {
return fireDamagePeriod;
}
public void setFireDamagePeriod(long fireDamagePeriod) {
this.fireDamagePeriod = fireDamagePeriod;
}
}