package net.glowstone.entity;
import com.flowpowered.network.Message;
import lombok.Getter;
import net.glowstone.EventFactory;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.ItemTable;
import net.glowstone.block.blocktype.BlockType;
import net.glowstone.constants.GlowPotionEffect;
import net.glowstone.entity.AttributeManager.Key;
import net.glowstone.entity.ai.MobState;
import net.glowstone.entity.ai.TaskManager;
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.EntityHeadRotationMessage;
import net.glowstone.net.message.play.entity.EntityRemoveEffectMessage;
import net.glowstone.util.InventoryUtil;
import net.glowstone.util.Position;
import net.glowstone.util.RayUtil;
import net.glowstone.util.SoundUtil;
import net.glowstone.util.loot.LootData;
import net.glowstone.util.loot.LootingManager;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.*;
import org.bukkit.event.entity.*;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
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.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* A GlowLivingEntity is a {@link Player} or {@link 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 LivingEntity's AttributeManager.
*/
private final AttributeManager attributeManager;
/**
* 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;
/**
* 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.
*/
@Getter
private EquipmentMonitor equipmentMonitor = new EquipmentMonitor(this);
/**
* The LivingEntity's number of ticks since death
*/
@Getter
protected int deathTicks;
/**
* Whether the entity can automatically glide when falling with an Elytra equipped.
* This value is ignored for players.
*/
private boolean fallFlying;
/**
* Ticks until the next ambient sound roll.
*/
private int nextAmbientTime = 1;
/**
* The last entity which damaged this living entity.
*/
private Entity lastDamager;
/**
* The head rotation of the living entity, if applicable.
*/
private float headYaw;
/**
* Whether the headYaw value should be updated.
*/
private boolean headRotated;
/**
* The entity's AI task manager.
*/
protected final TaskManager taskManager;
/**
* The entity's current state;
*/
private MobState aiState = MobState.NO_AI;
/**
* If this entity has swam in lava (for fire application).
*/
private boolean swamInLava;
/**
* The entity's movement as a unit vector, applied each tick
* according to the entity's speed.
*
* The y value is not used. X is used for forward movement
* and z is used for sideways movement. These values are relative
* to the entity's current yaw.
*/
protected Vector movement = new Vector();
/**
* The speed multiplier of the entity.
*/
protected double speed = 1;
/**
* 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.
* @param maxHealth The max health of this mob.
*/
protected GlowLivingEntity(Location location, double maxHealth) {
super(location);
attributeManager = new AttributeManager(this);
this.maxHealth = maxHealth;
attributeManager.setProperty(Key.KEY_MAX_HEALTH, maxHealth);
health = maxHealth;
taskManager = new TaskManager(this);
}
////////////////////////////////////////////////////////////////////////////
// Internals
@Override
public void pulse() {
super.pulse();
if (isDead()) {
deathTicks++;
if (deathTicks >= 20 && 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(DamageCause.DROWNING)) {
--airTicks;
if (airTicks <= -20) {
airTicks = 0;
damage(1, DamageCause.DROWNING);
}
}
} else {
airTicks = maximumAir;
}
if (isTouchingMaterial(Material.CACTUS)) {
damage(1, DamageCause.CONTACT);
}
if (location.getY() < -64) { // no canTakeDamage call - pierces through game modes
damage(4, DamageCause.VOID);
}
if (isWithinSolidBlock())
damage(1, DamageCause.SUFFOCATION);
if (getLocation().getBlock().getType() == Material.LAVA || getLocation().getBlock().getType() == Material.STATIONARY_LAVA) {
damage(4, DamageCause.LAVA);
if (swamInLava) {
setFireTicks(getFireTicks() + 2);
} else {
setFireTicks(getFireTicks() + 300);
swamInLava = true;
}
} else {
swamInLava = false;
if (getLocation().getBlock().getType() == Material.WATER || getLocation().getBlock().getType() == Material.STATIONARY_WATER) {
setFireTicks(0);
}
}
// 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);
}
}
if (getFireTicks() > 0 && getFireTicks() % 20 == 0) {
damage(1, DamageCause.FIRE_TICK);
}
GlowBlock under = (GlowBlock) getLocation().getBlock().getRelative(BlockFace.DOWN);
BlockType type = ItemTable.instance().getBlock(under.getType());
if (type != null) {
type.onEntityStep(under, this);
}
nextAmbientTime--;
if (!isDead() && getAmbientSound() != null && nextAmbientTime == 0 && !isSilent()) {
double v = ThreadLocalRandom.current().nextDouble();
if (v <= 0.2) {
world.playSound(getLocation(), getAmbientSound(), getSoundVolume(), getSoundPitch());
}
}
if (nextAmbientTime == 0) {
nextAmbientTime = getAmbientDelay();
}
}
@Override
protected void pulsePhysics() {
// drag application
movement.multiply(airDrag);
// convert movement x/z to a velocity
Vector velMovement = getVelocityFromMovement();
velocity.add(velMovement);
super.pulsePhysics();
}
protected Vector getVelocityFromMovement() {
// ensure movement vector is in correct format
movement.setY(0);
double mag = movement.getX() * movement.getX() + movement.getZ() * movement.getZ();
// don't do insignificant movement
if (mag < 0.01) {
return new Vector();
}
// unit vector of movement
movement.setX(movement.getX() / mag);
movement.setZ(movement.getZ() / mag);
// scale to how fast the entity can go
mag *= speed;
Vector movement = this.movement.clone();
movement.multiply(mag);
// make velocity vector relative to where the entity is facing
double yaw = Math.toRadians(location.getYaw());
double z = Math.sin(yaw);
double x = Math.cos(yaw);
movement.setX(movement.getZ() * x - movement.getX() * z);
movement.setZ(movement.getX() * x + movement.getZ() * z);
// apply the movement multiplier
if (!isOnGround() || location.getBlock().isLiquid()) {
// constant multiplier in liquid or not on ground
movement.multiply(0.02);
} else {
this.slipMultiplier = ((GlowBlock) location.getBlock()).getMaterialValues().getSlipperiness();
double slipperiness = slipMultiplier * 0.91;
movement.multiply(0.1 * (0.1627714 / Math.pow(slipperiness, 3)));
}
return movement;
}
public Vector getMovement() {
return movement.clone();
}
public void setMovement(Vector movement) {
this.movement = movement;
}
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
protected void jump() {
if (location.getBlock().isLiquid()) {
// jump out more when you breach the surface of the liquid
if (location.getBlock().getRelative(BlockFace.UP).isEmpty()) {
velocity.setY(velocity.getY() + 0.3);
}
// less jumping in liquid
velocity.setY(velocity.getY() + 0.04);
} else {
// jump normally
velocity.setY(velocity.getY() + 0.42);
}
}
@Override
public void reset() {
super.reset();
equipmentMonitor.resetChanges();
}
@Override
public List<Message> createUpdateMessage() {
List<Message> messages = super.createUpdateMessage();
messages.addAll(equipmentMonitor.getChanges().stream().map(change -> new EntityEquipmentMessage(id, change.slot, change.item)).collect(Collectors.toList()));
if (headRotated) {
messages.add(new EntityHeadRotationMessage(id, Position.getIntHeadYaw(headYaw)));
headRotated = false;
}
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;
}
public float getHeadYaw() {
return headYaw;
}
public void setHeadYaw(float headYaw) {
this.headYaw = headYaw;
this.headRotated = true;
}
@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 the ambient sound this entity makes randomly, or null for silence.
*
* @return the ambient sound if available
*/
protected Sound getAmbientSound() {
return null;
}
/**
* Get the minimal delay until the entity can produce an ambient sound.
*
* @return the minimal delay until the entity can produce an ambient sound
*/
protected int getAmbientDelay() {
return 80;
}
/**
* The volume of the sounds this entity makes
*
* @return the volume of the sounds
*/
protected float getSoundVolume() {
return 1.0F;
}
/**
* The pitch of the sounds this entity makes
*
* @return the pitch of the sounds
*/
protected float getSoundPitch() {
return SoundUtil.randomReal(0.2F) + 1F;
}
/**
* 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(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 = transparent.stream().map(Material::getMaterial).collect(Collectors.toSet());
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
*
* @return if the entity is in a solid block
*/
public boolean isWithinSolidBlock() {
return getEyeLocation().getBlock().getType().isOccluding();
}
////////////////////////////////////////////////////////////////////////////
// Projectiles
@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(getName()).setScore((int) health);
}
if (health <= 0) {
active = false;
Sound deathSound = getDeathSound();
if (deathSound != null && !isSilent()) {
world.playSound(location, deathSound, getSoundVolume(), getSoundPitch());
}
playEffect(EntityEffect.DEATH);
if (this instanceof GlowPlayer) {
GlowPlayer player = (GlowPlayer) this;
ItemStack mainHand = player.getInventory().getItemInMainHand();
ItemStack offHand = player.getInventory().getItemInOffHand();
if (!InventoryUtil.isEmpty(mainHand) && mainHand.getType() == Material.TOTEM) {
player.getInventory().setItemInMainHand(InventoryUtil.createEmptyStack());
player.setHealth(1.0);
active = true;
return;
} else if (!InventoryUtil.isEmpty(offHand) && offHand.getType() == Material.TOTEM) {
player.getInventory().setItemInOffHand(InventoryUtil.createEmptyStack());
player.setHealth(1.0);
active = true;
return;
}
List<ItemStack> items = new ArrayList<>();
if (!world.getGameRuleMap().getBoolean("keepInventory")) {
items = Arrays.stream(player.getInventory().getContents()).filter(stack -> !InventoryUtil.isEmpty(stack)).collect(Collectors.toList());
player.getInventory().clear();
}
PlayerDeathEvent event = new PlayerDeathEvent(player, items, 0, player.getDisplayName() + " died.");
EventFactory.callEvent(event);
server.broadcastMessage(event.getDeathMessage());
for (ItemStack item : items) {
world.dropItemNaturally(getLocation(), item);
}
player.incrementStatistic(Statistic.DEATHS);
} else {
EntityDeathEvent deathEvent = new EntityDeathEvent(this, new ArrayList<>());
if (world.getGameRuleMap().getBoolean("doMobLoot")) {
LootData data = LootingManager.generate(this);
Collections.addAll(deathEvent.getDrops(), data.getItems());
// todo: drop experience
}
deathEvent = EventFactory.callEvent(deathEvent);
for (ItemStack item : deathEvent.getDrops()) {
world.dropItemNaturally(getLocation(), item);
}
}
}
}
@Override
public void damage(double amount) {
damage(amount, null, DamageCause.CUSTOM);
}
@Override
public void damage(double amount, Entity source) {
damage(amount, source, DamageCause.CUSTOM);
}
@Override
public void damage(double amount, DamageCause cause) {
damage(amount, null, cause);
}
@Override
public void damage(double amount, Entity source, DamageCause cause) {
// invincibility timer
if (noDamageTicks > 0 || health <= 0 || !canTakeDamage(cause) || isInvulnerable()) {
return;
} else {
noDamageTicks = maxNoDamageTicks;
}
// fire resistance
if (cause != null && hasPotionEffect(PotionEffectType.FIRE_RESISTANCE)) {
switch (cause) {
case PROJECTILE:
if (!(source instanceof Fireball)) {
break;
}
case FIRE:
case FIRE_TICK:
case LAVA:
return;
}
}
// armor damage protection
// formula source: http://minecraft.gamepedia.com/Armor#Damage_Protection
double defensePoints = getAttributeManager().getPropertyValue(Key.KEY_ARMOR);
double toughness = getAttributeManager().getPropertyValue(Key.KEY_ARMOR_TOUGHNESS);
amount = amount * (1 - Math.min(20.0, Math.max(defensePoints / 5.0, defensePoints - amount / (2.0 + toughness / 4.0))) / 25);
// fire event
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;
}
// apply damage
amount = event.getFinalDamage();
lastDamage = amount;
setHealth(health - amount);
playEffect(EntityEffect.HURT);
if (cause == DamageCause.ENTITY_ATTACK && source != null) {
Vector distance = RayUtil.getRayBetween(getLocation(), ((LivingEntity) source).getEyeLocation());
Vector rayLength = RayUtil.getVelocityRay(distance).normalize();
Vector currentVelocity = getVelocity();
currentVelocity.add(rayLength.multiply(((amount + 1) / 2d) * 0.05));
setVelocity(currentVelocity);
}
// play sounds, handle death
if (health > 0) {
Sound hurtSound = getHurtSound();
if (hurtSound != null && !isSilent()) {
world.playSound(location, hurtSound, getSoundVolume(), getSoundPitch());
}
}
setLastDamager(source);
}
@Override
public double getMaxHealth() {
return attributeManager.getPropertyValue(Key.KEY_MAX_HEALTH);
}
@Override
public void setMaxHealth(double health) {
attributeManager.setProperty(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;
}
public Entity getLastDamager() {
return lastDamager;
}
public void setLastDamager(Entity lastDamager) {
this.lastDamager = lastDamager;
}
////////////////////////////////////////////////////////////////////////////
// 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 PotionEffect getPotionEffect(PotionEffectType potionEffectType) {
return null;
}
@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) {
if (onGround && getFallDistance() > 3f) {
float damage = getFallDistance() - 3f;
damage = Math.round(damage);
if (damage > 0f) {
Material standingType = location.getBlock().getRelative(BlockFace.DOWN).getType();
// todo: only when bouncing
if (standingType == Material.SLIME_BLOCK) {
damage = 0f;
}
if (standingType == Material.HAY_BLOCK) {
damage *= 0.2f;
}
EntityDamageEvent ev = new EntityDamageEvent(this, DamageCause.FALL, damage);
getServer().getPluginManager().callEvent(ev);
if (!ev.isCancelled()) {
setLastDamageCause(ev);
damage(ev.getDamage(), DamageCause.FALL);
}
}
}
super.setOnGround(onGround);
}
public boolean isFallFlying() {
return fallFlying;
}
public void setFallFlying(boolean fallFlying) {
this.fallFlying = fallFlying;
}
////////////////////////////////////////////////////////////////////////////
// Leashes
@Override
public boolean isLeashed() {
return false;
}
@Override
public Entity getLeashHolder() throws IllegalStateException {
return null;
}
@Override
public boolean setLeashHolder(Entity holder) {
return false;
}
@Override
public boolean isGliding() {
return metadata.getBit(MetadataIndex.STATUS, MetadataIndex.StatusFlags.GLIDING);
}
@Override
public void setGliding(boolean gliding) {
if (EventFactory.callEvent(new EntityToggleGlideEvent(this, gliding)).isCancelled()) {
return;
}
metadata.setBit(MetadataIndex.STATUS, MetadataIndex.StatusFlags.GLIDING, gliding);
}
public MobState getState() {
return aiState;
}
public void setState(MobState state) {
if (aiState != state) {
aiState = state;
getTaskManager().updateState();
}
}
@Override
public void setAI(boolean ai) {
if (ai) {
if (aiState == MobState.NO_AI) {
setState(MobState.IDLE);
}
} else {
setState(MobState.NO_AI);
}
}
@Override
public boolean hasAI() {
return aiState != MobState.NO_AI;
}
@Override
public void setCollidable(boolean collidable) {
// todo: 1.11
}
@Override
public boolean isCollidable() {
// todo: 1.11
return true;
}
@Override
public int getArrowsStuck() {
// todo: 1.11
return 0;
}
@Override
public void setArrowsStuck(int arrowsStuck) {
// todo: 1.11
}
@Override
public AttributeInstance getAttribute(Attribute attribute) {
// todo: 1.11
return null;
}
public TaskManager getTaskManager() {
return taskManager;
}
}