/** 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.List; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.util.DamageSource; import net.minecraft.util.StatCollector; import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.api.entity.IParryModifier; import zeldaswordskills.client.ZSSKeyHandler; 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.SkillActive; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.TargetUtils; import zeldaswordskills.util.WorldUtils; /** * * Tap 'down' arrow to parry an incoming attack with chance to disarm opponent. Only works on * opponents wielding an item, not against raw physical attacks such as a zombie touch. * * Once activated, their is a short window (4 ticks at level 1) during which all incoming * attacks will be parried, followed by another short period during which parry cannot be * activated again (to prevent spamming). The 'cooldown' time decreases with level, whereas * the 'window' time increases. * * Chance to Disarm: 0.1F per level + a time bonus of up to 0.2F * Exhaustion: 0.3F minus 0.02F per level (0.2F at level 5) * Notes: For players of equal parry skill, chance to disarm is based solely on timing * * Using vanilla controls, Parry is activated just like the Dodge skill, requiring either a * single tap and release, or a double-tap based on the Config settings. Parry never requires * a double tap when using the arrow key. * */ public class Parry extends SkillActive { /** Timer during which player is considered actively parrying */ private int parryTimer; /** Number of attacks parried this activation cycle */ private int attacksParried; /** Only for double-tap activation: Current number of ticks remaining before skill will not activate */ @SideOnly(Side.CLIENT) private int ticksTilFail; /** Notification to play miss sound; set to true when activated and false when attack parried */ private boolean playMissSound; public Parry(String name) { super(name); } private Parry(Parry skill) { super(skill); } @Override public Parry newInstance() { return new Parry(this); } @Override @SideOnly(Side.CLIENT) public void addInformation(List<String> desc, EntityPlayer player) { desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 1), (int)(getDisarmChance(player, null) * 100))); desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 2), (int)(2.5F * (getActiveTime() - getParryDelay())))); desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 3), getMaxParries())); desc.add(getTimeLimitDisplay(getActiveTime() - getParryDelay())); desc.add(getExhaustionDisplay(getExhaustion())); } @Override public boolean isActive() { return (parryTimer > 0); } @Override protected float getExhaustion() { return 0.3F - (0.02F * level); } /** Number of ticks that skill will be considered active */ private int getActiveTime() { return 6 + level; } /** Number of ticks before player may attempt to use this skill again */ private int getParryDelay() { return (5 - (level / 2)); // 2 tick usage window at level 1 } /** The maximum number of attacks that may be parried per use of the skill */ private int getMaxParries() { return (1 + level) / 2; } /** * Returns player's chance to disarm an attacker, including timing bonus * @param attacker entity attacking the player; if the attacker is an EntityPlayer, * their Parry score will decrease their chance of being disarmed */ public float getDisarmChance(EntityPlayer player, EntityLivingBase attacker) { float penalty = (0.15F * attacksParried); float bonus = Config.getDisarmTimingBonus() * (parryTimer > 0 ? (parryTimer - getParryDelay()) : 0); float modifier = getDisarmModifier(player, attacker); return (modifier - penalty + bonus); } /** * Returns the total disarm chance modifier based on the two entities and their held items; * includes all modifiers used by Parry except for the timing bonus and attacks parried. * @param defender Entity defending against an attack, possibly disarming the attacker * @param attacker Attacking entity who may be disarmed, possibly null * @return Combined total of all entity and item disarm modifiers */ public static float getDisarmModifier(EntityLivingBase defender, EntityLivingBase attacker) { ItemStack defStack = defender.getHeldItem(); ItemStack offStack = (attacker != null ? attacker.getHeldItem() : null); float modifier = 0.0F; // DEFENDER if (defender instanceof EntityPlayer) { modifier += 0.1F * ZSSPlayerSkills.get((EntityPlayer) defender).getSkillLevel(parry); } if (defender instanceof IParryModifier) { modifier += ((IParryModifier) defender).getDefensiveModifier(defender, defStack); } if (defStack != null && defStack.getItem() instanceof IParryModifier) { modifier += ((IParryModifier) defStack.getItem()).getDefensiveModifier(defender, defStack); } // ATTACKER if (attacker instanceof EntityPlayer) { modifier -= Config.getDisarmPenalty() * ZSSPlayerSkills.get((EntityPlayer) attacker).getSkillLevel(parry); } if (attacker instanceof IParryModifier) { modifier -= ((IParryModifier) attacker).getOffensiveModifier(attacker, offStack); } if (offStack != null && offStack.getItem() instanceof IParryModifier) { modifier -= ((IParryModifier) offStack.getItem()).getOffensiveModifier(attacker, offStack); } return modifier; } @Override public boolean canUse(EntityPlayer player) { return super.canUse(player) && !isActive() && PlayerUtils.isWeapon(player.getHeldItem()); } /** * Only allow activation if player not using item, to prevent clashing with SwordBreak */ @Override @SideOnly(Side.CLIENT) public boolean canExecute(EntityPlayer player) { return canUse(player) && !PlayerUtils.isBlocking(player); } @Override @SideOnly(Side.CLIENT) public boolean isKeyListener(Minecraft mc, KeyBinding key) { return (key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_DOWN] || (Config.allowVanillaControls && key == mc.gameSettings.keyBindBack)); } @Override @SideOnly(Side.CLIENT) public boolean keyPressed(Minecraft mc, KeyBinding key, EntityPlayer player) { if (canExecute(player)) { if (Config.requireDoubleTap) { if (ticksTilFail > 0) { PacketDispatcher.sendToServer(new ActivateSkillPacket(this)); ticksTilFail = 0; return true; } else { ticksTilFail = 6; } } else if (key != mc.gameSettings.keyBindBack) { // activate on first press, but not for vanilla key! PacketDispatcher.sendToServer(new ActivateSkillPacket(this)); return true; } } return false; } @Override protected boolean onActivated(World world, EntityPlayer player) { parryTimer = getActiveTime(); attacksParried = 0; playMissSound = true; player.swingItem(); return isActive(); } @Override protected void onDeactivated(World world, EntityPlayer player) { parryTimer = 0; } @Override public void onUpdate(EntityPlayer player) { if (isActive()) { if (--parryTimer <= getParryDelay() && playMissSound) { playMissSound = false; WorldUtils.playSoundAtEntity(player, Sounds.SWORD_MISS, 0.4F, 0.5F); } } else if (player.worldObj.isRemote && ticksTilFail > 0) { --ticksTilFail; } } @Override public boolean onBeingAttacked(EntityPlayer player, DamageSource source) { if (source.getEntity() instanceof EntityLivingBase) { EntityLivingBase attacker = (EntityLivingBase) source.getEntity(); if (attacksParried < getMaxParries() && parryTimer > getParryDelay() && attacker.getHeldItem() != null && PlayerUtils.isWeapon(player.getHeldItem())) { if (player.worldObj.rand.nextFloat() < getDisarmChance(player, attacker)) { WorldUtils.dropHeldItem(attacker); } ++attacksParried; // increment after disarm WorldUtils.playSoundAtEntity(player, Sounds.SWORD_STRIKE, 0.4F, 0.5F); playMissSound = false; TargetUtils.knockTargetBack(attacker, player); return true; } // don't deactivate early, as there is a delay between uses } return false; } }