/** 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.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.attributes.AttributeModifier; import net.minecraft.entity.ai.attributes.IAttributeInstance; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.IChatComponent; import net.minecraft.world.World; import net.minecraftforge.common.util.Constants; import net.minecraftforge.event.entity.living.LivingAttackEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.ZSSAchievements; import zeldaswordskills.client.ZSSKeyHandler; import zeldaswordskills.item.ItemTreasure.Treasures; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.client.SyncPlayerInfoPacket; import zeldaswordskills.network.client.SyncSkillPacket; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.Sounds; import zeldaswordskills.skills.ICombo; import zeldaswordskills.skills.ILockOnTarget; import zeldaswordskills.skills.SkillActive; import zeldaswordskills.skills.SkillBase; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.TimedAddItem; import zeldaswordskills.util.TimedChatDialogue; /** * * Handles all skill-related information for the player. * */ public class ZSSPlayerSkills { private static final UUID hardcoreHeartUUID = UUID.fromString("83E54288-6BE2-4398-A880-957654F515AB"); /** Max health modifier for players with Hardcore Zelda Fan mode enabled */ private static final AttributeModifier hardcoreHeartModifier = (new AttributeModifier(hardcoreHeartUUID, "Hardcore Zelda Hearts", -14.0D, 0)).setSaved(true); private final EntityPlayer player; /** Stores information on the player's Attributes and Passive Skills */ private final Map<Byte, SkillBase> skills; /** Currently active skills */ private final List<SkillActive> activeSkills = new LinkedList<SkillActive>(); /** * Currently animating skill that {@link SkillActive#hasAnimation() has an animation}; * it may or may not currently be {@link SkillActive#isAnimating() animating} */ @SideOnly(Side.CLIENT) private SkillActive animatingSkill; /** Number of crests given to Orca, implying number of Hurricane Spin orbs received */ private int crestsGiven = 0; public ZSSPlayerSkills(EntityPlayer player) { this.player = player; this.skills = new HashMap<Byte, SkillBase>(SkillBase.getNumSkills()); } public static ZSSPlayerSkills get(EntityPlayer player) { return ZSSPlayerInfo.get(player).getPlayerSkills(); } /** * Removes the skill with the given name, or "all" skills * @param name Unlocalized skill name or "all" to remove all skills * @return False if no skill was removed */ public boolean removeSkill(String name) { if (("all").equals(name)) { resetSkills(); return true; } else { // TODO change skill storage to use unlocalized name instead of id SkillBase dummy = null; for (SkillBase skill : skills.values()) { if (skill.getUnlocalizedName().equals(name)) { dummy = skill; break; } } if (dummy != null) { removeSkill(dummy); return true; } } return false; } private void removeSkill(SkillBase skill) { SkillBase dummy = skill.newInstance(); skills.put(dummy.getId(), dummy); validateSkills(); skills.remove(dummy.getId()); if (player instanceof EntityPlayerMP) { PacketDispatcher.sendTo(new SyncSkillPacket(dummy), (EntityPlayerMP) player); } } /** * Resets all data related to skills */ public void resetSkills() { // need level zero skills for validation, specifically for attribute-affecting skills for (SkillBase skill : SkillBase.getSkills()) { skills.put(skill.getId(), skill.newInstance()); } validateSkills(); skills.clear(); crestsGiven = 0; if (player instanceof EntityPlayerMP) { PacketDispatcher.sendTo(new SyncPlayerInfoPacket(ZSSPlayerInfo.get(player)), (EntityPlayerMP) player); } } /** * Validates each skill upon player respawn, ensuring all bonuses are correct */ public final void validateSkills() { for (SkillBase skill : skills.values()) { skill.validateSkill(player); } } /** * Applies hardcore Zelda fan heart modifier, if appropriate */ public void verifyMaxHealth() { IAttributeInstance attributeinstance = player.getEntityAttribute(SharedMonsterAttributes.maxHealth); if (attributeinstance.getModifier(hardcoreHeartUUID) != null) { attributeinstance.removeModifier(hardcoreHeartModifier); } if (Config.isHardcoreZeldaFan()) { attributeinstance.applyModifier(hardcoreHeartModifier); } if (player.getHealth() > player.getMaxHealth()) { player.setHealth(player.getMaxHealth()); } } /** Returns true if the player has at least one level in the specified skill */ public boolean hasSkill(SkillBase skill) { return hasSkill(skill.getId()); } /** Returns true if the player has at least one level in the specified skill (of any class) */ private boolean hasSkill(byte id) { return getSkillLevel(id) > 0; } /** Returns the player's skill level for given skill, or 0 if the player doesn't have that skill */ public byte getSkillLevel(SkillBase skill) { return getSkillLevel(skill.getId()); } /** Returns the player's skill level for given skill, or 0 if the player doesn't have that skill */ public byte getSkillLevel(byte id) { return (skills.containsKey(id) ? skills.get(id).getLevel() : 0); } /** * Returns true if the player has the skill and the skill is currently active */ public boolean isSkillActive(SkillBase skill) { SkillBase active = getPlayerSkill(skill); return (active instanceof SkillActive && ((SkillActive) active).isActive()); } /** * Returns the {@link #animatingSkill}, which may be null */ @SideOnly(Side.CLIENT) public SkillActive getCurrentlyAnimatingSkill() { return animatingSkill; } /** * This method is called automatically from {@link #onSkillActivated} for each skill activated. * @param skill If this skill {@link SkillActive#hasAnimation has an animation}, it will be set * as the currently animating skill. */ @SideOnly(Side.CLIENT) public void setCurrentlyAnimatingSkill(SkillActive skill) { animatingSkill = (skill == null || skill.hasAnimation() ? skill : animatingSkill); } /** * Returns whether key/mouse input and skill interactions are currently allowed, * i.e. the {@link #animatingSkill} is either null or not currently animating */ @SideOnly(Side.CLIENT) public boolean canInteract() { // don't set the current skill to null just yet if it is still animating // this allows skills to prevent key/mouse input without having to be 'active' if (animatingSkill != null && !animatingSkill.isActive() && !animatingSkill.isAnimating()) {//!isSkillActive(currentActiveSkill)) { animatingSkill = null; } return animatingSkill == null || !animatingSkill.isAnimating(); } /** * Call when a key is pressed to pass the key press to the player's skills' * {@link SkillActive#keyPressed keyPressed} method, but only if the skill returns * true from {@link SkillActive#isKeyListener isKeyListener} for the key pressed. * The first skill to return true from keyPressed precludes any remaining skills * from receiving the key press. * @return True if a listening skill's {@link SkillActive#keyPressed} signals that the key press was handled */ @SideOnly(Side.CLIENT) public boolean onKeyPressed(Minecraft mc, KeyBinding key) { // For docs: If there is a skill currently active, it is given first priority. // give precedence to currently active skill? except those that need it (e.g. ArmorBreak) may not yet be set as the active skill /*if (currentActiveSkill != null && currentActiveSkill.isKeyListener(mc, key)) { if (currentActiveSkill.keyPressed(mc, key, player)) { return true; } }*/ for (SkillBase skill : skills.values()) { if (skill instanceof SkillActive && ((SkillActive) skill).isKeyListener(mc, key)) { if (((SkillActive) skill).keyPressed(mc, key, player)) { return true; } } } return false; } /** * Called from LivingAttackEvent to trigger {@link SkillActive#onBeingAttacked} for each * currently active skill, potentially canceling the event. If the event is canceled, it * returns immediately without processing any remaining active skills. */ public void onBeingAttacked(LivingAttackEvent event) { for (SkillActive skill : activeSkills) { if (skill.isActive() && skill.onBeingAttacked(player, event.source)) { event.setCanceled(true); return; } } } /** * Called from LivingHurtEvent to trigger {@link SkillActive#postImpact} for each * currently active skill, potentially altering the value of event.ammount, as * well as calling {@link ICombo#onHurtTarget onHurtTarget} for the current ICombo. */ public void onPostImpact(LivingHurtEvent event) { for (SkillActive skill : activeSkills) { if (skill.isActive()) { event.ammount = skill.postImpact(player, event.entityLiving, event.ammount); } } // combo gets updated last, after all damage modifications are completed if (getComboSkill() != null) { getComboSkill().onHurtTarget(player, event); } } /** * Returns a SkillActive version of the player's actual skill instance, or * null if the player doesn't have the skill or it is not the correct type. * Note that the skill is not necessarily {@link SkillActive#isActive} - use * {@link #isSkillActive} to check that. */ public SkillActive getActiveSkill(SkillBase skill) { SkillBase active = getPlayerSkill(skill.getId()); return (active instanceof SkillActive ? (SkillActive) active : null); } /** Returns the player's actual skill instance or null if the player doesn't have the skill */ public SkillBase getPlayerSkill(SkillBase skill) { return getPlayerSkill(skill.getId()); } /** Returns the player's actual skill instance or null if the player doesn't have the skill */ public SkillBase getPlayerSkill(byte id) { return (skills.containsKey(id) ? skills.get(id) : null); } /** * Returns first ICombo from a currently active skill, if any; ICombo may or may not be in progress */ public ICombo getComboSkill() { SkillBase skill = getPlayerSkill(SkillBase.swordBasic); if (skill != null && (((ICombo) skill).getCombo() != null || ((SkillActive) skill).isActive())) { return (ICombo) skill; } return null; } /** Returns player's ILockOnTarget skill, if any */ public ILockOnTarget getTargetingSkill() { return (ILockOnTarget) getPlayerSkill(SkillBase.swordBasic); } /** Grants a skill with target level of current skill level plus one */ public boolean grantSkill(SkillBase skill) { return grantSkill(skill.getId(), (byte)(getSkillLevel(skill) + 1)); } /** * Grants skill to player if player meets the requirements; returns true if skill learned */ public boolean grantSkill(byte id, byte targetLevel) { SkillBase skill = skills.containsKey(id) ? (SkillBase) skills.get(id) : SkillBase.getNewSkillInstance(id); if (skill.grantSkill(player, targetLevel)) { skills.put(id, skill); return true; } else { return false; } } /** * Called after {@link SkillActive#onActivated} returns true to add the skill to the * list of currently active skills, as well as set the currently animating skill */ private void onSkillActivated(World world, SkillActive skill) { if (skill.isActive()) { activeSkills.add(skill); if (world.isRemote) { setCurrentlyAnimatingSkill(skill); } } } /** * Returns true if the player has this skill and {@link SkillActive#activate} returns true */ public boolean activateSkill(World world, SkillBase skill) { return activateSkill(world, skill.getId()); } /** * Returns true if the player has this skill and {@link SkillActive#activate} returns true */ public boolean activateSkill(World world, byte id) { SkillBase skill = skills.get(id); if (skill instanceof SkillActive && ((SkillActive) skill).activate(world, player)) { onSkillActivated(world, (SkillActive) skill); return true; } return false; } /** * Returns true if the player has this skill and {@link SkillActive#trigger} returns true */ public boolean triggerSkill(World world, SkillBase skill) { return triggerSkill(world, skill.getId()); } /** * Returns true if the player has this skill and {@link SkillActive#trigger} returns true */ public boolean triggerSkill(World world, byte id) { SkillBase skill = skills.get(id); if (skill instanceof SkillActive && ((SkillActive) skill).trigger(world, player, true)) { onSkillActivated(world, (SkillActive) skill); return true; } return false; } /** * Returns true if this player has completed the Knight's Crest quest line for Orca */ public boolean completedCrests() { return crestsGiven >= 100; } /** * If the player has at least 1 level of Spin Attack and the number of crests given * is less than the max, one Knight's Crest is consumed and the player's progress on * Orca's quest continues; otherwise no crest is given and a chat message displays instead. */ public void giveCrest() { if (getSkillLevel(SkillBase.spinAttack) < 1) { PlayerUtils.sendTranslatedChat(player, "chat.zss.npc.orca.unfit." + player.worldObj.rand.nextInt(4)); } else if (getSkillLevel(SkillBase.superSpinAttack) < Math.min(crestsGiven / 20, SkillBase.superSpinAttack.getMaxLevel())) { PlayerUtils.sendTranslatedChat(player, "chat.zss.npc.orca.unfit." + player.worldObj.rand.nextInt(4)); } else if (getSkillLevel(SkillBase.backSlice) < Math.min((crestsGiven + 10) / 20, SkillBase.backSlice.getMaxLevel())) { PlayerUtils.sendTranslatedChat(player, "chat.zss.npc.orca.unfit." + player.worldObj.rand.nextInt(4)); } else if (crestsGiven >= 100) { PlayerUtils.sendTranslatedChat(player, "chat.zss.npc.orca.master." + player.worldObj.rand.nextInt(4)); } else if (PlayerUtils.consumeInventoryItem(player, ZSSItems.treasure, Treasures.KNIGHTS_CREST.ordinal(), 1)) { ++crestsGiven; List<IChatComponent> chat = new ArrayList<IChatComponent>(); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.redeem." + player.worldObj.rand.nextInt(4))); if (crestsGiven == 1) { new TimedChatDialogue(player, new ChatComponentTranslation("chat.zss.npc.orca.begin.0"), new ChatComponentTranslation("chat.zss.npc.orca.begin.1"), new ChatComponentTranslation("chat.zss.npc.orca.begin.2")); player.triggerAchievement(ZSSAchievements.orcaRequest); return; // prevent other timed chat message } else if (crestsGiven > 19 && (crestsGiven % 20) == 0) { // Every 20 crests Orca will teach one level of Super Spin Attack boolean flag = true; // for timed give item timing if (crestsGiven == 100) { player.triggerAchievement(ZSSAchievements.orcaMaster); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.final.0")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.final.1")); } else if (crestsGiven == 20) { player.triggerAchievement(ZSSAchievements.orcaSecondLesson); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.first.0")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.first.1")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.first.2")); } else { chat.add(new ChatComponentTranslation("chat.zss.npc.orca.hurricane.train")); flag = false; } new TimedAddItem(player, new ItemStack(ZSSItems.skillOrb, 1, SkillBase.superSpinAttack.getId()), (flag ? 4000 : 3000), Sounds.SUCCESS); } else if (crestsGiven > 9 && (crestsGiven % 10) == 0) { // Every 10 crests Orca will teach one more level of Back Slice boolean flag = true; // for timed give item timing if (crestsGiven == 90) { chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.final.0")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.final.1")); } else if (crestsGiven == 10) { player.triggerAchievement(ZSSAchievements.orcaFirstLesson); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.first.0")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.first.1")); chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.first.2")); } else { chat.add(new ChatComponentTranslation("chat.zss.npc.orca.backslice.train")); flag = false; } new TimedAddItem(player, new ItemStack(ZSSItems.skillOrb, 1, SkillBase.backSlice.getId()), (flag ? 4000 : 3000), Sounds.SUCCESS); } else { chat.add(new ChatComponentTranslation("chat.zss.npc.orca.more." + player.worldObj.rand.nextInt(4), crestsGiven)); } new TimedChatDialogue(player, chat.toArray(new IChatComponent[chat.size()])); } } /** * Reads a SkillBase from stream and updates the local skills map; if the skill * loaded from NBT is level 0, that skill will be removed. * Called client side only for synchronizing a skill with the server version. */ @SideOnly(Side.CLIENT) public void syncClientSideSkill(byte id, NBTTagCompound compound) { if (SkillBase.doesSkillExist(id)) { SkillBase skill = SkillBase.getNewSkillInstance(id).loadFromNBT(compound); if (skill.getLevel() > 0) { skills.put(id, skill); } else { skills.remove(id); } } } /** * Call during the render tick to update animating and ILockOnTarget skills */ @SideOnly(Side.CLIENT) public void onRenderTick(float partialRenderTick) { // flags whether a skill is currently animating boolean flag = false; if (animatingSkill != null) { if (animatingSkill.isAnimating()) { flag = animatingSkill.onRenderTick(player, partialRenderTick); } else if (!animatingSkill.isActive()) { setCurrentlyAnimatingSkill(null); } } ILockOnTarget skill = getTargetingSkill(); if (!flag && skill != null && skill.isLockedOn()) { ((SkillActive) skill).onRenderTick(player, partialRenderTick); } } public void onUpdate() { // let skill's update tick occur first for (SkillBase skill : skills.values()) { skill.onUpdate(player); } // and then remove from active list if no longer active // must use iterators to avoid concurrent modification exceptions to list Iterator<SkillActive> iterator = activeSkills.iterator(); while (iterator.hasNext()) { SkillActive skill = iterator.next(); if (!skill.isActive()) { iterator.remove(); } } if (player.worldObj.isRemote) { if (ZSSKeyHandler.keys[ZSSKeyHandler.KEY_BLOCK].isKeyDown() && isSkillActive(SkillBase.swordBasic) && player.getHeldItem() != null) { Minecraft.getMinecraft().playerController.sendUseItem(player, player.worldObj, player.getHeldItem()); } } } public void saveNBTData(NBTTagCompound compound) { NBTTagList taglist = new NBTTagList(); for (SkillBase skill : skills.values()) { NBTTagCompound skillTag = new NBTTagCompound(); skill.writeToNBT(skillTag); taglist.appendTag(skillTag); } compound.setTag("ZeldaSwordSkills", taglist); compound.setInteger("crestsGiven", crestsGiven); } public void loadNBTData(NBTTagCompound compound) { skills.clear(); // allows skills to reset on client without re-adding all the skills NBTTagList taglist = compound.getTagList("ZeldaSwordSkills", Constants.NBT.TAG_COMPOUND); for (int i = 0; i < taglist.tagCount(); ++i) { NBTTagCompound skill = taglist.getCompoundTagAt(i); byte id = skill.getByte("id"); skills.put(id, SkillBase.getSkill(id).loadFromNBT(skill)); } crestsGiven = compound.getInteger("crestsGiven"); } }