/** 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.enchantment.EnchantmentHelper; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.EnumParticleTypes; 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.block.BlockAncientTablet; import zeldaswordskills.client.ZSSKeyHandler; import zeldaswordskills.entity.player.ZSSPlayerInfo; import zeldaswordskills.entity.player.ZSSPlayerSkills; import zeldaswordskills.entity.projectile.EntityBombosFireball; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.bidirectional.ActivateSkillPacket; import zeldaswordskills.network.server.RefreshSpinPacket; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.Sounds; import zeldaswordskills.skills.SkillActive; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.TargetUtils; import zeldaswordskills.util.WorldUtils; /** * * Activated by left or right arrow; player spins in designated direction attacking * all enemies within 360 arc (like Link's spin attack in Zelda). With the Super Spin * Attack, most attributes are doubled, but it may only be used at full health. * * Activation: Hold left or right arrow key to charge up until spin attack commences * Vanilla: Begin moving either left or right, then press the other direction to * commence charging; both keys must be held to continue charging * Tapping attack will continue the spin (Super Spin Attack only) * Arc: 360 degrees, plus an extra 360 degrees for every level of Super Spin Attack * Charge time: 20 ticks, minus 2 per level * Range: 3.0D plus 0.5D per level each of Spin and Super Spin Attack * Exhaustion: 3.0F - 0.2F per level, added on the first spin only * Magic: 5.75F - 0.75F per level for each additional spin; +10.0F per spin if using the Bombos Medallion * */ public class SpinAttack extends SkillActive { /** Current charge time; only ever set on the client - server is never charging */ private int charge; /** Current spin progress is incremented each tick and signals that the skill is active */ private float currentSpin; /** Number of degrees to spin and used as flag for isActive(); incremented by 360F each time spin is refreshed */ private float arc; /** Number of times the spin has been 'refreshed' during this activation cycle; incremented in {@link #startSpin} */ private int refreshed; /** Direction in which to spin */ @SideOnly(Side.CLIENT) private boolean clockwise; /** Used to allow vanilla keys to determine spin direction */ @SideOnly(Side.CLIENT) private boolean wasKeyPressed; /** Entities within range upon activation so no entity targeted more than once */ @SideOnly(Side.CLIENT) private List<EntityLivingBase> targets; /** Whether flame particles should render along the sword's arc */ private boolean isFlaming; /** Whether the spin attack is enhanced by the Bombos Medallion */ private boolean isBombos; /** The player's Super Spin Attack level will allow multiple spins and extended range */ private int superLevel; public SpinAttack(String name) { super(name); } private SpinAttack(SpinAttack skill) { super(skill); } @Override public SpinAttack newInstance() { return new SpinAttack(this); } @Override @SideOnly(Side.CLIENT) public void addInformation(List<String> desc, EntityPlayer player) { byte temp = level; if (!isActive()) { superLevel = (checkHealth(player) ? ZSSPlayerSkills.get(player).getSkillLevel(superSpinAttack) : 0); level = ZSSPlayerSkills.get(player).getSkillLevel(spinAttack); } desc.add(getChargeDisplay(getChargeTime())); desc.add(getRangeDisplay(getRange())); desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 1).replace("super", ""), superLevel + 1)); desc.add(getExhaustionDisplay(getExhaustion())); if (this.getId() == superSpinAttack.getId()) { // player's skill is a different instance desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 2), String.format("%.2f", getMagicCost()))); } level = temp; } @Override public boolean canDrop() { return this == spinAttack; } @Override public boolean isLoot() { return this == spinAttack; } @Override public boolean isActive() { return arc > 0; } @Override @SideOnly(Side.CLIENT) public boolean isAnimating() { return isActive() && !isCharging(); } @Override protected float getExhaustion() { return (refreshed > 0 ? 0.0F : 3.0F - (0.2F * level)); } private float getMagicCost() { return 5.75F - (0.75F * superLevel); } /** Returns time required before spin will execute */ private int getChargeTime() { return 20 - (level * 2); } /** Returns true if the skill is still charging up; always false on the server */ private boolean isCharging() { return charge > 0; } /** * Returns true if the arc may be extended by 360 more degrees */ private boolean canRefresh(EntityPlayer player) { float cost = getMagicCost(); if (isBombos && ZSSPlayerInfo.get(player).getCurrentMagic() < (10.0F + cost)) { isBombos = false; } if (ZSSPlayerInfo.get(player).getCurrentMagic() < cost) { return false; } return (refreshed < (superLevel + 1) && arc == (360F * refreshed)); } /** Max sword range for striking targets */ private float getRange() { return (3.0F + ((superLevel + level) * 0.5F)); } /** Returns the spin speed modified based on the skill's level */ private float getSpinSpeed() { return 70 + (3 * (superLevel + level)); } /** Returns true if players current health is within the allowed limit */ private boolean checkHealth(EntityPlayer player) { return player.capabilities.isCreativeMode || PlayerUtils.getHealthMissing(player) <= Config.getHealthAllowance(level); } @Override public boolean canUse(EntityPlayer player) { return super.canUse(player) && !isActive(); } @Override @SideOnly(Side.CLIENT) public boolean canExecute(EntityPlayer player) { // return super.canUse instead of this.canUse to avoid !isActive() check; allows // canExecute to be checked when refreshing super spin attack return super.canUse(player) && PlayerUtils.isWeapon(player.getHeldItem()); } /** * Returns true if either left or right arrow key is currently being pressed (or both in the case of vanilla controls) */ @SideOnly(Side.CLIENT) private boolean isKeyPressed() { return ZSSKeyHandler.keys[ZSSKeyHandler.KEY_LEFT].isKeyDown() || ZSSKeyHandler.keys[ZSSKeyHandler.KEY_RIGHT].isKeyDown() || (Config.allowVanillaControls && (Minecraft.getMinecraft().gameSettings.keyBindLeft.isKeyDown() && Minecraft.getMinecraft().gameSettings.keyBindRight.isKeyDown())); } @Override @SideOnly(Side.CLIENT) public boolean isKeyListener(Minecraft mc, KeyBinding key) { // attack key is handled separately in order to intercept the key before it may be passed // to other skills; this is necessary to prevent another skill activating while spin attack is active return (//key == mc.gameSettings.keyBindAttack || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_ATTACK] || (Config.allowVanillaControls && (key == mc.gameSettings.keyBindLeft || key == mc.gameSettings.keyBindRight)) || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_LEFT] || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_RIGHT]); } /** * Sets direction of spin and activates skill when left or right arrow key pressed * or adds extra spin for Super Spin Attack when attack key pressed * NOTE: Super Spin Attack requires this method to be called explicitly for the * attack key, since the attack key is normally only processed if canInteract * returns true, which is not the case while spin attack is active */ @Override @SideOnly(Side.CLIENT) public boolean keyPressed(Minecraft mc, KeyBinding key, EntityPlayer player) { if (key == mc.gameSettings.keyBindAttack || key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_ATTACK]) { if (isActive() && canRefresh(player) && canExecute(player)) { PacketDispatcher.sendToServer(new RefreshSpinPacket()); refreshSpin(player); return true; } } else if (!isCharging()) { // prevents activation of Dodge from interfering with spin direction if (wasKeyPressed) { wasKeyPressed = false; } else { clockwise = (key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_RIGHT] || key == mc.gameSettings.keyBindRight); wasKeyPressed = true; } if (isKeyPressed()) { wasKeyPressed = false; charge = getChargeTime(); return true; } } return false; } @Override protected boolean onActivated(World world, EntityPlayer player) { currentSpin = 0F; arc = 360F; refreshed = 0; superLevel = (checkHealth(player) ? ZSSPlayerSkills.get(player).getSkillLevel(superSpinAttack) : 0); // TODO if (Baubles is enabled) { check only that one slot if (PlayerUtils.isHoldingMasterSword(player) && PlayerUtils.hasItem(player, ZSSItems.medallion, BlockAncientTablet.EnumType.BOMBOS.ordinal())) { isBombos = ZSSPlayerInfo.get(player).useMagic(10.0F); } isFlaming = (isBombos || EnchantmentHelper.getFireAspectModifier(player) > 0); startSpin(world, player); return true; } @Override protected void onDeactivated(World world, EntityPlayer player) { charge = 0; currentSpin = 0.0F; arc = 0.0F; isBombos = false; } @Override public void onUpdate(EntityPlayer player) { // isCharging can only be true on the client, which is where charging is handled if (isCharging()) { // check isRemote before accessing @client stuff anyway, just in case charge somehow set on server if (PlayerUtils.isWeapon(player.getHeldItem()) && player.worldObj.isRemote && isKeyPressed()) { if (charge < (getChargeTime() - 1)) { Minecraft.getMinecraft().playerController.sendUseItem(player, player.worldObj, player.getHeldItem()); } --charge; if (charge == 0 && canExecute(player)) { PacketDispatcher.sendToServer(new ActivateSkillPacket(this)); } } else { charge = 0; } } else if (isActive()) { incrementSpin(player); if (isBombos && !player.worldObj.isRemote) { spawnFireballs(player); } } } @Override @SideOnly(Side.CLIENT) public boolean onRenderTick(EntityPlayer player, float partialTickTime) { if (PlayerUtils.isWeapon(player.getHeldItem())) { List<EntityLivingBase> list = TargetUtils.acquireAllLookTargets(player, (int)(getRange() + 0.5F), 1.0D); for (EntityLivingBase target : list) { if (targets != null && targets.contains(target)) { Minecraft.getMinecraft().playerController.attackEntity(player, target); targets.remove(target); } } spawnParticles(player); player.swingProgress = 0.5F; player.setAngles((clockwise ? getSpinSpeed() : -getSpinSpeed()), 0); } return true; } /** * Initiates spin attack and increments refreshed * Client populates the nearby target list * Server plays spin sound and, if not the first spin, adds exhaustion */ private void startSpin(World world, EntityPlayer player) { ++refreshed; if (world.isRemote) { targets = world.getEntitiesWithinAABB(EntityLivingBase.class, player.getEntityBoundingBox().expand(getRange(), 0.0D, getRange())); if (targets.contains(player)) { targets.remove(player); } } else { WorldUtils.playSoundAtEntity(player, Sounds.SPIN_ATTACK, 0.4F, 0.5F); } } /** * Updates the spin progress counter and terminates the spin once it reaches the max spin arc */ private void incrementSpin(EntityPlayer player) { // 0.15D is the multiplier from Entity.setAngles, but that is too little now that no longer in render tick // 0.24D results in a perfect circle per spin, at all levels, taking 21 ticks to complete at level 1, and 15 at level 10 currentSpin += getSpinSpeed() * 0.24D; if (currentSpin >= arc) { deactivate(player); } else if (currentSpin > (360F * refreshed)) { startSpin(player.worldObj, player); } } private void spawnFireballs(EntityPlayer player) { player.worldObj.spawnEntityInWorld(new EntityBombosFireball(player.worldObj, player).setDamage(10.0F)); } @SideOnly(Side.CLIENT) private void spawnParticles(EntityPlayer player) { // TODO these will not be seen by other players EnumParticleTypes particle = (isFlaming ? EnumParticleTypes.FLAME : (superLevel > 0 ? EnumParticleTypes.CRIT_MAGIC : EnumParticleTypes.CRIT)); Vec3 vec3 = player.getLookVec(); double posX = player.posX + (vec3.xCoord * getRange()); double posY = player.posY + player.getEyeHeight() - 0.1D; double posZ = player.posZ + (vec3.zCoord * getRange()); for (int i = 0; i < 2; ++i) { player.worldObj.spawnParticle(particle, posX, posY, posZ, vec3.xCoord * 0.15D, 0.01D, vec3.zCoord * 0.15D); } } /** * Called on the server after receiving the {@link RefreshSpinPacket} */ public void refreshServerSpin(EntityPlayer player) { if (canRefresh(player) && super.canUse(player) && PlayerUtils.isWeapon(player.getHeldItem())) { refreshSpin(player); } } private void refreshSpin(EntityPlayer player) { if (ZSSPlayerInfo.get(player).useMagic(getMagicCost())) { arc += 360F; } if (isBombos && !ZSSPlayerInfo.get(player).useMagic(10.0F)) { isBombos = false; } } }