/**
Copyright (C) <2017> <coolAlias>
This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such,
you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package zeldaswordskills.handler;
import java.util.Set;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.monster.IMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import net.minecraft.util.DamageSource;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.fml.common.eventhandler.EventPriority;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import zeldaswordskills.api.damage.DamageUtils;
import zeldaswordskills.api.damage.DamageUtils.DamageSourceArmorBreak;
import zeldaswordskills.api.damage.EnumDamageType;
import zeldaswordskills.api.damage.IDamageAoE;
import zeldaswordskills.api.damage.IDamageType;
import zeldaswordskills.api.damage.IPostDamageEffect;
import zeldaswordskills.api.item.ArmorIndex;
import zeldaswordskills.api.item.IArmorBreak;
import zeldaswordskills.api.item.ISwingSpeed;
import zeldaswordskills.entity.DirtyEntityAccessor;
import zeldaswordskills.entity.ZSSEntityInfo;
import zeldaswordskills.entity.buff.Buff;
import zeldaswordskills.entity.player.ZSSPlayerInfo;
import zeldaswordskills.entity.player.ZSSPlayerSkills;
import zeldaswordskills.item.ItemArmorTunic;
import zeldaswordskills.item.ItemFairyBottle;
import zeldaswordskills.item.ItemZeldaShield;
import zeldaswordskills.item.ItemZeldaSword;
import zeldaswordskills.item.ZSSItems;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.client.UnpressKeyPacket;
import zeldaswordskills.network.server.AddExhaustionPacket;
import zeldaswordskills.ref.Config;
import zeldaswordskills.ref.Sounds;
import zeldaswordskills.skills.ICombo;
import zeldaswordskills.skills.SkillBase;
import zeldaswordskills.skills.sword.ArmorBreak;
import zeldaswordskills.skills.sword.MortalDraw;
import zeldaswordskills.util.TargetUtils;
import zeldaswordskills.util.WorldUtils;
/**
*
* Event handler for all combat-related events
*
*/
public class ZSSCombatEvents
{
/**
* Sets the attack timer for the player if using an ISwingSpeed item
* All other items default to vanilla behavior, which is spam-happy
* Note that the attackTime is deliberately NOT synced between client
* and server; otherwise the smash mechanics will break: left-click is
* processed first on the client, and the server gets notified before
* the smash can process
*/
public static void setPlayerAttackTime(EntityPlayer player) {
if (!player.capabilities.isCreativeMode) {
ZSSPlayerInfo info = ZSSPlayerInfo.get(player);
ItemStack stack = player.getHeldItem();
int nextSwing = Config.getBaseSwingSpeed();
if (stack != null && stack.getItem() instanceof ISwingSpeed) {
nextSwing += Math.max(info.getAttackTime(), ((ISwingSpeed) stack.getItem()).getSwingSpeed());
if (player.worldObj.isRemote) {
float exhaustion = ((ISwingSpeed) stack.getItem()).getExhaustion();
if (exhaustion > 0.0F) {
PacketDispatcher.sendToServer(new AddExhaustionPacket(exhaustion));
}
} else {
PacketDispatcher.sendTo(new UnpressKeyPacket(UnpressKeyPacket.LMB), (EntityPlayerMP) player);
}
}
info.setAttackTime(Math.max(info.getAttackTime(), nextSwing));
}
}
/**
* Using this event to set attack time on the server side only in order
* to prevent left-click processing on blocks with ISmashBlock items
*/
@SubscribeEvent(priority=EventPriority.HIGHEST, receiveCanceled=true)
public void onPlayerAttack(AttackEntityEvent event) {
if (!event.entityPlayer.worldObj.isRemote) {
setPlayerAttackTime(event.entityPlayer);
}
}
@SubscribeEvent
public void onSetAttackTarget(LivingSetAttackTargetEvent event) {
if (event.target instanceof EntityPlayer && event.entity instanceof EntityLiving) {
ItemStack mask = ((EntityPlayer) event.target).getCurrentArmor(ArmorIndex.WORN_HELM);
if (mask != null && mask.getItem() == ZSSItems.maskSpooky && event.entityLiving.getAttackingEntity() != event.target) {
((EntityLiving) event.entity).setAttackTarget(null);
}
}
}
/**
* This event is called when an entity is attacked by another entity; it is only
* called on the server unless the source of the attack is an EntityPlayer
*/
@SubscribeEvent
public void onAttacked(LivingAttackEvent event) {
if (event.source.getEntity() instanceof EntityLivingBase) {
event.setCanceled(ZSSEntityInfo.get((EntityLivingBase) event.source.getEntity()).isBuffActive(Buff.STUN));
}
if (event.isCanceled()) {
return;
}
// Possible for damage to be negated by resistances, in which case we don't want the hurt animation to play
float amount = applyDamageModifiers(event.entityLiving, event.source, event.ammount);
if (event.entity instanceof EntityPlayer) {
EntityPlayer player = (EntityPlayer) event.entity;
ZSSPlayerSkills.get(player).onBeingAttacked(event);
if (amount < 0.1F) {
event.setCanceled(true);
} else if (event.source.isFireDamage() && event.source.getSourceOfDamage() == null) {
event.setCanceled(ItemArmorTunic.onFireDamage(player, event.ammount));
}
} else if (amount < 0.1F) {
event.setCanceled(true);
} else if (event.source.getEntity() != null && (!(event.source instanceof IDamageAoE) || !((IDamageAoE) event.source).isAoEDamage())) {
EntityLivingBase entity = event.entityLiving;
float evade = ZSSEntityInfo.get(entity).getBuffAmplifier(Buff.EVADE_UP) * 0.01F;
if (evade > 0.0F && !ZSSEntityInfo.get(entity).isBuffActive(Buff.STUN)) {
float penalty = ZSSEntityInfo.get(entity).getBuffAmplifier(Buff.EVADE_DOWN) * 0.01F;
if (entity.worldObj.rand.nextFloat() < evade - penalty) {
WorldUtils.playSoundAtEntity(entity, Sounds.SWORD_MISS, 0.4F, 0.5F);
event.setCanceled(true);
}
}
}
}
/**
* Pre-event to handle shield blocking (only posted when not in BG2 battle mode)
*/
@SubscribeEvent(priority=EventPriority.NORMAL)
public void onPreHurt(LivingHurtEvent event) {
if (event.entity instanceof EntityPlayer) {
EntityPlayer player = (EntityPlayer) event.entity;
ItemStack stack = player.getHeldItem();
if (stack != null && stack.getItem() instanceof ItemZeldaShield && player.isUsingItem()) {
ItemZeldaShield shield = (ItemZeldaShield) stack.getItem();
if (ZSSPlayerInfo.get(player).canBlock() && shield.canBlockDamage(stack, event.source)) {
Entity opponent = event.source.getEntity();
if (opponent != null && TargetUtils.isTargetInFrontOf(opponent, player, 60)) {
event.ammount = shield.onBlock(player, stack, event.source, event.ammount);
event.setCanceled(event.ammount < 0.1F);
}
}
}
}
}
/**
* Use LOW or LOWEST priority to prevent interrupting a combo when the event may be canceled elsewhere.
*/
@SubscribeEvent(priority=EventPriority.LOWEST)
public void onHurt(LivingHurtEvent event) {
// handle armor break first, since it will post LivingHurtEvent once again
if (event.source.getEntity() instanceof EntityPlayer && !(event.source instanceof DamageSourceArmorBreak)) {
EntityPlayer player = (EntityPlayer) event.source.getEntity();
ZSSPlayerSkills skills = ZSSPlayerSkills.get(player);
ICombo combo = skills.getComboSkill();
if (combo != null && combo.isComboInProgress()) {
event.ammount += combo.getCombo().getNumHits();
}
if (skills.isSkillActive(SkillBase.armorBreak)) {
//LogHelper.info("Entity hurt by armor break; player weapon pre-impact damage: " + player.getHeldItem().getItemDamage());
((ArmorBreak) skills.getPlayerSkill(SkillBase.armorBreak)).onImpact(player, event);
//LogHelper.info("Entity hurt by armor break; player weapon post-impact damage: " + player.getHeldItem().getItemDamage());
return;
} else if (skills.isSkillActive(SkillBase.mortalDraw)) {
((MortalDraw) skills.getPlayerSkill(SkillBase.mortalDraw)).onImpact(player, event);
}
if (player.getHeldItem() != null && player.getHeldItem().getItem() instanceof IArmorBreak && event.source.damageType.equals("player")) {
float damage = (event.ammount * ((IArmorBreak) player.getHeldItem().getItem()).getPercentArmorIgnored() * 0.01F);
// use dirty accessor to avoid checking / setting hurt resistant time, which
// allows the current remaining damage to process normally and the armor break
// damage to be applied from the second event posted from #damageEntity
DirtyEntityAccessor.damageEntity(event.entityLiving, DamageUtils.causeIArmorBreakDamage(player), damage);
event.ammount -= damage; // subtract armor break damage
}
}
event.ammount = applyDamageModifiers(event.entityLiving, event.source, event.ammount);
// apply magic armor and combo onHurt last, after other resistances
if (event.ammount > 0.0F && event.entity instanceof EntityPlayer) {
EntityPlayer player = (EntityPlayer) event.entity;
/*
// TODO magic armor:
if (player.getCurrentArmor(ArmorIndex.EQUIPPED_CHEST) != null && player.getCurrentArmor(ArmorIndex.EQUIPPED_CHEST).getItem() == ZSSItems.magicArmor) {
while (event.ammount > 0 && player.inventory.consumeInventoryItem(Item.emerald.itemID)) {
event.ammount -= 1.0F;
}
event.setCanceled(event.ammount < 0.1F);
}
*/
if (event.isCanceled()) {
return;
}
ICombo combo = ZSSPlayerSkills.get(player).getComboSkill();
if (combo != null && event.ammount > 0) {
combo.onPlayerHurt(player, event);
}
}
// final call for active skills to modify damage
// update combo and last, after all resistances and weaknesses are accounted for
if (event.ammount > 0.0F && event.source.getEntity() instanceof EntityPlayer) {
ZSSPlayerSkills.get((EntityPlayer) event.source.getEntity()).onPostImpact(event);
}
handleSecondaryEffects(event);
}
/**
* Set to highest priority to prevent loss of "extra lives" from HQM mod
*/
@SubscribeEvent(priority=EventPriority.HIGHEST)
public void onLivingDeathEvent(LivingDeathEvent event) {
if (!event.entity.worldObj.isRemote && event.entity instanceof EntityPlayer) {
event.setCanceled(ItemFairyBottle.onDeath((EntityPlayer) event.entity));
}
if (event.source.getEntity() instanceof EntityPlayer && event.entity instanceof IMob) {
ItemZeldaSword.onKilledMob((EntityPlayer) event.source.getEntity(), (IMob) event.entity);
}
}
/**
* Returns the damage amount modified by both the attacker's and the defender's relevant buffs
*/
public static float applyDamageModifiers(EntityLivingBase defender, DamageSource source, float amount) {
if (source.getEntity() instanceof EntityLivingBase) {
EntityLivingBase entity = (EntityLivingBase) source.getEntity();
amount *= 1.0F - (ZSSEntityInfo.get(entity).getBuffAmplifier(Buff.ATTACK_DOWN) * 0.01F);
amount *= 1.0F + (ZSSEntityInfo.get(entity).getBuffAmplifier(Buff.ATTACK_UP) * 0.01F);
}
amount = applyDamageWeaknesses(defender, source, amount);
amount = applyDamageResistances(defender, source, amount);
return amount;
}
/**
* Returns modified damage amount based on the defender's resistances vs. the damage source
*/
private static float applyDamageResistances(EntityLivingBase defender, DamageSource source, float amount) {
ZSSEntityInfo info = ZSSEntityInfo.get(defender);
float defenseUp = info.getBuffAmplifier(Buff.DEFENSE_UP) * 0.01F;
float defenseDown = info.getBuffAmplifier(Buff.DEFENSE_DOWN) * 0.01F;
amount *= (1.0F + defenseDown - defenseUp);
if (source instanceof IDamageType && amount > 0.0F) {
Set<EnumDamageType> damageTypes = ((IDamageType) source).getEnumDamageTypes();
if (damageTypes != null) {
for (EnumDamageType type : damageTypes) {
if (EnumDamageType.damageResistMap.get(type) != null) {
amount *= 1.0F - (info.getBuffAmplifier(EnumDamageType.damageResistMap.get(type)) * 0.01F);
}
}
}
}
if (source.isFireDamage()) {
amount *= 1.0F - (info.getBuffAmplifier(Buff.RESIST_FIRE) * 0.01F);
}
if (source.isMagicDamage()) {
amount *= 1.0F - (info.getBuffAmplifier(Buff.RESIST_MAGIC) * 0.01F);
}
return amount;
}
/**
* Returns modified damage amount based on the defender's weaknesses to the damage source
*/
private static float applyDamageWeaknesses(EntityLivingBase defender, DamageSource source, float amount) {
ZSSEntityInfo info = ZSSEntityInfo.get(defender);
if (source instanceof IDamageType) {
Set<EnumDamageType> damageTypes = ((IDamageType) source).getEnumDamageTypes();
if (damageTypes != null) {
for (EnumDamageType type : damageTypes) {
if (EnumDamageType.damageWeaknessMap.get(type) != null) {
amount *= 1.0F + (info.getBuffAmplifier(EnumDamageType.damageWeaknessMap.get(type)) * 0.01F);
}
}
}
}
if (source.isFireDamage()) {
amount *= 1.0F + (info.getBuffAmplifier(Buff.WEAKNESS_FIRE) * 0.01F);
}
if (source.isMagicDamage()) {
amount *= 1.0F + (info.getBuffAmplifier(Buff.WEAKNESS_MAGIC) * 0.01F);
}
return amount;
}
/**
* Applies any secondary effects that may occur when a living entity is injured
*/
private static void handleSecondaryEffects(LivingHurtEvent event) {
if (event.ammount >= 1.0F && event.source instanceof IDamageType && event.source instanceof IPostDamageEffect) {
Set<EnumDamageType> damageTypes = ((IDamageType) event.source).getEnumDamageTypes();
if (damageTypes != null) {
for (EnumDamageType type : damageTypes) {
type.handleSecondaryEffects((IPostDamageEffect) event.source, event.entityLiving, event.ammount);
}
}
}
}
}