/**
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.entity.player;
import java.util.EnumMap;
import java.util.Map;
import net.minecraft.entity.Entity;
import net.minecraft.entity.ai.attributes.IAttribute;
import net.minecraft.entity.ai.attributes.RangedAttribute;
import net.minecraft.entity.passive.EntityHorse;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
import net.minecraftforge.common.IExtendedEntityProperties;
import net.minecraftforge.common.util.Constants;
import org.apache.commons.lang3.ArrayUtils;
import zeldaswordskills.api.item.ArmorIndex;
import zeldaswordskills.entity.ZSSEntityInfo;
import zeldaswordskills.entity.buff.Buff;
import zeldaswordskills.entity.player.quests.ZSSQuests;
import zeldaswordskills.handler.ZSSCombatEvents;
import zeldaswordskills.item.ItemArmorBoots;
import zeldaswordskills.item.ItemHeroBow;
import zeldaswordskills.item.ItemMask;
import zeldaswordskills.item.ItemZeldaShield;
import zeldaswordskills.item.ZSSItems;
import zeldaswordskills.network.PacketDispatcher;
import zeldaswordskills.network.client.AttackBlockedPacket;
import zeldaswordskills.network.client.SetNockedArrowPacket;
import zeldaswordskills.network.client.SpawnNayruParticlesPacket;
import zeldaswordskills.network.client.SyncCurrentMagicPacket;
import zeldaswordskills.network.client.SyncPlayerInfoPacket;
import zeldaswordskills.network.client.SyncQuestsPacket;
import zeldaswordskills.network.server.RequestCurrentMagicPacket;
import zeldaswordskills.ref.Config;
import zeldaswordskills.util.PlayerUtils;
public class ZSSPlayerInfo implements IExtendedEntityProperties
{
private static final String EXT_PROP_NAME = "ZSSPlayerInfo";
private final EntityPlayer player;
private final ZSSPlayerSkills playerSkills;
private final ZSSPlayerSongs playerSongs;
/** Current magic points */
private float mp, lastMp;
public static final IAttribute maxMagic = (new RangedAttribute(null, "zss.max_magic", 50.0D, 0.0D, Double.MAX_VALUE)).setDescription("Max Magic").setShouldWatch(true);
/** Last ridden entity so it can be set after player is no longer riding */
private Entity lastRidden;
/** Maximum time the player may be prevented from taking a left-click action */
private final static int MAX_ATTACK_DELAY = 50;
/** Time remaining until player may perform another left-click action, such as an attack */
private int attackTime; // TODO move to ZSSEntityInfo ? or player skills?
/** Special block timer for shields; player cannot block while this is greater than zero */
private int blockTime = 0;
public static enum Stats {
/** Number of secret rooms this player has opened */
STAT_SECRET_ROOMS,
/** Bit storage for each type of boss room opened */
STAT_BOSS_ROOMS
};
/** ZSS player statistics */
private final Map<Stats, Integer> playerStats = new EnumMap<Stats, Integer>(Stats.class);
/** Set to true when the player receives the bonus starting gear */
private byte receivedGear = 0;
/** Flags for worn gear and other things; none of these should be saved */
private byte flags = 0;
public static final byte
/** Flag for whether the player is wearing special boots */
IS_WEARING_BOOTS = 1,
/** Flag for whether the player is wearing special headgear */
IS_WEARING_HELM = 2,
/** Flag for whether Nayru's Love is currently active */
IS_NAYRU_ACTIVE = 4,
/** Flag for whether the player's lateral movement should be increased while in the air */
MOBILITY = 8;
/** Last equipped special boots */
private ItemStack lastBootsWorn;
/** Last equipped special helm (used for masks) */
private ItemStack lastHelmWorn;
/** Current amount of time hovered */
public int hoverTime = 0;
@Deprecated
private Item borrowedMask = null;
@Deprecated
private int maskStage = 0;
/** Maximum number of skulltula tokens which can be turned in */
public static final int MAX_SKULLTULA_TOKENS = 100;
/** Number of Gold Skulltula Tokens this player has turned in */
private int skulltulaTokens = 0;
/** [Hero's Bow] Stores the currently nocked arrow in order to avoid the graphical glitch caused by writing to the stack's NBT */
private ItemStack arrowStack = null;
/** [Hero's Bow] Part of the graphical glitch fix: Whether the player retrieved a bomb arrow via getAutoBombArrow */
public boolean hasAutoBombArrow = false;
/** [Slingshot] Current mode index */
public int slingshotMode = 0;
/** Reduces fall damage next impact; used for Rising Cut */
public float reduceFallAmount = 0.0F;
public ZSSPlayerInfo(EntityPlayer player) {
this.player = player;
playerSkills = new ZSSPlayerSkills(player);
playerSongs = new ZSSPlayerSongs(player);
player.getAttributeMap().registerAttribute(maxMagic).setBaseValue(50.0D);
mp = getMaxMagic(); // don't use setter here: don't want to send packet
initStats();
}
@Override
public void init(Entity entity, World world) {}
private void initStats() {
for (Stats stat : Stats.values()) {
playerStats.put(stat, 0);
}
}
/**
* Returns the player's current MP
*/
public float getCurrentMagic() {
if (mp > getMaxMagic()) {
setCurrentMagic(getMaxMagic());
}
return mp;
}
/**
* Restores the given amount of MP
*/
public void restoreMagic(float amount) {
setCurrentMagic(getCurrentMagic() + amount);
}
/**
* Sets the player's current MP
*/
public void setCurrentMagic(float value) {
this.mp = MathHelper.clamp_float(value, 0.0F, getMaxMagic());
}
/**
* Sets the player's current MP upon first joining the world (since max MP attribute not yet synced)
*/
public void setInitialMagic(float value) {
this.mp = Math.max(0.0F, value);
}
/**
* Returns the player's current maximum magic points
*/
public float getMaxMagic() {
return (float) player.getAttributeMap().getAttributeInstance(maxMagic).getAttributeValue();
}
/**
* Sets the player's maximum magic points
*/
public void setMaxMagic(float value) {
value = MathHelper.clamp_float(value, 0.0F, (float)Config.getMaxMagicPoints());
player.getAttributeMap().getAttributeInstance(maxMagic).setBaseValue(value);
if (getCurrentMagic() > getMaxMagic()) {
setCurrentMagic(getMaxMagic());
}
}
/**
* Depletes the magic bar by the amount whether the player has enough magic or not
* @return True if the player had sufficient magic for the amount
*/
public boolean consumeMagic(float amount) {
return (canUseMagic() ? useMagic(amount, true) : false);
}
/**
* Depletes the magic bar by the amount only if the player has sufficient magic
* @return True if the player had sufficient magic for the amount
*/
public boolean useMagic(float amount) {
return (canUseMagic() ? useMagic(amount, false) : false);
}
/**
* Returns true if current magic is at least equal to the amount to use
* @param consume true to deplete magic bar even if magic is insufficient
*/
private boolean useMagic(float amount, boolean consume) {
if (player.capabilities.isCreativeMode || ZSSEntityInfo.get(player).isBuffActive(Buff.UNLIMITED_MAGIC)) {
return true;
}
boolean sufficient = amount <= getCurrentMagic();
if (sufficient || consume) {
setCurrentMagic(getCurrentMagic() - amount);
}
return sufficient;
}
/**
* Returns whether the player is currently allowed to use magic
*/
public boolean canUseMagic() {
return player.capabilities.isCreativeMode || (getMaxMagic() > 0 && !isNayruActive());
}
/** Gets the player's current stat value */
public int getStat(Stats stat) {
return playerStats.get(stat);
}
/** Adds the value to the player's stats */
public void addStat(Stats stat, int value) {
int i = playerStats.remove(stat);
switch(stat) {
case STAT_BOSS_ROOMS: playerStats.put(stat, i | value); break;
default: playerStats.put(stat, i + value);
}
}
public ZSSPlayerSkills getPlayerSkills() {
return playerSkills;
}
public ZSSPlayerSongs getPlayerSongs() {
return playerSongs;
}
/**
* True if the player can perform a left-click action (i.e. the action timer is zero)
*/
public boolean canAttack() {
return attackTime == 0 || player.capabilities.isCreativeMode;
}
/**
* Returns the current amount of time remaining before a left-click action may be performed
*/
public int getAttackTime() {
return attackTime;
}
/**
* Sets the number of ticks remaining before another action may be performed, but
* no less than the current value and no more than MAX_ATTACK_DELAY.
*/
public void setAttackTime(int ticks) {
this.attackTime = MathHelper.clamp_int(ticks, attackTime, MAX_ATTACK_DELAY);
}
/** Whether the player is able to block at this time (block timer is zero) */
public boolean canBlock() {
return blockTime == 0;
}
/**
* Sets the player's block timer, clears the item in use and adds exhaustion upon blocking an attack
* @param damage only used server side to calculate exhaustion: 0.3F * damage
*/
public void onAttackBlocked(ItemStack shield, float damage) {
ZSSCombatEvents.setPlayerAttackTime(player);
blockTime = (shield.getItem() instanceof ItemZeldaShield ? ((ItemZeldaShield) shield.getItem()).getRecoveryTime() : 20);
player.clearItemInUse();
if (player instanceof EntityPlayerMP) {
PacketDispatcher.sendTo(new AttackBlockedPacket(shield), (EntityPlayerMP) player);
player.addExhaustion(0.3F * damage);
}
}
/**
* If player has not received starting gear, it is provided
*/
public void verifyStartingGear() {
if ((receivedGear & 1) == 0 && ZSSItems.grantBonusGear(player)) {
receivedGear |= 1;
}
}
/**
* Sets a bitflag to true or false; see flags for valid flag ids
*/
public void setFlag(byte flag, boolean value) {
if (value) {
flags |= flag;
} else {
flags &= ~flag;
}
}
/** Returns true if the specified flag is set */
public boolean getFlag(byte flag) {
return (flags & flag) == flag;
}
/** Activates Nayru's Love effect; should call on both client and server */
public void activateNayru() {
if (Config.allowUnlimitedNayru() || !ZSSEntityInfo.get(player).isBuffActive(Buff.UNLIMITED_MAGIC)) {
setFlag(IS_NAYRU_ACTIVE, true);
}
}
/** Returns whether the player is currently under the effects of Nayru's Love */
public boolean isNayruActive() {
return getFlag(IS_NAYRU_ACTIVE);
}
/**
* Sets whether player is wearing special boots based on currently equipped gear
* and applies / removes modifiers as appropriate
* @param boots Currently equipped boots (null if none worn)
*/
public void setWearingBoots(ItemStack boots) {
if (lastBootsWorn != null && lastBootsWorn.getItem() instanceof ItemArmorBoots) {
((ItemArmorBoots) lastBootsWorn.getItem()).removeModifiers(lastBootsWorn, player);
}
lastBootsWorn = boots;
setFlag(IS_WEARING_BOOTS, (boots != null && boots.getItem() instanceof ItemArmorBoots));
if (getFlag(IS_WEARING_BOOTS)) {
((ItemArmorBoots) boots.getItem()).applyModifiers(boots, player);
}
if (getFlag(IS_WEARING_HELM)) {
ItemStack helm = player.getCurrentArmor(ArmorIndex.WORN_HELM);
if (helm != null && helm.getItem() instanceof ItemMask) {
((ItemMask) helm.getItem()).applyModifiers(helm, player);
}
}
}
/**
* Checks whether player is wearing a special helm based on currently equipped gear
* and applies / removes modifiers as appropriate
* @param helm Currently equipped helm (null if none worn)
*/
public void setWearingHelm(ItemStack helm) {
if (lastHelmWorn != null && lastHelmWorn.getItem() instanceof ItemMask) {
((ItemMask) lastHelmWorn.getItem()).removeModifiers(lastHelmWorn, player);
}
lastHelmWorn = helm;
setFlag(IS_WEARING_HELM, (helm != null && helm.getItem() instanceof ItemMask));
if (getFlag(IS_WEARING_HELM)) {
((ItemMask) helm.getItem()).applyModifiers(helm, player);
}
if (getFlag(IS_WEARING_BOOTS)) {
ItemStack boots = player.getCurrentArmor(ArmorIndex.WORN_BOOTS);
if (boots != null && boots.getItem() instanceof ItemArmorBoots) {
((ItemArmorBoots) boots.getItem()).applyModifiers(boots, player);
}
}
}
@Deprecated
public Item getBorrowedMask() {
return borrowedMask;
}
@Deprecated
public void setBorrowedMask(Item item) {
borrowedMask = item;
}
@Deprecated
public int getCurrentMaskStage() {
return maskStage;
}
/**
* Returns true if the player is able to turn in more Skulltula Tokens
*/
public boolean canIncrementSkulltulaTokens() {
return skulltulaTokens < MAX_SKULLTULA_TOKENS;
}
/**
* Returns number of skulltula tokens this player has given to the Cursed Man
*/
public int getSkulltulaTokens() {
return skulltulaTokens;
}
/**
* Attempts to turn in (i.e. consume) a skulltula token, returning true on success.
*/
public boolean incrementSkulltulaTokens() {
if (canIncrementSkulltulaTokens() && PlayerUtils.consumeHeldItem(player, ZSSItems.skulltulaToken, 1)) {
++skulltulaTokens;
return true;
}
return false;
}
/**
* Returns the currently nocked arrow for the Hero's Bow, possibly null
*/
public ItemStack getNockedArrow() {
return arrowStack;
}
/**
* Marks this arrow as nocked in the Hero's Bow
* @param stack The current arrow or null if empty
*/
public void setNockedArrow(ItemStack stack) {
arrowStack = stack;
if (player instanceof EntityPlayerMP) {
PacketDispatcher.sendTo(new SetNockedArrowPacket(stack), (EntityPlayerMP) player);
}
}
/**
* This method should be called every update tick; currently called from LivingUpdateEvent
*/
public void onUpdate() {
playerSkills.onUpdate();
// keeps MP packet count down to maximum of 1 per tick
if (!player.worldObj.isRemote && lastMp != mp) {
lastMp = mp;
if (player instanceof EntityPlayerMP) {
PacketDispatcher.sendTo(new SyncCurrentMagicPacket(player), (EntityPlayerMP) player);
}
}
if (attackTime > 0) {
--attackTime;
}
if (blockTime > 0) {
--blockTime;
}
if (getFlag(IS_NAYRU_ACTIVE)) {
updateNayru();
}
if (getFlag(IS_WEARING_BOOTS)) {
ItemStack boots = player.getCurrentArmor(ArmorIndex.WORN_BOOTS);
if (boots == null || (lastBootsWorn != null && boots.getItem() != lastBootsWorn.getItem())) {
setWearingBoots(boots);
}
}
if (getFlag(IS_WEARING_HELM)) {
ItemStack helm = player.getCurrentArmor(ArmorIndex.WORN_HELM);
if (helm == null || (lastHelmWorn != null && helm.getItem() != lastHelmWorn.getItem())) {
setWearingHelm(helm);
}
}
if (getFlag(MOBILITY) && !player.onGround && Math.abs(player.motionY) > 0.05D
&& !player.capabilities.isFlying && player.worldObj.getTotalWorldTime() % 2 == 0) {
player.motionX *= 1.15D;
player.motionZ *= 1.15D;
}
if (hasAutoBombArrow && (player.getHeldItem() == null || !(player.getHeldItem().getItem() instanceof ItemHeroBow))) {
hasAutoBombArrow = false;
}
// Check for currently ridden horse, used for Epona's Song
if (lastRidden == null && player.ridingEntity != null) {
lastRidden = player.ridingEntity;
if (lastRidden instanceof EntityHorse) {
playerSongs.setHorseRidden((EntityHorse) lastRidden);
}
} else if (player.ridingEntity == null && lastRidden instanceof EntityHorse) {
playerSongs.setHorseRidden((EntityHorse) lastRidden);
lastRidden = null; // dismounted and horse's last known coordinates set
}
}
/**
* Updates effects of Nayru's Love
*/
private void updateNayru() {
player.hurtResistantTime = player.maxHurtResistantTime;
if (player.ticksExisted % 4 == 0) {
if (!useMagic(0.5F, true)) { // call private method directly to circumvent canUseMagic()
setFlag(IS_NAYRU_ACTIVE, false);
} else if (player instanceof EntityPlayerMP) {
PacketDispatcher.sendToAllAround(new SpawnNayruParticlesPacket(player), player, 64.0D);
}
}
}
/** Used to register these extended properties for the player during EntityConstructing event */
public static final void register(EntityPlayer player) {
player.registerExtendedProperties(EXT_PROP_NAME, new ZSSPlayerInfo(player));
ZSSQuests.register(player);
}
/** Returns ExtendedPlayer properties for player */
public static final ZSSPlayerInfo get(EntityPlayer player) {
return (ZSSPlayerInfo) player.getExtendedProperties(EXT_PROP_NAME);
}
/**
* Called when a player logs in for the first time
*/
public void onPlayerLoggedIn() {
if (player instanceof EntityPlayerMP) {
verifyStartingGear();
}
}
/**
* Call each time the player joins the world to sync data to the client
*/
public void onJoinWorld() {
if (player instanceof EntityPlayerMP) {
playerSkills.validateSkills();
playerSkills.verifyMaxHealth();
PacketDispatcher.sendTo(new SyncPlayerInfoPacket(this), (EntityPlayerMP) player);
PacketDispatcher.sendTo(new SyncQuestsPacket(ZSSQuests.get(player)), (EntityPlayerMP) player);
} else { // Re-request current mana (truncated by attribute having incorrect value initially)
PacketDispatcher.sendToServer(new RequestCurrentMagicPacket());
}
}
/**
* Copies given data to this one when a player is cloned
* If the client also needs the data, the packet must be sent from
* EntityJoinWorldEvent to ensure it is sent to the new client player
*/
public void copy(ZSSPlayerInfo info) {
NBTTagCompound compound = new NBTTagCompound();
info.saveNBTData(compound);
this.loadNBTData(compound);
ZSSQuests.get(this.player).copy(ZSSQuests.get(info.player));
}
@Override
public void saveNBTData(NBTTagCompound compound) {
playerSkills.saveNBTData(compound);
playerSongs.saveNBTData(compound);
compound.setFloat("zssCurrentMagic", mp);
compound.setIntArray("zssStats", ArrayUtils.toPrimitive(playerStats.values().toArray(new Integer[playerStats.size()])));
compound.setByte("ZSSGearReceived", receivedGear);
if (lastBootsWorn != null) {
NBTTagCompound tag = new NBTTagCompound();
compound.setTag("lastBootsWorn", lastBootsWorn.writeToNBT(tag));
}
if (lastHelmWorn != null) {
NBTTagCompound tag = new NBTTagCompound();
compound.setTag("lastHelmWorn", lastHelmWorn.writeToNBT(tag));
}
compound.setInteger("slingshotMode", slingshotMode);
compound.setInteger("skulltulaTokens", skulltulaTokens);
}
@Override
public void loadNBTData(NBTTagCompound compound) {
playerSkills.loadNBTData(compound);
playerSongs.loadNBTData(compound);
mp = compound.getFloat("zssCurrentMagic");
int[] stats = compound.getIntArray("zssStats");
for (int i = 0; i < stats.length; ++i) {
playerStats.put(Stats.values()[i], stats[i]);
}
receivedGear = compound.getByte("ZSSGearReceived");
if (compound.hasKey("lastBootsWorn", Constants.NBT.TAG_COMPOUND)) {
lastBootsWorn = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("lastBootsWorn"));
}
if (compound.hasKey("lastHelmWorn", Constants.NBT.TAG_COMPOUND)) {
lastHelmWorn = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("lastHelmWorn"));
}
// For backwards compatibility:
Item boots = Item.getItemById(compound.getInteger("lastBoots"));
if (boots != null) {
lastBootsWorn = new ItemStack(boots);
}
Item helm = Item.getItemById(compound.getInteger("lastHelm"));
if (helm != null) {
lastHelmWorn = new ItemStack(helm);
}
// For backwards compatibility:
int maskID = compound.getInteger("borrowedMask");
borrowedMask = maskID > -1 ? Item.getItemById(maskID) : null;
maskStage = compound.getInteger("maskStage");
slingshotMode = compound.getInteger("slingshotMode");
skulltulaTokens = compound.getInteger("skulltulaTokens");
}
}