blob: 68f906ec043c50fc6b9abaa647ec85d208e6cbfa [file] [log] [blame] [raw]
package net.glowstone.entity;
import com.flowpowered.networking.Message;
import lombok.Getter;
import net.glowstone.EventFactory;
import net.glowstone.constants.GlowPotionEffect;
import net.glowstone.entity.meta.MetadataIndex;
import net.glowstone.inventory.EquipmentMonitor;
import net.glowstone.net.message.play.entity.EntityEffectMessage;
import net.glowstone.net.message.play.entity.EntityEquipmentMessage;
import net.glowstone.net.message.play.entity.EntityRemoveEffectMessage;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.*;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scoreboard.Criterias;
import org.bukkit.scoreboard.Objective;
import org.bukkit.util.BlockIterator;
import org.bukkit.util.Vector;
import java.util.*;
/**
* A GlowLivingEntity is a {@link org.bukkit.entity.Player} or {@link org.bukkit.entity.Monster}.
*
* @author Graham Edgecombe.
*/
public abstract class GlowLivingEntity extends GlowEntity implements LivingEntity {
/**
* Potion effects on the entity.
*/
private final Map<PotionEffectType, PotionEffect> potionEffects = new HashMap<>();
/**
* The entity's health.
*/
protected double health;
/**
* The entity's max health.
*/
protected double maxHealth;
/**
* The magnitude of the last damage the entity took.
*/
private double lastDamage;
/**
* How long the entity has until it runs out of air.
*/
private int airTicks = 300;
/**
* The maximum amount of air the entity can hold.
*/
private int maximumAir = 300;
/**
* The number of ticks remaining in the invincibility period.
*/
private int noDamageTicks = 0;
/**
* The default length of the invincibility period.
*/
private int maxNoDamageTicks = 10;
/**
* Whether the entity should be removed if it is too distant from players.
*/
private boolean removeDistance;
/**
* Whether the (non-Player) entity can pick up armor and tools.
*/
private boolean pickupItems;
/**
* Monitor for the equipment of this entity.
*/
private EquipmentMonitor equipmentMonitor = new EquipmentMonitor(this);
/**
* The LivingEntity's AttributeManager.
*/
private final AttributeManager attributeManager;
/**
* The LivingEntity's number of ticks since death
*/
@Getter
private int deathTicks = 0;
/**
* Creates a mob within the specified world.
*
* @param location The location.
*/
public GlowLivingEntity(Location location) {
this(location, 20);
}
/**
* Creates a mob within the specified world.
*
* @param location The location.
*/
protected GlowLivingEntity(Location location, double maxHealth) {
super(location);
attributeManager = new AttributeManager(this);
this.maxHealth = maxHealth;
attributeManager.setProperty(AttributeManager.Key.KEY_MAX_HEALTH, maxHealth);
health = maxHealth;
}
////////////////////////////////////////////////////////////////////////////
// Internals
@Override
public void pulse() {
super.pulse();
if (isDead()) {
deathTicks++;
if (deathTicks >= 20 && this.getClass() != GlowPlayer.class) {
remove();
}
}
// invulnerability
if (noDamageTicks > 0) {
--noDamageTicks;
}
Material mat = getEyeLocation().getBlock().getType();
// breathing
if (mat == Material.WATER || mat == Material.STATIONARY_WATER) {
if (canTakeDamage(EntityDamageEvent.DamageCause.DROWNING)) {
--airTicks;
if (airTicks <= -20) {
airTicks = 0;
damage(1, EntityDamageEvent.DamageCause.DROWNING);
}
}
} else {
airTicks = maximumAir;
}
if (isTouchingMaterial(Material.CACTUS) && canTakeDamage(EntityDamageEvent.DamageCause.CONTACT)) {
damage(1, EntityDamageEvent.DamageCause.CONTACT);
}
if (location.getY() < -64) { // no canTakeDamage call - pierces through game modes
damage(4, EntityDamageEvent.DamageCause.VOID);
}
if (isWithinSolidBlock())
damage(1, EntityDamageEvent.DamageCause.SUFFOCATION);
// potion effects
List<PotionEffect> effects = new ArrayList<>(potionEffects.values());
for (PotionEffect effect : effects) {
// pulse effect
PotionEffectType type = effect.getType();
GlowPotionEffect glowType = GlowPotionEffect.getEffect(type);
if (glowType != null) {
glowType.pulse(this, effect);
}
if (effect.getDuration() > 0) {
// reduce duration and re-add
addPotionEffect(new PotionEffect(type, effect.getDuration() - 1, effect.getAmplifier(), effect.isAmbient()), true);
} else {
// remove
removePotionEffect(type);
}
}
}
@Override
public void reset() {
super.reset();
equipmentMonitor.resetChanges();
}
@Override
public List<Message> createUpdateMessage() {
List<Message> messages = super.createUpdateMessage();
for (EquipmentMonitor.Entry change : equipmentMonitor.getChanges()) {
messages.add(new EntityEquipmentMessage(id, change.slot, change.item));
}
attributeManager.applyMessages(messages);
return messages;
}
public AttributeManager getAttributeManager() {
return attributeManager;
}
////////////////////////////////////////////////////////////////////////////
// Properties
@Override
public double getEyeHeight() {
return 0;
}
@Override
public double getEyeHeight(boolean ignoreSneaking) {
return getEyeHeight();
}
@Override
public Location getEyeLocation() {
return getLocation().add(0, getEyeHeight(), 0);
}
@Override
public Player getKiller() {
return null;
}
@Override
public boolean hasLineOfSight(Entity other) {
return false;
}
@Override
public EntityEquipment getEquipment() {
return null;
}
////////////////////////////////////////////////////////////////////////////
// Properties
@Override
public int getNoDamageTicks() {
return noDamageTicks;
}
@Override
public void setNoDamageTicks(int ticks) {
noDamageTicks = ticks;
}
@Override
public int getMaximumNoDamageTicks() {
return maxNoDamageTicks;
}
@Override
public void setMaximumNoDamageTicks(int ticks) {
maxNoDamageTicks = ticks;
}
@Override
public int getRemainingAir() {
return airTicks;
}
@Override
public void setRemainingAir(int ticks) {
airTicks = Math.min(ticks, maximumAir);
}
@Override
public int getMaximumAir() {
return maximumAir;
}
@Override
public void setMaximumAir(int ticks) {
maximumAir = Math.max(0, ticks);
}
@Override
public boolean getRemoveWhenFarAway() {
return removeDistance;
}
@Override
public void setRemoveWhenFarAway(boolean remove) {
removeDistance = remove;
}
@Override
public boolean getCanPickupItems() {
return pickupItems;
}
@Override
public void setCanPickupItems(boolean pickup) {
pickupItems = pickup;
}
/**
* Get the hurt sound of this entity, or null for silence.
*
* @return the hurt sound if available
*/
protected Sound getHurtSound() {
return null;
}
/**
* Get the death sound of this entity, or null for silence.
*
* @return the death sound if available
*/
protected Sound getDeathSound() {
return null;
}
/**
* Get whether this entity should take damage from the specified source.
* Usually used to check environmental sources such as drowning.
*
* @param damageCause the damage source to check
* @return whether this entity can take damage from the source
*/
public boolean canTakeDamage(EntityDamageEvent.DamageCause damageCause) {
return true;
}
////////////////////////////////////////////////////////////////////////////
// Line of Sight
private List<Block> getLineOfSight(Set<Material> transparent, int maxDistance, int maxLength) {
// same limit as CraftBukkit
if (maxDistance > 120) {
maxDistance = 120;
}
LinkedList<Block> blocks = new LinkedList<>();
Iterator<Block> itr = new BlockIterator(this, maxDistance);
while (itr.hasNext()) {
Block block = itr.next();
blocks.add(block);
if (maxLength != 0 && blocks.size() > maxLength) {
blocks.removeFirst();
}
Material material = block.getType();
if (transparent == null) {
if (material != Material.AIR) {
break;
}
} else {
if (!transparent.contains(material)) {
break;
}
}
}
return blocks;
}
private List<Block> getLineOfSight(HashSet<Byte> transparent, int maxDistance, int maxLength) {
Set<Material> materials = new HashSet<Material>();
Iterator<Byte> itr = transparent.iterator();
while (itr.hasNext()) {
byte b = itr.next().byteValue();
materials.add(Material.getMaterial((int) b));
}
return getLineOfSight(materials, maxDistance, maxLength);
}
@Override
public List<Block> getLineOfSight(Set<Material> transparent, int maxDistance) {
return getLineOfSight(transparent, maxDistance, 0);
}
@Deprecated
@Override
public List<Block> getLineOfSight(HashSet<Byte> transparent, int maxDistance) {
return getLineOfSight(transparent, maxDistance, 0);
}
@Deprecated
@Override
public Block getTargetBlock(HashSet<Byte> transparent, int maxDistance) {
return getLineOfSight(transparent, maxDistance).get(0);
}
@Override
public Block getTargetBlock(Set<Material> materials, int maxDistance) {
return getLineOfSight(materials, maxDistance).get(0);
}
@Deprecated
@Override
public List<Block> getLastTwoTargetBlocks(HashSet<Byte> transparent, int maxDistance) {
return getLineOfSight(transparent, maxDistance, 2);
}
@Override
public List<Block> getLastTwoTargetBlocks(Set<Material> materials, int maxDistance) {
return getLineOfSight(materials, maxDistance, 2);
}
/**
* Returns whether the entity's eye location is within a solid block
*/
public boolean isWithinSolidBlock() {
return getEyeLocation().getBlock().getType().isOccluding();
}
////////////////////////////////////////////////////////////////////////////
// Projectiles
@Override
public Egg throwEgg() {
return launchProjectile(Egg.class);
}
@Override
public Snowball throwSnowball() {
return launchProjectile(Snowball.class);
}
@Override
public Arrow shootArrow() {
return launchProjectile(Arrow.class);
}
@Override
public <T extends Projectile> T launchProjectile(Class<? extends T> projectile) {
return launchProjectile(projectile, getLocation().getDirection()); // todo: multiply by some speed
}
@Override
public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity) {
T entity = world.spawn(getEyeLocation(), projectile);
entity.setVelocity(velocity);
return entity;
}
////////////////////////////////////////////////////////////////////////////
// Health
@Override
public double getHealth() {
return health;
}
@Override
public void setHealth(double health) {
if (health < 0) health = 0;
if (health > getMaxHealth()) health = getMaxHealth();
this.health = health;
metadata.set(MetadataIndex.HEALTH, (float) health);
for (Objective objective : getServer().getScoreboardManager().getMainScoreboard().getObjectivesByCriteria(Criterias.HEALTH)) {
objective.getScore(this.getName()).setScore((int) health);
}
if (health == 0) {
active = false;
Sound deathSound = getDeathSound();
if (deathSound != null) {
world.playSound(location, deathSound, 1.0f, 1.0f);
}
playEffect(EntityEffect.DEATH);
// todo: drop items
}
}
@Override
public void damage(double amount) {
damage(amount, null, EntityDamageEvent.DamageCause.CUSTOM);
}
@Override
public void damage(double amount, Entity source) {
damage(amount, source, EntityDamageEvent.DamageCause.CUSTOM);
}
@Override
public void damage(double amount, EntityDamageEvent.DamageCause cause) {
damage(amount, null, cause);
}
@Override
public void damage(double amount, Entity source, EntityDamageEvent.DamageCause cause) {
// invincibility timer
if (noDamageTicks > 0 || health <= 0 || !canTakeDamage(cause)) {
return;
} else {
noDamageTicks = maxNoDamageTicks;
}
// fire resistance
if (cause != null && hasPotionEffect(PotionEffectType.FIRE_RESISTANCE)) {
switch (cause) {
case PROJECTILE:
if (source == null || !(source instanceof Fireball)) {
break;
}
case FIRE:
case FIRE_TICK:
case LAVA:
return;
}
}
// fire event
// todo: use damage modifier system
EntityDamageEvent event;
if (source == null) {
event = new EntityDamageEvent(this, cause, amount);
} else {
event = new EntityDamageByEntityEvent(source, this, cause, amount);
}
EventFactory.callEvent(event);
if (event.isCancelled()) {
return;
}
if (source != null && source instanceof GlowPlayer) {
((GlowPlayer) source).addExhaustion(0.3f);
}
// apply damage
amount = event.getFinalDamage();
lastDamage = amount;
setHealth(health - amount);
playEffect(EntityEffect.HURT);
// play sounds, handle death
if (health > 0.0) {
Sound hurtSound = getHurtSound();
if (hurtSound != null) {
world.playSound(location, hurtSound, 1.0f, 1.0f);
}
}
}
@Override
public double getMaxHealth() {
return attributeManager.getPropertyValue(AttributeManager.Key.KEY_MAX_HEALTH);
}
@Override
public void setMaxHealth(double health) {
attributeManager.setProperty(AttributeManager.Key.KEY_MAX_HEALTH, health);
}
@Override
public void resetMaxHealth() {
setMaxHealth(maxHealth);
}
@Override
public double getLastDamage() {
return lastDamage;
}
@Override
public void setLastDamage(double damage) {
lastDamage = damage;
}
////////////////////////////////////////////////////////////////////////////
// Invalid health methods
@Override
public void _INVALID_damage(int amount) {
damage(amount);
}
@Override
public int _INVALID_getLastDamage() {
return (int) getLastDamage();
}
@Override
public void _INVALID_setLastDamage(int damage) {
setLastDamage(damage);
}
@Override
public void _INVALID_setMaxHealth(int health) {
setMaxHealth(health);
}
@Override
public int _INVALID_getMaxHealth() {
return (int) getMaxHealth();
}
@Override
public void _INVALID_damage(int amount, Entity source) {
damage(amount, source);
}
@Override
public int _INVALID_getHealth() {
return (int) getHealth();
}
@Override
public void _INVALID_setHealth(int health) {
setHealth(health);
}
////////////////////////////////////////////////////////////////////////////
// Potion effects
@Override
public boolean addPotionEffect(PotionEffect effect) {
return addPotionEffect(effect, false);
}
@Override
public boolean addPotionEffect(PotionEffect effect, boolean force) {
if (potionEffects.containsKey(effect.getType())) {
if (force) {
removePotionEffect(effect.getType());
} else {
return false;
}
}
potionEffects.put(effect.getType(), effect);
EntityEffectMessage msg = new EntityEffectMessage(getEntityId(), effect.getType().getId(), effect.getAmplifier(), effect.getDuration(), effect.isAmbient());
for (GlowPlayer player : world.getRawPlayers()) {
if (player == this) {
// special handling for players having a different view of themselves
player.getSession().send(new EntityEffectMessage(0, effect.getType().getId(), effect.getAmplifier(), effect.getDuration(), effect.isAmbient()));
} else if (player.canSeeEntity(this)) {
player.getSession().send(msg);
}
}
return true;
}
@Override
public boolean addPotionEffects(Collection<PotionEffect> effects) {
boolean result = true;
for (PotionEffect effect : effects) {
if (!addPotionEffect(effect)) {
result = false;
}
}
return result;
}
@Override
public boolean hasPotionEffect(PotionEffectType type) {
return potionEffects.containsKey(type);
}
@Override
public void removePotionEffect(PotionEffectType type) {
if (!hasPotionEffect(type)) return;
potionEffects.remove(type);
EntityRemoveEffectMessage msg = new EntityRemoveEffectMessage(getEntityId(), type.getId());
for (GlowPlayer player : world.getRawPlayers()) {
if (player == this) {
// special handling for players having a different view of themselves
player.getSession().send(new EntityRemoveEffectMessage(0, type.getId()));
} else if (player.canSeeEntity(this)) {
player.getSession().send(msg);
}
}
}
@Override
public Collection<PotionEffect> getActivePotionEffects() {
return Collections.unmodifiableCollection(potionEffects.values());
}
@Override
public void setOnGround(boolean onGround) {
super.setOnGround(onGround);
if (onGround && getFallDistance() > 3) {
if (getClass() == GlowPlayer.class && ((GlowPlayer) this).getAllowFlight()) {
return;
}
float damage = this.getFallDistance() - 3;
damage = Math.round(damage);
if (damage == 0) {
setFallDistance(0);
return;
}
EntityDamageEvent ev = new EntityDamageEvent(this, EntityDamageEvent.DamageCause.FALL, damage);
this.getServer().getPluginManager().callEvent(ev);
if (ev.isCancelled()) {
setFallDistance(0);
return;
}
this.setLastDamageCause(ev);
this.damage(ev.getDamage());
}
this.setFallDistance(0);
}
////////////////////////////////////////////////////////////////////////////
// Leashes
@Override
public boolean isLeashed() {
return false;
}
@Override
public Entity getLeashHolder() throws IllegalStateException {
return null;
}
@Override
public boolean setLeashHolder(Entity holder) {
return false;
}
}