/** 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.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.network.play.server.S12PacketEntityVelocity; import net.minecraft.util.DamageSource; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.MovingObjectPosition.MovingObjectType; 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.damage.DamageUtils; import zeldaswordskills.api.item.IDashItem; import zeldaswordskills.client.ZSSKeyHandler; import zeldaswordskills.entity.player.ZSSPlayerInfo; import zeldaswordskills.entity.player.ZSSPlayerSkills; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.bidirectional.ActivateSkillPacket; import zeldaswordskills.network.server.DashImpactPacket; 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; /** * * Attacking while blocking and locked on to a target will execute a bash attack. * The player charges into the target, inflicting damage and knocking the target back. * * Range: 4 blocks plus 1 block per additional level * Damage: 2 plus 1 per additional level * Knockback: 2 blocks, plus 1 per additional level * Exhaustion: Light [1.0F - (level * 0.05F)] * Special: Must be at least 2 blocks away from target when skill is activated to * inflict damage, minus 0.2F per level (down to 1 block at level 5) * */ public class Dash extends SkillActive { /** Player's base movement speed */ public static final double BASE_MOVE = 0.10000000149011612D; /** True when Slam is used and while the player is in motion towards the target */ private boolean isActive = false; /** Total distance currently traveled */ private double distance; /** * The dash trajectory is set once when activated, to prevent the vec3 coordinates from * shrinking as the player nears the target; as a bonus, Dash is no longer 'homing' */ @SideOnly(Side.CLIENT) private Vec3 trajectory; /** Player's starting position is used to determine actual distance traveled upon impact */ private Vec3 initialPosition; /** Target acquired from ILockOnTarget skill; set to the entity hit upon impact */ private Entity target; /** Impact timer used to make player immune to damage from struck target only, vs. setting hurtResistantTime */ private int impactTime; public Dash(String name) { super(name); } private Dash(Dash skill) { super(skill); } @Override public Dash newInstance() { return new Dash(this); } @Override @SideOnly(Side.CLIENT) public void addInformation(List<String> desc, EntityPlayer player) { desc.add(getDamageDisplay(getDamage(), false)); desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 1), 2 + level)); desc.add(getRangeDisplay(getRange())); desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 2), String.format("%.1f", getMinDistance()))); desc.add(getExhaustionDisplay(getExhaustion())); } @Override public boolean isActive() { return isActive || impactTime > 0; } @Override protected float getExhaustion() { return 1.0F - (0.05F * level); } /** Damage is base damage plus one per level */ private int getDamage() { return (2 + level); } /** Range increases by 1 block per level */ private double getRange() { return (3.0D + level); } /** Minimum distance the player must cover before the dash is effective */ private double getMinDistance() { return 2.0D - (0.2D * level); } @Override public boolean canUse(EntityPlayer player) { ItemStack stack = (player.getHeldItem() != null ? player.getHeldItem() : null); return super.canUse(player) && !isActive() && (PlayerUtils.isWeapon(stack) || stack.getItem() instanceof IDashItem); } @Override @SideOnly(Side.CLIENT) public boolean canExecute(EntityPlayer player) { return player.onGround && PlayerUtils.isBlocking(player) && canUse(player); } @Override @SideOnly(Side.CLIENT) public boolean isKeyListener(Minecraft mc, KeyBinding key) { return (key == ZSSKeyHandler.keys[ZSSKeyHandler.KEY_ATTACK] || (Config.allowVanillaControls && key == mc.gameSettings.keyBindAttack)); } @Override @SideOnly(Side.CLIENT) public boolean keyPressed(Minecraft mc, KeyBinding key, EntityPlayer player) { if (canExecute(player)) { PacketDispatcher.sendToServer(new ActivateSkillPacket(this)); return true; } return false; } @Override protected boolean onActivated(World world, EntityPlayer player) { isActive = true; initialPosition = new Vec3(player.posX, player.posY + player.getEyeHeight() - 0.10000000149011612D, player.posZ); ILockOnTarget skill = ZSSPlayerSkills.get(player).getTargetingSkill(); if (skill != null && skill.isLockedOn()) { target = skill.getCurrentTarget(); } else { target = TargetUtils.acquireLookTarget(player, (int) getRange(), getRange(), true); } if (target != null && world.isRemote) { double d0 = (target.posX - player.posX); double d1 = (target.posY + (double)(target.height / 3.0F) - player.posY); double d2 = (target.posZ - player.posZ); trajectory = new Vec3(d0, d1, d2).normalize(); } return isActive(); } @Override protected void onDeactivated(World world, EntityPlayer player) { impactTime = 0; // no longer active, target will be set to null from setNotDashing setNotDashing(); // sets all remaining fields to 0 or null } @Override public void onUpdate(EntityPlayer player) { if (impactTime > 0) { --impactTime; if (impactTime == 0) { target = null; } } // don't use isActive() method, as that also returns true after impact if (isActive) { // Only check for impact on the client, as the server is not reliable for this step // If a collision is detected, DashImpactPacket is sent to conclude the server-side if (player.worldObj.isRemote) { MovingObjectPosition mop = TargetUtils.checkForImpact(player.worldObj, player, player, 0.5D, false); if (mop != null) { PacketDispatcher.sendToServer(new DashImpactPacket(player, mop)); // Player cannot attack directly after impacting something ZSSPlayerInfo.get(player).setAttackTime((player.capabilities.isCreativeMode ? 0 : 10 - level)); impactTime = 5; if (mop.typeOfHit == MovingObjectType.ENTITY) { target = mop.entityHit; } double d = Math.sqrt((player.motionX * player.motionX) + (player.motionZ * player.motionZ)); player.setVelocity(-player.motionX * d, 0.15D * d, -player.motionZ * d); trajectory = null; // set to null so player doesn't keep moving forward setNotDashing(); } } // increment distance AFTER update, otherwise Dash thinks it can damage entities right in front of player ++distance; if (distance > (getRange() + 1.0D) || !(target instanceof EntityLivingBase)) { setNotDashing(); } } } /** * Called on the server from {@link DashImpactPacket} to process the impact data from the client * @param player Player's motionX and motionZ have been set by the packet, so the values may be used * @param mop Null assumes a block was hit (none of the block data is needed, so it is not sent), * or a valid MovingObjectPosition for the entity hit */ public void onImpact(World world, EntityPlayer player, MovingObjectPosition mop) { if (mop != null && mop.typeOfHit == MovingObjectType.ENTITY) { target = mop.entityHit; double dist = target.getDistance(initialPosition.xCoord, initialPosition.yCoord, initialPosition.zCoord); // Subtract half the width for each entity to account for their bounding box size dist -= (target.width / 2.0F) + (player.width / 2.0F); // Base player speed is 0.1D; heavy boots = 0.04D, pegasus = 0.13D double speed = player.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.movementSpeed).getAttributeValue(); double sf = (1.0D + (speed - BASE_MOVE)); // speed factor if (speed > 0.075D && dist > getMinDistance() && player.getDistanceSqToEntity(target) < 6.0D) { float dmg = (float) getDamage() + (float)((dist / 2.0D) - 2.0D); impactTime = 5; // time player will be immune to damage from the target entity target.attackEntityFrom(DamageUtils.causeNonSwordDamage(player), (float)(dmg * sf * sf)); double resist = 1.0D; if (target instanceof EntityLivingBase) { resist -= ((EntityLivingBase) target).getEntityAttribute(SharedMonsterAttributes.knockbackResistance).getAttributeValue(); } double k = sf * resist * (distance / 3.0F) * 0.6000000238418579D; target.addVelocity(player.motionX * k * (0.2D + (0.1D * level)), 0.1D + k * (level * 0.025D), player.motionZ * k * (0.2D + (0.1D * level))); // if player, send velocity update to client if (target instanceof EntityPlayerMP && !player.worldObj.isRemote) { ((EntityPlayerMP) target).playerNetServerHandler.sendPacket(new S12PacketEntityVelocity(target)); } } } WorldUtils.playSoundAtEntity(player, Sounds.SLAM, 0.4F, 0.5F); setNotDashing(); } @Override @SideOnly(Side.CLIENT) public boolean isAnimating() { return isActive; // don't continue render tick updates after impact } @Override @SideOnly(Side.CLIENT) public boolean onRenderTick(EntityPlayer player, float partialTickTime) { if (target instanceof EntityLivingBase && trajectory != null) { double speed = player.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.movementSpeed).getAttributeValue() - BASE_MOVE; double dfactor = (1.0D + (speed) + (speed * (1.0D - ((getRange() - distance) / getRange())))); player.motionX = trajectory.xCoord * dfactor * dfactor; player.motionZ = trajectory.zCoord * dfactor * dfactor; } return false; // this skill doesn't need to control the camera } @Override public boolean onBeingAttacked(EntityPlayer player, DamageSource source) { if (impactTime > 0 && source.getEntity() == target) { return true; } else if (source.damageType.equals("mob") && source.getEntity() != null && player.getDistanceSqToEntity(source.getEntity()) < 6.0D) { return true; // stop stupid zombies from hitting player right before impact } return false; } /** * After calling this method, {@link #isAnimating()} will always return false; * {@link #isActive()} will return false if no entity was impacted, otherwise it * will still be true for {@link #impactTime} ticks to prevent damage from the {@link #target}. */ private void setNotDashing() { isActive = false; distance = 0.0D; initialPosition = null; if (!isActive()) { target = null; } } }