/**
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.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import zeldaswordskills.api.damage.DamageUtils;
import zeldaswordskills.entity.DirtyEntityAccessor;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.server.EndComboPacket;
import zeldaswordskills.network.server.TargetIdPacket;
import zeldaswordskills.ref.Config;
import zeldaswordskills.ref.Sounds;
import zeldaswordskills.skills.Combo;
import zeldaswordskills.skills.ICombo;
import zeldaswordskills.skills.ILockOnTarget;
import zeldaswordskills.skills.SkillActive;
import zeldaswordskills.util.PlayerUtils;
import zeldaswordskills.util.TargetUtils;
import zeldaswordskills.util.WorldUtils;
/**
*
* BASIC SWORD SKILL
* Description: Foundation for all other Sword Skills
* Activation: Standard (toggle), but must be looking near a target within range
* Effects: 1. must be active in order to use any of the Sword Skills (see below)
* 2. camera locks on to target so long as player remains within range
* 3. chain up to (2 + level) attacks:
* - each attack adds the combo's current size minus one to damage
* - taking more than (0.5F * level) in damage at once will terminate an ongoing combo, as will
* missing a strike or taking too long between consecutive hits
* Exhaustion: 0.0F - does not cost exertion to use
* Duration: (a) targeting: unlimited
* (b) combo: time allowed between strikes is 20 ticks + (2 * level)
* Range: 6 + level, distance within which targets can be acquired, in blocks
* Special: - intended (but not required) for player to use keyboard instead of mouse while skill is active
* - deactivates if the player is no longer holding a sword or if there are no longer any valid targets
*
* Basic sword technique skill; it is a prerequisite for all other sword skills and may only
* remain active while a sword is in hand.
*
* While active, the player's field of view is locked onto the current target; pressing the 'next
* target' key (default TAB) will switch to the next closest available target that hasn't been
* targeted before, or the previous target if no new targets are available
*
* While active, 'R-Ctrl' may be used to block in lieu of the right mouse button
*
* Combos may be performed while locked on using normal attacks and any known sword skills.
*
* Up to 3 attacks can be chained at level 1, plus an additional attack per skill level. Additionally,
* each Basic Sword level increases the amount of time allowed between successive attacks while
* chaining combos, the amount of damage the player can take before the combo is broken and also the
* maximum distance at which the player may remain locked on to a target.
*
* Default Controls
* Tab (tap) - acquire next target
* RCtrl (hold) - block
* Up arrow (tap) - regular attack
* Up arrow (tap while jumping) - Leaping Blow
* Up arrow (tap while blocking) - Slam
* Up arrow (hold) - Armor Break
* Left / Right arrow (tap) - Dodge
* Left / Right arrow (hold) - Spin Attack
* Down arrow (tap) - Parry/Disarm
*
*/
public class SwordBasic extends SkillActive implements ICombo, ILockOnTarget
{
/** True if this skill is currently active */
private boolean isActive = false;
/** The current target, if any; kept synchronized between the client and server */
private EntityLivingBase currentTarget = null;
/** The previous target; only used client side */
@SideOnly(Side.CLIENT)
private EntityLivingBase prevTarget;
/** Set to a new instance each time a combo begins */
private Combo combo = null;
public SwordBasic(String name) {
super(name);
}
private SwordBasic(SwordBasic skill) {
super(skill);
}
@Override
public SwordBasic newInstance() {
return new SwordBasic(this);
}
@Override
@SideOnly(Side.CLIENT)
public void addInformation(List<String> desc, EntityPlayer player) {
desc.add(getRangeDisplay(getRange()));
desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 1), getMaxComboSize()));
desc.add(getTimeLimitDisplay(getComboTimeLimit()));
desc.add(StatCollector.translateToLocalFormatted(getInfoString("info", 2), String.format("%.1f", (0.5F * level))));
}
@Override
public boolean canUse(EntityPlayer player) {
return level > 0;
}
@Override
public boolean isActive() {
return isActive;
}
@Override
public boolean hasAnimation() {
return false;
}
@Override
protected float getExhaustion() {
return 0.0F;
}
@Override
public byte getMaxLevel() {
return (MAX_LEVEL * 2);
}
/** Returns amount of time allowed between successful attacks before combo terminates */
private final int getComboTimeLimit() {
return (20 + (level * 2));
}
/** Returns the max combo size attainable (2 plus skill level) */
private final int getMaxComboSize() {
return (2 + level);
}
/** Returns max distance at which targets may be acquired or remain targetable */
private final int getRange() {
return (6 + level);
}
@Override
protected boolean onActivated(World world, EntityPlayer player) {
if (isActive) { // deactivate if already active
onDeactivated(world, player); // don't need to use deactivate, as packets already sent
} else { // otherwise activate
isActive = true;
if (!isComboInProgress()) {
combo = null;
}
currentTarget = TargetUtils.acquireLookTarget(player, getRange(), getRange(), true);
}
return true;
}
@Override
protected void onDeactivated(World world, EntityPlayer player) {
isActive = false;
currentTarget = null;
if (world.isRemote) {
prevTarget = null;
}
}
@Override
public void onUpdate(EntityPlayer player) {
if (isActive() && player.worldObj.isRemote) {
if (Minecraft.getMinecraft().currentScreen != null || !updateTargets(player)) {
deactivate(player);
}
}
if (isComboInProgress()) {
combo.onUpdate(player);
}
}
@Override
@SideOnly(Side.CLIENT)
public boolean onRenderTick(EntityPlayer player, float partialTickTime) {
double dx = player.posX - currentTarget.posX;
double dz = player.posZ - currentTarget.posZ;
double angle = Math.atan2(dz, dx) * 180 / Math.PI;
double pitch = Math.atan2((player.posY + player.getEyeHeight()) - (currentTarget.posY + (currentTarget.height / 2.0F)), Math.sqrt(dx * dx + dz * dz)) * 180 / Math.PI;
double distance = player.getDistanceToEntity(currentTarget);
float rYaw = (float)(angle - player.rotationYaw);
while (rYaw > 180) { rYaw -= 360; }
while (rYaw < -180) { rYaw += 360; }
rYaw += 90F;
float rPitch = (float) pitch - (float)(10.0F / Math.sqrt(distance)) + (float)(distance * Math.PI / 90);
player.setAngles(rYaw, -(rPitch - player.rotationPitch));
return false;
}
@Override
public final boolean isLockedOn() {
return currentTarget != null;
}
@Override
public final EntityLivingBase getCurrentTarget() {
return currentTarget;
}
@Override
public void setCurrentTarget(EntityPlayer player, Entity target) {
if (target instanceof EntityLivingBase) {
currentTarget = (EntityLivingBase) target;
} else { // null or invalid target, deactivate skill
deactivate(player);
}
}
/**
* Returns the next closest new target or locks on to the previous target, if any
*/
@Override
@SideOnly(Side.CLIENT)
public final void getNextTarget(EntityPlayer player) {
EntityLivingBase nextTarget = null;
double dTarget = 0;
List<EntityLivingBase> list = TargetUtils.acquireAllLookTargets(player, getRange(), getRange());
for (EntityLivingBase entity : list) {
if (entity == player) { continue; }
if (entity != currentTarget && entity != prevTarget && isTargetValid(player, entity)) {
if (nextTarget == null) {
dTarget = player.getDistanceSqToEntity(entity);
nextTarget = entity;
} else {
double distance = player.getDistanceSqToEntity(entity);
if (distance < dTarget) {
nextTarget = entity;
dTarget = distance;
}
}
}
}
if (nextTarget != null) {
prevTarget = currentTarget;
currentTarget = nextTarget;
} else {
nextTarget = currentTarget;
currentTarget = prevTarget;
prevTarget = nextTarget;
}
PacketDispatcher.sendToServer(new TargetIdPacket(this));
}
/**
* Updates targets, setting to null if no longer valid and acquiring new target if necessary
* @return returns true if the current target is valid
*/
@SideOnly(Side.CLIENT)
private boolean updateTargets(EntityPlayer player) {
if (!isTargetValid(player, prevTarget) || !TargetUtils.isTargetInSight(player, prevTarget)) {
prevTarget = null;
}
if (!isTargetValid(player, currentTarget)) {
currentTarget = null;
if (Config.enableAutoTarget) {
getNextTarget(player);
}
}
return isTargetValid(player, currentTarget);
}
/**
* Returns true if target entity is valid: not dead and still within lock-on range
*/
@SideOnly(Side.CLIENT)
private boolean isTargetValid(EntityPlayer player, EntityLivingBase target) {
return (target != null && !target.isDead && target.getHealth() > 0F &&
player.getDistanceToEntity(target) < (float) getRange() && !target.isInvisible() &&
(Config.canTargetPlayers || !(target instanceof EntityPlayer)));
}
@Override
public final Combo getCombo() {
return combo;
}
@Override
public final void setCombo(Combo combo) {
this.combo = combo;
}
@Override
public final boolean isComboInProgress() {
return (combo != null && !combo.isFinished());
}
@Override
@SideOnly(Side.CLIENT)
public boolean onAttack(EntityPlayer player) {
Entity mouseOver = TargetUtils.getMouseOverEntity();
boolean attackHit = (isLockedOn() && mouseOver != null && TargetUtils.canReachTarget(player, mouseOver));
if (!attackHit) {
PlayerUtils.playRandomizedSound(player, Sounds.SWORD_MISS, 0.4F, 0.5F);
if (isComboInProgress()) {
PacketDispatcher.sendToServer(new EndComboPacket(this));
}
}
return attackHit;
}
@Override
public void onHurtTarget(EntityPlayer player, LivingHurtEvent event) {
boolean addHitFlag = event.source.getDamageType().equals(DamageUtils.INDIRECT_COMBO);
if (event.source.isProjectile() && !addHitFlag) { return; }
if (combo == null || combo.isFinished()) {
combo = new Combo(player, this, getMaxComboSize(), getComboTimeLimit());
}
float damage = DirtyEntityAccessor.getModifiedDamage(event.entityLiving, event.source, event.ammount);
if (damage > 0) {
boolean flag = event.source.getDamageType().equals(DamageUtils.IARMOR_BREAK);
if (flag || event.source.getDamageType().equals(DamageUtils.INDIRECT_SWORD)) {
combo.addDamageOnly(player, damage, flag);
} else {
combo.add(player, event.entityLiving, damage);
}
}
if (addHitFlag || event.source.getDamageType().equals("player")) {
String sound = (PlayerUtils.isSword(player.getHeldItem()) ? Sounds.SWORD_CUT : Sounds.HURT_FLESH);
WorldUtils.playSoundAtEntity(player, sound, 0.4F, 0.5F);
}
}
@Override
public void onPlayerHurt(EntityPlayer player, LivingHurtEvent event) {
if (isComboInProgress() && DirtyEntityAccessor.getModifiedDamage(player, event.source, event.ammount) > (0.5F * level)) {
WorldUtils.playSoundAtEntity(player, Sounds.GRUNT, 0.3F, 0.8F);
combo.endCombo(player);
}
}
}