/**
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;
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.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.DamageSource;
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.ZSSMain;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.bidirectional.ActivateSkillPacket;
import zeldaswordskills.network.bidirectional.DeactivateSkillPacket;
import zeldaswordskills.util.PlayerUtils;
/**
*
* Base class for active skills. Extend this class to add specific functionality.
*
* Note that additional fields in child classes are not saved to NBT, so should not be
* used to store any data that needs to be maintained between game sessions.
*
* Unless the skill's activation is handled exclusively by the client side, ONLY activate or trigger
* the skill on the server, as a packet will be sent automatically to notify the client
*
* Each Skill should contain the following documentation followed by a full description
*
* NAME: name of the skill
* Description: one-line summary of skill
* Activation: Standard - activated by selecting the skill in the Skill Bar and pressing 'x'
* Triggered - this skill can not be directly activated by the player
* (toggle) - this skill is toggled on or off when activated
* other - give details of how to activate the skill
* Exhaustion: format is [0.0F +- (amount * level)]
* Damage: if any, give the amount and additional details as necessary
* Duration: if any, give the amount in ticks or seconds, as applicable
* Range: if restricted, give the range in an applicable format, such as in blocks
* Area: if any, give the dimensions and additional details as necessary
* Special: any special notes
*
* Full description goes here.
*
*/
public abstract class SkillActive extends SkillBase
{
/**
* Constructs the first instance of a skill and stores it in the skill list
* @param name this is the unlocalized name and should not contain any spaces
*/
protected SkillActive(String name) {
super(name, true);
}
protected SkillActive(SkillActive skill) {
super(skill);
}
/**
* Return false if this skill may not be directly activated manually, in which case it
* should have some other method of {@link #trigger(World, EntityPlayer) triggering}
* @return Default returns TRUE, allowing activation via {@link #activate}
*/
protected boolean allowUserActivation() {
return true;
}
/** Returns true if this skill is currently active, however that is defined by the child class */
public abstract boolean isActive();
/** Amount of exhaustion added to the player each time this skill is used */
protected abstract float getExhaustion();
/**
* Return true to automatically add exhaustion amount upon activation.
* Used by LeapingBlow, since it may or may not trigger upon landing.
*/
protected boolean autoAddExhaustion() {
return true;
}
@Override
protected void levelUp(EntityPlayer player) {}
/**
* Returns true if this skill can currently be used by the player (i.e. activated or triggered)
* @return Default returns true if the skill's level is at least one and either the player
* is in Creative Mode or the food bar is not empty
*/
public boolean canUse(EntityPlayer player) {
return (level > 0 && (player.capabilities.isCreativeMode || player.getFoodStats().getFoodLevel() > 0));
}
/**
* Called only on the client side as a pre-activation check for some skills;
* typical use is to check if all necessary keys have been pressed
*/
@SideOnly(Side.CLIENT)
public boolean canExecute(EntityPlayer player) {
return true;
}
/**
* Return true if {@link #keyPressed} should be called when the given key is pressed
*/
@SideOnly(Side.CLIENT)
public boolean isKeyListener(Minecraft mc, KeyBinding key) {
return false;
}
/**
* This method is called if {@link #isKeyListener} returns true for the given key,
* allowing the skill to handle the key input accordingly. Note that each key press
* may only be handled once, on a first-come first-serve basis.
* @return True signals that the key press was handled: no other key listeners
* will receive this key press
*/
@SideOnly(Side.CLIENT)
public boolean keyPressed(Minecraft mc, KeyBinding key, EntityPlayer player) {
return false;
}
/**
* Whether this skill automatically sends an {@link ActivateSkillPacket} to the client from {@link #trigger}
*/
protected boolean sendClientUpdate() {
return true;
}
/**
* Called after a skill is activated via {@link #trigger}; on the client, this only
* gets called after receiving the {@link ActivateSkillPacket} sent when triggered on
* the server. If {@link #sendClientUpdate} returns false, then the packet is not sent
* and this method will not be called on the client, in which case any client-side
* requirements (e.g. player.swingItem) should be done when sending the activation
* packet to the server.
*
* Anything that needs to happen when the skill is activated should be done here,
* such as setting timers, etc.
*
* @return Return true to have this skill added to the currently active skills list;
* Typically returns {@link #isActive}, which is almost always true after this method is called
*/
protected abstract boolean onActivated(World world, EntityPlayer player);
/**
* Called when the skill is forcefully deactivated on either side via {@link #deactivate}.
*
* Each implementation MUST guarantee that {@link isActive} no longer returns
* true once the method has completed.
*
* {@link #isAnimating} is not required to return false after deactivation, though it usually should.
*/
protected abstract void onDeactivated(World world, EntityPlayer player);
/**
* This is the method that should be called when a player tries to activate a skill
* directly, e.g. from a key binding, HUD, or other such means, to ensure that skills
* with special activation requirements are not circumvented: i.e. {@link #trigger} is
* called only if direct {@link #allowUserActivation() user activation} is allowed.
*
* @return false if the skill could not be activated, or returns {@link #trigger}
*/
public final boolean activate(World world, EntityPlayer player) {
return (allowUserActivation() ? trigger(world, player, false) : false);
}
/**
* Forcefully deactivates a skill.
*
* Call this method on either side to ensure that the skill is deactivated on both.
* If the skill is currently {@link #isActive active}, then {@link #onDeactivated}
* is called and a {@link DeactivateSkillPacket} is sent to the other side.
*
* If the skill is still active after onDeactivated was called, a SEVERE message
* is generated noting the skill that failed to meet onDeactivated's specifications,
* as such behavior may result in severe instability or even crashes and should be
* fixed immediately.
*/
public final void deactivate(EntityPlayer player) throws IllegalStateException {
if (isActive()) {
onDeactivated(player.worldObj, player);
if (isActive()) {
ZSSMain.logger.error(getDisplayName() + " is still active after onDeactivated called - this may result in SEVERE errors or even crashes!!!");
} else if (player.worldObj.isRemote) {
PacketDispatcher.sendToServer(new DeactivateSkillPacket(this));
} else {
PacketDispatcher.sendTo(new DeactivateSkillPacket(this), (EntityPlayerMP) player);
}
}
}
/**
* This method should not be called directly except from an {@link ActivateSkillPacket}
* sent by a skill when it determines that any special activation requirements have been
* met (e.g. Armor Break must first charge up by holding the 'attack' key for a while).
*
* If {@link #canUse} returns true, the skill will be activated.
* {@link #getExhaustion} is added if {@link #autoAddExhaustion} is true, and an
* {@link ActivateSkillPacket} is sent to the client if required.
*
* Finally, {@link #onActivated} is called, allowing the skill to initialize its
* active state.
*
* @param wasTriggered Flag for {@link ActivateSkillPacket} when received on the client:
* true to call {@link #trigger}, false to call {@link #activate}.
* @return Returns {@link #onActivated}, signaling whether or not to add the skill to the
* list of currently active skills.
*/
public final boolean trigger(World world, EntityPlayer player, boolean wasTriggered) {
if (canUse(player)) {
if (autoAddExhaustion() && !player.capabilities.isCreativeMode) {
player.addExhaustion(getExhaustion());
}
if (!world.isRemote) {
if (sendClientUpdate()) {
PacketDispatcher.sendTo(new ActivateSkillPacket(this, wasTriggered), (EntityPlayerMP) player);
}
}
return onActivated(world, player);
} else {
if (level > 0) {
PlayerUtils.sendTranslatedChat(player, "chat.zss.skill.use.fail", new ChatComponentTranslation(getTranslationString()));
}
return false;
}
}
/**
* Return true to flag this skill as requiring animation, in which case interactions
* via mouse or keyboard are disabled while {@link #isAnimating} returns true
* @return Default is TRUE - override for skills that do not have animations
*/
public boolean hasAnimation() {
return true;
}
/**
* Whether this skill's animation is currently in progress, in which case {@link #onRenderTick}
* will be called each render tick and mouse/keyboard interactions are disabled.
* @return Default implementation returns {@link #isActive()}
*/
@SideOnly(Side.CLIENT)
public boolean isAnimating() {
return isActive();
}
/**
* This method is called each render tick that {@link #isAnimating} returns true
* @param partialTickTime The current render tick time
* @return Return true to prevent the targeting camera from auto-updating the player's view
*/
@SideOnly(Side.CLIENT)
public boolean onRenderTick(EntityPlayer player, float partialTickTime) {
return false;
}
/**
* Called from LivingAttackEvent only if the skill is currently {@link #isActive() active}
* @param player The skill-using player under attack
* @param source The source of damage; source#getEntity() is the entity that will strike the player,
* source.getSourceOfDamage() is either the same, or the entity responsible for
* unleashing the other entity (such as the shooter of an arrow)
* @return Return true to cancel the attack event
*/
public boolean onBeingAttacked(EntityPlayer player, DamageSource source) {
return false;
}
/**
* Called from LivingHurtEvent when a player using this skill first damages an entity,
* before any other modifiers are applied.
* The skill should currently be {@link #isActive() active}. Setting the event damage
* to zero or canceling the event will prevent any further processing of the LivingHurtEvent.
* @param player The skill-using player inflicting damage (i.e. event.source.getEntity() is the player)
* @param event The hurt event may be canceled, damage amount modified, etc.
*/
//public void onImpact(EntityPlayer player, LivingHurtEvent event) {}
/**
* Called from LivingHurtEvent only if the skill is currently {@link #isActive() active}
* for the player that inflicted the damage, after all damage modifiers have been taken
* into account, providing a final chance to modify the damage or perform other actions.
*
* @param player The skill-using player inflicting damage (i.e. event.source.getEntity() is the player)
* @param entity The entity damaged, i.e. LivingHurtEvent's entityLiving
* @param amount The current damage amount from {@link LivingHurtEvent#ammount}
* @return The final damage amount to inflict
*/
public float postImpact(EntityPlayer player, EntityLivingBase entity, float amount) {
return amount;
}
@Override
public final void writeToNBT(NBTTagCompound compound) {
compound.setByte("id", getId());
compound.setByte("level", level);
}
@Override
public final void readFromNBT(NBTTagCompound compound) {
level = compound.getByte("level");
}
@Override
public final SkillActive loadFromNBT(NBTTagCompound compound) {
SkillActive skill = (SkillActive) getNewSkillInstance(compound.getByte("id"));
skill.readFromNBT(compound);
return skill;
}
}