package net.aufdemrand.denizen.npc.traits;
import net.aufdemrand.denizen.Settings;
import net.aufdemrand.denizen.objects.dEntity;
import net.aufdemrand.denizen.objects.dLocation;
import net.aufdemrand.denizen.objects.dNPC;
import net.aufdemrand.denizen.objects.dPlayer;
import net.aufdemrand.denizen.tags.BukkitTagContext;
import net.aufdemrand.denizen.utilities.DenizenAPI;
import net.aufdemrand.denizencore.objects.Duration;
import net.aufdemrand.denizencore.objects.Element;
import net.aufdemrand.denizencore.objects.aH;
import net.aufdemrand.denizencore.objects.dObject;
import net.aufdemrand.denizencore.tags.TagManager;
import net.aufdemrand.denizencore.utilities.CoreUtilities;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.event.DespawnReason;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.projectiles.ProjectileSource;
import java.util.HashMap;
import java.util.Map;
public class HealthTrait extends Trait implements Listener {
// <--[language]
// @name Health Trait
// @group NPC Traits
// @description
// By default, NPCs are invulnerable, unable to be damaged. If you want your NPC
// to be able to take damage, or use the left click as an interaction, it must
// have the health trait. The Health trait is automatically enabled if you set
// the damage trigger state to true.
//
// You can use the denizen vulnerable command to make your NPCs react to left
// click, but not take damage. - vulnerable state:false
//
// Enable Damage trigger via dScript: - trigger name:damage state:true
// Enable Health trait via dScript: - trait state:true health
// Enable Health trait via npc command: /npc health --set # (-r)
//
// Enable automatic respawn (default delay 300t): /npc health --respawndelay [delay as a duration]
// Set respawn location: - flag <npc> respawn_location:<location>
//
// Related Tags
// <@link tag npc.health>
// <@link tag npc.health.formatted>
// <@link tag npc.health.max>
// <@link tag npc.health.percentage>
// <@link tag npc.has_trait[health]>
//
// Related Mechanisms
// <@link mechanism health>
// <@link mechanism max_health>
//
// Related Commands
// <@link command heal>
// <@link command health>
// <@link command vulnerable>
//
// Related Actions
// <@link action on damage>
// <@link action on damaged>
// <@link action on no damage trigger>
//
// -->
// Saved to the C2 saves.yml
@Persist("animatedeath")
private boolean animatedeath = Settings.healthTraitAnimatedDeathEnabled();
@Persist("respawnondeath")
private boolean respawn = Settings.healthTraitRespawnEnabled();
@Persist("respawndelayinseconds")
private String respawnDelay = Settings.healthTraitRespawnDelay();
@Persist("respawnlocation")
private String respawnLocation = "<npc.flag[respawn_location] || <npc.location>>";
// internal
private dPlayer player = null;
private boolean dying = false;
private Location loc;
private int entityId = -1;
public Duration getRespawnDelay() {
return Duration.valueOf(respawnDelay);
}
public void setRespawnLocation(String string) {
if (aH.matchesLocation("location:" + string)) {
respawnLocation = string;
}
}
public void setRespawnDelay(int seconds) {
respawnDelay = String.valueOf(seconds);
}
public void setRespawnDelay(String string) {
if (aH.matchesDuration("duration:" + string)) {
respawnDelay = string;
}
}
public String getRespawnLocationAsString() {
return respawnLocation;
}
public Location getRespawnLocation() {
return dLocation.valueOf(TagManager.tag(respawnLocation, new BukkitTagContext(null,
dNPC.mirrorCitizensNPC(npc), false, null, false, null)));
}
public void setRespawnable(boolean respawnable) {
respawn = respawnable;
}
public boolean isRespawnable() {
return respawn;
}
public void animateOnDeath(boolean animate) {
animatedeath = animate;
}
public boolean animatesOnDeath() {
return animatedeath;
}
public Integer void_watcher_task = null;
/**
* Listens for spawn of an NPC and updates its health with the max health
* information for this trait.
*/
@Override
public void onSpawn() {
dying = false;
setHealth();
void_watcher_task = Bukkit.getScheduler().scheduleSyncRepeatingTask(DenizenAPI.getCurrentInstance(), new Runnable() {
@Override
public void run() {
if (!npc.isSpawned()) {
Bukkit.getScheduler().cancelTask(void_watcher_task);
return;
}
if (npc.getEntity().getLocation().getY() < -1000) {
npc.despawn(DespawnReason.DEATH);
if (respawn) {
Location res = getRespawnLocation();
if (res.getY() < 1) {
res.setY(res.getWorld().getHighestBlockYAt(res.getBlockX(), res.getBlockZ()));
}
if (npc.isSpawned()) {
npc.getEntity().teleport(res);
}
else {
npc.spawn(res);
}
}
}
}
}, 200, 200);
}
public HealthTrait() {
super("health");
}
/**
* Gets the current health of this NPC.
*
* @return current health points
*/
public double getHealth() {
if (!npc.isSpawned()) {
return 0;
}
else {
return ((LivingEntity) npc.getEntity()).getHealth();
}
}
/**
* Sets the maximum health for this NPC. Default max is 20.
*
* @param newMax new maximum health
*/
public void setMaxhealth(int newMax) {
((LivingEntity) npc.getEntity()).setMaxHealth(newMax);
}
/**
* Gets the maximum health for this NPC.
*
* @return maximum health
*/
public double getMaxhealth() {
return ((LivingEntity) npc.getEntity()).getMaxHealth();
}
/**
* Heals the NPC.
*
* @param health number of health points to heal
*/
public void heal(int health) {
setHealth(getHealth() + health);
}
/**
* Sets the NPCs health to maximum.
*/
public void setHealth() {
setHealth(((LivingEntity) npc.getEntity()).getMaxHealth());
}
/**
* Sets the NPCs health to a specific amount.
*
* @param health total health points
*/
public void setHealth(double health) {
if (npc.getEntity() != null) {
((LivingEntity) npc.getEntity()).setHealth(health);
}
}
public void die() {
((LivingEntity) npc.getEntity()).damage(((LivingEntity) npc.getEntity()).getHealth());
}
// Listen for deaths to clear drops
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDeath(EntityDeathEvent event) {
if (event.getEntity().getEntityId() != entityId) {
return;
}
event.getDrops().clear();
}
// <--[action]
// @Actions
// death
// death by entity
// death by <entity>
// death by block
// death by <cause>
//
// @Triggers when the NPC dies. (Requires Health Trait)
//
// @Context
// <context.killer> returns the entity that killed the NPC (if any)
// <context.shooter> returns the shooter of the killing projectile (if any)
//
// -->
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDamage(EntityDamageEvent event) {
// Don't use NPCDamageEvent because it doesn't work well
// Check if the event pertains to this NPC
if (event.getEntity() != npc.getEntity() || dying) {
return;
}
// Make sure this is a killing blow
if (this.getHealth() - event.getFinalDamage() > 0) {
return;
}
dying = true;
player = null;
// Save entityId for EntityDeath event
entityId = npc.getEntity().getEntityId();
String deathCause = CoreUtilities.toLowerCase(event.getCause().toString()).replace('_', ' ');
Map<String, dObject> context = new HashMap<String, dObject>();
context.put("damage", new Element(event.getDamage()));
context.put("death_cause", new Element(deathCause));
// Check if the entity has been killed by another entity
if (event instanceof EntityDamageByEntityEvent) {
Entity killerEntity = ((EntityDamageByEntityEvent) event).getDamager();
context.put("killer", new dEntity(killerEntity));
// Check if the damager was a player and, if so, attach
// that player to the action's ScriptEntry
if (killerEntity instanceof Player) {
player = dPlayer.mirrorBukkitPlayer((Player) killerEntity);
}
// If the damager was a projectile, take its shooter into
// account as well
else if (killerEntity instanceof Projectile) {
ProjectileSource shooter = ((Projectile) killerEntity).getShooter();
if (shooter != null && shooter instanceof LivingEntity) {
context.put("shooter", new dEntity((LivingEntity) shooter));
if (shooter instanceof Player) {
player = dPlayer.mirrorBukkitPlayer((Player) shooter);
}
DenizenAPI.getDenizenNPC(npc).action("death by " +
((LivingEntity) shooter).getType().toString(), player, context);
}
// TODO: Handle other shooter source thingy types
}
DenizenAPI.getDenizenNPC(npc).action("death by entity", player, context);
DenizenAPI.getDenizenNPC(npc).action("death by " +
killerEntity.getType().toString(), player, context);
}
// If not, check if the entity has been killed by a block
else if (event instanceof EntityDamageByBlockEvent) {
DenizenAPI.getDenizenNPC(npc).action("death by block", player, context);
// TODO:
// The line of code below should work, but a Bukkit bug makes the damager
// return null. Uncomment it once the bug is fixed.
// DenizenAPI.getDenizenNPC(npc).action("death by " +
// ((EntityDamageByBlockEvent) event).getDamager().getType().name(), null);
}
DenizenAPI.getDenizenNPC(npc).action("death", player, context);
DenizenAPI.getDenizenNPC(npc).action("death by " + deathCause, player, context);
// One of the actions above may have removed the NPC, so check if the
// NPC's entity still exists before proceeding
if (npc.getEntity() == null) {
return;
}
loc = dLocation.valueOf(TagManager.tag(respawnLocation, // TODO: debug option?
new BukkitTagContext(null, DenizenAPI.getDenizenNPC(npc), false, null, true, null)));
if (loc == null) {
loc = npc.getEntity().getLocation();
}
if (animatedeath) {
// Cancel navigation to keep the NPC from damaging players
// while the death animation is being carried out.
npc.getNavigator().cancelNavigation();
// Reset health now to avoid the death from happening instantly
//setHealth();
// Play animation (TODO)
// playDeathAnimation(npc.getEntity());
}
die();
if (respawn && (Duration.valueOf(respawnDelay).getTicks() > 0)) {
Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(DenizenAPI.getCurrentInstance(),
new Runnable() {
public void run() {
if (CitizensAPI.getNPCRegistry().getById(npc.getId()) == null || npc.isSpawned()) {
return;
}
else {
npc.spawn(loc);
}
}
}, (Duration.valueOf(respawnDelay).getTicks()));
}
}
}