/**
Copyright (C) <2015> <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.skills.sword;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.monster.EntityBlaze;
import net.minecraft.entity.monster.EntitySlime;
import net.minecraft.entity.monster.EntitySnowman;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemArmor;
import net.minecraft.item.ItemStack;
import net.minecraft.util.DamageSource;
import net.minecraft.util.StatCollector;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import zeldaswordskills.api.entity.IEntityBackslice;
import zeldaswordskills.api.item.ArmorIndex;
import zeldaswordskills.client.ZSSClientEvents;
import zeldaswordskills.client.ZSSKeyHandler;
import zeldaswordskills.entity.mobs.EntityChu;
import zeldaswordskills.entity.player.ZSSPlayerSkills;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.bidirectional.ActivateSkillPacket;
import zeldaswordskills.ref.Config;
import zeldaswordskills.ref.Sounds;
import zeldaswordskills.skills.ILockOnTarget;
import zeldaswordskills.skills.SkillActive;
import zeldaswordskills.util.PlayerUtils;
import zeldaswordskills.util.TargetUtils;
import zeldaswordskills.util.WorldUtils;
/**
*
* BACK SLICE
* Description: Circle around a foe to strike at its vulnerable backside!
* Activation: Hold left or right arrow key and double-tap forward
* Exhaustion: Light [1.0F - (level * 0.05F)]
* Damage: Adds +10% to the damage per level, possibly more against vulnerable enemies
* Effective Angle: 40 degrees plus 20 per level
* Special:
* - Considered 'dodging' against current target for the first 5 + (2 * level) ticks
* - Bonus damage only applies when target is struck in the back
* - Some enemies do not take extra damage, usually because they have no back (e.g. slimes)
* - Other enemies may take no damage at all, if they have a hard carapace or some such
* - Chance to remove enemy's chest armor
*/
public class BackSlice extends SkillActive
{
/** List of vanilla entities immune to increased damage */
private static Set<Class<? extends EntityLivingBase>> immuneEntities;
/** Key that was pressed to initiate movement, either left or right */
@SideOnly(Side.CLIENT)
private KeyBinding keyPressed;
/** Current number of ticks remaining before skill will fail to activate */
@SideOnly(Side.CLIENT)
private int ticksTilFail;
/** Timer during which player may evade incoming attacks */
private int dodgeTimer = 0;
/** Used client side to get an extra renderTick for the targeting camera */
private SkillActive targetingSkill;
public BackSlice(String name) {
super(name);
init(); // populates list of immune entities
}
private BackSlice(BackSlice skill) {
super(skill);
}
@Override
public BackSlice newInstance() {
return new BackSlice(this);
}
@Override
@SideOnly(Side.CLIENT)
public void addInformation(List<String> desc, EntityPlayer player) {
desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 1), 360 - (2 * getAttackAngle())));
desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 2),
String.format("%.2f", getDisarmorChance(null, player.getHeldItem(), level))));
desc.add(getDamageDisplay(level * 10, true) + "%");
desc.add(getExhaustionDisplay(getExhaustion()));
}
@Override
public boolean canDrop() {
return false;
}
@Override
public boolean isLoot() {
return false;
}
/** Number of ticks for which skill will remain active */
private int getActiveTime() {
return 15 + level;
}
/** Angle at which player will be considered behind the target */
private int getAttackAngle() {
return (160 - (level * 10));
}
/** Number of ticks for which player will dodge targeted entity's attacks */
private int getDodgeTime() {
return 10 - level;
}
/**
* Returns chance to remove armor, with higher tier armor being more difficult to remove:
* each point of armor above 5 reduces the chance by 5%, and each point below 5 increases
* the chance by 5%, e.g. diamond plate gives damage reduction 8, so incurs a -15% penalty
* to the attacker's chance to disarmor. Each level of Unbreaking adds another -5%.
* @param armor null is allowed to return base chance for addInformation
* @param weapon attacking entity's held item adds bonus for Sharpness; null is allowed, adding a -100% penalty
* @param level base chance is 5% per skill level
* @return chance that the armor stack will be knocked off of the damaged entity, a value between 0.0F and 1.0F inclusive
*/
public static float getDisarmorChance(ItemStack armorStack, ItemStack weapon, int level) {
float chance = ((float) level * 0.05F);
if (armorStack != null && armorStack.getItem() instanceof ItemArmor) {
ItemArmor armor = (ItemArmor) armorStack.getItem();
int i = armor.getArmorMaterial().getDamageReductionAmount(armor.armorType);
chance += (float)(5 - i) * 0.05F;
i = EnchantmentHelper.getEnchantmentLevel(Enchantment.unbreaking.effectId, armorStack);
if (i > 0) { // -5% per level of Unbreaking
chance -= (float) i * 0.05F;
}
}
if (weapon != null) {
int i = EnchantmentHelper.getEnchantmentLevel(Enchantment.sharpness.effectId, weapon);
if (i > 0) { // +5% per level of Sharpness
chance += (float) i * 0.05F;
}
} else {
chance -= 1.0F;
}
return chance;
}
@Override
public boolean isActive() {
return (dodgeTimer > 0);
}
@Override
protected float getExhaustion() {
return (1.0F - (0.1F * level));
}
@Override
public boolean canUse(EntityPlayer player) {
return super.canUse(player) && !isActive() && PlayerUtils.isWeapon(player.getHeldItem()) && ZSSPlayerSkills.get(player).isSkillActive(swordBasic);
}
@Override
@SideOnly(Side.CLIENT)
public boolean canExecute(EntityPlayer player) {
return player.onGround && canUse(player);
}
@Override
@SideOnly(Side.CLIENT)
public boolean isKeyListener(Minecraft mc, KeyBinding key) {
if (isActive()) {
return (key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_ATTACK] || (Config.allowVanillaControls && key == mc.gameSettings.keyBindAttack));
}
return key == mc.gameSettings.keyBindForward || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_LEFT] || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_RIGHT] ||
((Config.allowVanillaControls && (key == mc.gameSettings.keyBindLeft || key == mc.gameSettings.keyBindRight)));
}
@Override
@SideOnly(Side.CLIENT)
public boolean keyPressed(Minecraft mc, KeyBinding key, EntityPlayer player) {
if (canExecute(player)) {
if (keyPressed != null && keyPressed.isKeyDown() && key == mc.gameSettings.keyBindForward) {
if (ticksTilFail > 0) {
PacketDispatcher.sendToServer(new ActivateSkillPacket(this));
ticksTilFail = 0;
return true;
} else {
ticksTilFail = 6;
}
} else if (key != mc.gameSettings.keyBindForward) {
keyPressed = key;
}
} else if (isActive() && (key == mc.gameSettings.keyBindAttack || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_ATTACK])) {
ZSSClientEvents.performComboAttack(mc, ZSSPlayerSkills.get(player).getTargetingSkill());
}
return false; // allow other skills to receive this key press (e.g. Spin Attack)
}
@Override
public boolean onActivated(World world, EntityPlayer player) {
dodgeTimer = getActiveTime();
targetingSkill = ZSSPlayerSkills.get(player).getActiveSkill(swordBasic);
return isActive();
}
@Override
protected void onDeactivated(World world, EntityPlayer player) {
dodgeTimer = 0;
targetingSkill = null;
if (world.isRemote) {
keyPressed = null;
}
}
@Override
public void onUpdate(EntityPlayer player) {
if (isActive()) {
--dodgeTimer;
} else if (player.worldObj.isRemote && ticksTilFail > 0) {
if (--ticksTilFail == 0) {
keyPressed = null;
}
}
}
@Override
@SideOnly(Side.CLIENT)
public boolean isAnimating() {
return (dodgeTimer > level);
}
@Override
@SideOnly(Side.CLIENT)
public boolean onRenderTick(EntityPlayer player, float partialTickTime) {
if (player.onGround) {
// force extra camera update so player can more easily hit target:
if (targetingSkill != null && targetingSkill.isActive()) {
targetingSkill.onRenderTick(player, partialTickTime);
}
double speed = 1.0D + 10.0D * (player.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.movementSpeed).getAttributeValue() - Dash.BASE_MOVE);
if (speed > 1.0D) {
speed = 1.0D;
}
double d = 0.15D * speed * speed;
if (player.isInWater() || player.isInLava()) {
d *= 0.15D;
}
Vec3 vec3 = player.getLookVec();
if (keyPressed == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_RIGHT] || keyPressed == Minecraft.getMinecraft().gameSettings.keyBindRight) {
player.addVelocity(-vec3.zCoord * d, 0.0D, vec3.xCoord * d);
} else {
player.addVelocity(vec3.zCoord * d, 0.0D, -vec3.xCoord * d);
}
player.addVelocity(vec3.xCoord * d * 1.15D, 0.0D, vec3.zCoord * d * 1.15D);
}
return false; // allow camera to update again
}
@Override
public boolean onBeingAttacked(EntityPlayer player, DamageSource source) {
ILockOnTarget targeting = ZSSPlayerSkills.get(player).getTargetingSkill();
return (dodgeTimer > getDodgeTime() && targeting != null && targeting.getCurrentTarget() == source.getEntity());
}
@Override
public float postImpact(EntityPlayer player, EntityLivingBase entity, float amount) {
if (isActive() && dodgeTimer <= (getActiveTime() - 5)) { // can strike any time after 5 ticks have passed
ILockOnTarget targeting = ZSSPlayerSkills.get(player).getTargetingSkill();
if (targeting != null && targeting.getCurrentTarget() == entity) {
if (!TargetUtils.isTargetInFrontOf(entity, player, getAttackAngle())) {
boolean flag = false;
boolean isIBackEntity = (entity instanceof IEntityBackslice);
// IEntityBackside takes priority over default immunities
if (isIBackEntity) {
flag = ((IEntityBackslice) entity).allowDamageMultiplier(player);
} else if (!isEntityImmune(entity)) {
flag = true;
}
// damage multiplier:
if (flag) {
amount *= 1.0F + (level * 0.1F);
WorldUtils.playSoundAtEntity(player, Sounds.MORTAL_DRAW, 0.4F, 0.5F);
}
if (isIBackEntity) {
amount = ((IEntityBackslice) entity).onBackSliced(player, level, amount);
}
flag = (flag && (Config.canDisarmorPlayers() || !(entity instanceof EntityPlayer)));
// chance of knocking off an armor piece only if extra damage was allowed or IEntityBackside allows it
if (flag && (!isIBackEntity || ((IEntityBackslice) entity).allowDisarmorment(player, amount))) {
ItemStack armor = entity.getEquipmentInSlot(ArmorIndex.EQUIPPED_CHEST);
if (armor != null && player.worldObj.rand.nextFloat() < getDisarmorChance(armor, player.getHeldItem(), level)) {
WorldUtils.spawnItemWithRandom(entity.worldObj, armor, entity.posX, entity.posY, entity.posZ);
entity.setCurrentItemOrArmor(ArmorIndex.EQUIPPED_CHEST, null);
}
}
}
}
}
deactivate(player); // now deactivate on server side; if player missed, they just have to wait
return amount;
}
/**
* Returns true if the given entity inherits from any of the classes marked
* as immune to back damage multipliers, such as EntitySlime
*/
private static boolean isEntityImmune(EntityLivingBase entity) {
for (Class<? extends EntityLivingBase> clazz : immuneEntities) {
if (clazz.isAssignableFrom(entity.getClass())) {
return true;
}
}
return false;
}
// As finicky as BackSlice is, this is probably not even necessary...
private static void init() {
if (immuneEntities == null) {
immuneEntities = new HashSet<Class<? extends EntityLivingBase>>();
immuneEntities.add(EntityBlaze.class);
immuneEntities.add(EntityChu.class);
immuneEntities.add(EntitySnowman.class);
immuneEntities.add(EntitySlime.class);
}
}
}