/** 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 java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.ZSSMain; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.client.SyncSkillPacket; import zeldaswordskills.ref.ModInfo; import zeldaswordskills.skills.sword.ArmorBreak; import zeldaswordskills.skills.sword.BackSlice; import zeldaswordskills.skills.sword.Dash; import zeldaswordskills.skills.sword.Dodge; import zeldaswordskills.skills.sword.EndingBlow; import zeldaswordskills.skills.sword.LeapingBlow; import zeldaswordskills.skills.sword.MortalDraw; import zeldaswordskills.skills.sword.Parry; import zeldaswordskills.skills.sword.RisingCut; import zeldaswordskills.skills.sword.SpinAttack; import zeldaswordskills.skills.sword.SwordBasic; import zeldaswordskills.skills.sword.SwordBeam; import zeldaswordskills.skills.sword.SwordBreak; /** * * Abstract base skill class provides foundation for both passive and active skills * */ public abstract class SkillBase { /** Default maximum skill level */ public static final byte MAX_LEVEL = 5; /** For convenience in providing initial id values */ private static byte skillIndex = 0; /** Map containing all registered skills */ private static final Map<Byte, SkillBase> skillsMap = new HashMap<Byte, SkillBase>(); /** List of registered skills' unlocalized names, for use in Commands */ // if the skillsMap was keyed by unlocalized name, could just return the key set private static final List<String> skillNames = new ArrayList<String>(); public static final SkillBase swordBasic = new SwordBasic("swordbasic").addDescriptions(1); public static final SkillBase armorBreak = new ArmorBreak("armorbreak").addDescriptions(1); public static final SkillBase dodge = new Dodge("dodge").addDescriptions(1); public static final SkillBase leapingBlow = new LeapingBlow("leapingblow").addDescriptions(1); public static final SkillBase parry = new Parry("parry").addDescriptions(1); public static final SkillBase dash = new Dash("dash").addDescriptions(1); public static final SkillBase spinAttack = new SpinAttack("spinattack").addDescriptions(1); public static final SkillBase superSpinAttack = new SpinAttack("superspinattack").addDescriptions(1); public static final SkillBase swordBeam = new SwordBeam("swordbeam").addDescriptions(1); public static final SkillBase swordBreak = new SwordBreak("swordbreak").addDescriptions(1); public static final SkillBase mortalDraw = new MortalDraw("mortaldraw").addDescriptions(1); /* PASSIVE SKILLS */ public static final SkillBase bonusHeart = new BonusHeart("bonusheart").addDescriptions(1); /* NEW SKILLS */ public static final SkillBase risingCut = new RisingCut("risingcut").addDescriptions(1); public static final SkillBase endingBlow = new EndingBlow("endingblow").addDescriptions(1); public static final SkillBase backSlice = new BackSlice("backslice").addDescriptions(1); /** Unlocalized name for language registry */ private final String unlocalizedName; /** IDs are determined internally; used as key to retrieve skill instance from skills map */ private final byte id; /** Mutable field storing current level for this instance of SkillBase */ protected byte level = 0; /** Contains descriptions for tooltip display */ private final List<String> tooltip = new ArrayList<String>(); /** * 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 * @param register whether to register the skill, adding the skill to the skill list; * seems to always be true since skills are declared statically */ protected SkillBase(String name, boolean register) { this.unlocalizedName = name.toLowerCase(); this.id = skillIndex++; if (register) { if (skillsMap.containsKey(id)) { ZSSMain.logger.warn("CONFLICT @ skill " + id + " id already occupied by " + skillsMap.get(id).unlocalizedName + " while adding " + name); } skillsMap.put(id, this); skillNames.add(unlocalizedName); } } /** * Copy constructor creates a level zero version of the skill */ protected SkillBase(SkillBase skill) { this.unlocalizedName = skill.unlocalizedName; this.id = skill.id; this.tooltip.addAll(skill.tooltip); } /** Returns true if the id provided is mapped to a skill */ public static final boolean doesSkillExist(int id) { return (id >= 0 && id <= Byte.MAX_VALUE && skillsMap.containsKey((byte) id)); } /** Returns a new instance of the skill with id, or null if it doesn't exist */ public static final SkillBase getNewSkillInstance(byte id) { return (skillsMap.containsKey(id) ? skillsMap.get(id).newInstance() : null); } /** Returns the instance of the skill stored in the map if it exists, or null */ public static final SkillBase getSkill(int id) { return (doesSkillExist(id) ? skillsMap.get((byte) id) : null); } /** Returns an iterable collection of all the skills in the map */ public static final Collection<SkillBase> getSkills() { return Collections.unmodifiableCollection(skillsMap.values()); } /** Returns the total number of registered skills */ public static final int getNumSkills() { return skillsMap.size(); } /** Returns all registered skills' unlocalized names as an array */ public static final String[] getSkillNames() { return skillNames.toArray(new String[skillNames.size()]); } /** * Retrieves a skill by its unlocalized name, or null if not found */ public static final SkillBase getSkillByName(String name) { for (SkillBase skill : SkillBase.getSkills()) { if (name.equals(skill.getUnlocalizedName())) { return skill; } } return null; } /** * Returns a leveled skill from an ISkillItem using {@link ISkillItem#getSkillId(ItemStack)} * and {@link ISkillItem#getSkillLevel(ItemStack)}, or null if not possible */ /* TODO public static final SkillBase getSkillFromItem(final ItemStack stack, final ISkillItem item) { return createLeveledSkill(item.getSkillId(stack), item.getSkillLevel(stack)); } */ /** * Returns a leveled skill from an id and level, capped at the max level for the skill; * May return null if the id is invalid or level is less than 1 */ public static final SkillBase createLeveledSkill(final int id, final byte level) { if (doesSkillExist(id) && level > 0) { SkillBase skill = getNewSkillInstance((byte) id); skill.level = (level > skill.getMaxLevel() ? skill.getMaxLevel() : level); return skill; } return null; } /** * Note that mutable objects such as this are not suitable as Map keys */ @Override public int hashCode() { return 31 * (31 + id) + level; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } SkillBase skill = (SkillBase) obj; return (skill.id == this.id && skill.level == this.level); } /** Returns a new instance of the skill with appropriate class type without registering it to the Skill database */ public abstract SkillBase newInstance(); /** * Return the translated name of this skill. Note that the translation is only correct on the client; * on the server, send a ChatComponentTranslation using {@link #getTranslationString()} instead. */ public final String getDisplayName() { return StatCollector.translateToLocal(getFullUnlocalizedName() + ".name"); } /** Returns the string used to translate this skill's name */ public final String getTranslationString() { return getFullUnlocalizedName() + ".name"; } /** Returns the unlocalized name with no prefix, exactly as the skill was registered */ public final String getUnlocalizedName() { return unlocalizedName; } /** Returns the unlocalized name prefixed by 'skill.zss' */ public final String getFullUnlocalizedName() { return "skill.zss." + unlocalizedName; } /** Returns texture path for the skill's icon */ public String getIconTexture() { return ModInfo.ID + ":skillorb_" + unlocalizedName; } /** Returns whether this skill can drop as an orb randomly from mobs */ public boolean canDrop() { return true; } /** Returns whether this skill can generate as random loot in chests */ public boolean isLoot() { return true; } /** Each skill's ID can be used as a key to retrieve it from the map */ public final byte getId() { return id; } /** Returns current skill level */ public final byte getLevel() { return level; } /** Returns max level this skill can reach; override to change */ public byte getMaxLevel() { return MAX_LEVEL; } /** * Returns the key used by the language file for getting tooltip description n * Language file should contain key "skill.{unlocalizedName}.desc.{label}.n" * @param label the category for the data, usually "tooltip" or "info" * @param n if less than zero, ".n" will not be appended */ protected final String getInfoString(String label, int n) { return getFullUnlocalizedName() + ".desc." + label + (n < 0 ? "" : ("." + n)); } /** Allows subclasses to add descriptions of pertinent traits (damage, range, etc.) */ @SideOnly(Side.CLIENT) public void addInformation(List<String> desc, EntityPlayer player) {} /** Adds a single untranslated string to the skill's tooltip display */ protected final SkillBase addDescription(String string) { tooltip.add(string); return this; } /** Adds all entries in the provided list to the skill's tooltip display */ protected final SkillBase addDescription(List<String> list) { for (String s : list) { tooltip.add(s); } return this; } /** * Adds n descriptions to the tooltip using the default 'tooltip' label: * {@link SkillBase#getInfoString(String label, int n) getInfoString} * @param n the number of descriptions to add should be at least 1 */ protected final SkillBase addDescriptions(int n) { for (int i = 1; i <= n; ++i) { tooltip.add(getInfoString("tooltip", i)); } return this; } /** * Returns the translated tooltip, possibly with advanced display with player information */ @SideOnly(Side.CLIENT) public final List<String> getTranslatedTooltip(EntityPlayer player) { List<String> desc = new ArrayList<String>(tooltip.size()); for (String s : tooltip) { desc.add(StatCollector.translateToLocal(s)); } if (Minecraft.getMinecraft().gameSettings.advancedItemTooltips) { addInformation(desc, player); } return desc; } /** Returns the translated list containing Strings for tooltip display */ @SideOnly(Side.CLIENT) public final List<String> getDescription() { List<String> desc = new ArrayList<String>(tooltip.size()); for (String s : tooltip) { desc.add(StatCollector.translateToLocal(s)); } return desc; } /** Returns a personalized tooltip display containing info about skill at current level */ @SideOnly(Side.CLIENT) public List<String> getDescription(EntityPlayer player) { List<String> desc = getDescription(); addInformation(desc, player); return desc; } /** Returns the translated description of the skill's activation requirements (long version) */ public String getActivationDisplay() { return StatCollector.translateToLocal(getFullUnlocalizedName() + ".desc.activate"); } /** Returns a translated description of the skill's AoE, using the value provided */ public String getAreaDisplay(double area) { return StatCollector.translateToLocalFormatted("skill.zss.desc.area", String.format("%.1f", area)); } /** Returns a translated description of the skill's charge time in ticks, using the value provided */ public String getChargeDisplay(int chargeTime) { return StatCollector.translateToLocalFormatted("skill.zss.desc.charge", chargeTime); } /** Returns a translated description of the skill's damage, using the value provided and with "+" if desired */ public String getDamageDisplay(float damage, boolean displayPlus) { return StatCollector.translateToLocalFormatted("skill.zss.desc.damage", (displayPlus ? "+" : ""), String.format("%.1f", damage)); } /** Returns a translated description of the skill's damage, using the value provided and with "+" if desired */ public String getDamageDisplay(int damage, boolean displayPlus) { return StatCollector.translateToLocalFormatted("skill.zss.desc.damage", (displayPlus ? "+" : ""), damage); } /** Returns a translated description of the skill's duration, in ticks or seconds, using the value provided */ public String getDurationDisplay(int duration, boolean inTicks) { return StatCollector.translateToLocalFormatted("skill.zss.desc.duration", (inTicks ? duration : duration / 20), (inTicks ? StatCollector.translateToLocal("skill.zss.ticks") : StatCollector.translateToLocal("skill.zss.seconds"))); } /** Returns a translated description of the skill's exhaustion, using the value provided */ public String getExhaustionDisplay(float exhaustion) { return StatCollector.translateToLocalFormatted("skill.zss.desc.exhaustion", String.format("%.2f", exhaustion)); } /** Returns the translated description of the skill's effect (long version) */ public String getFullDescription() { return StatCollector.translateToLocal(getFullUnlocalizedName() + ".desc.full"); } /** * Returns the skill's current level / max level * @param simpleMax whether to replace the numerical display with MAX LEVEL when appropriate */ public String getLevelDisplay(boolean simpleMax) { return ((simpleMax && level == getMaxLevel()) ? StatCollector.translateToLocal("skill.zss.level.max") : StatCollector.translateToLocalFormatted("skill.zss.desc.level", level, getMaxLevel())); } /** Returns a translated description of the skill's range, using the value provided */ public String getRangeDisplay(double range) { return StatCollector.translateToLocalFormatted("skill.zss.desc.range", String.format("%.1f", range)); } /** Returns a translated description of the skill's time limit, using the value provided */ public String getTimeLimitDisplay(int time) { return StatCollector.translateToLocalFormatted("skill.zss.desc.time", time); } /** Returns true if player meets requirements to learn this skill at target level */ protected boolean canIncreaseLevel(EntityPlayer player, int targetLevel) { return ((level + 1) == targetLevel && targetLevel <= getMaxLevel()); } /** Called each time a skill's level increases; responsible for everything OTHER than increasing the skill's level: applying any bonuses, handling Xp, etc. */ protected abstract void levelUp(EntityPlayer player); /** Recalculates bonuses, etc. upon player respawn; Override if levelUp does things other than just calculate bonuses! */ public void validateSkill(EntityPlayer player) { levelUp(player); } /** Shortcut method to grant skill at current level + 1 */ public final boolean grantSkill(EntityPlayer player) { return grantSkill(player, level + 1); } /** * Attempts to level up the skill to target level, returning true if skill's level increased (not necessarily to the target level) */ public final boolean grantSkill(EntityPlayer player, int targetLevel) { if (targetLevel <= level || targetLevel > getMaxLevel()) { return false; } byte oldLevel = level; while (level < targetLevel && canIncreaseLevel(player, level + 1)) { ++level; levelUp(player); } if (!player.worldObj.isRemote && oldLevel < level) { PacketDispatcher.sendTo(new SyncSkillPacket(this), (EntityPlayerMP) player); } return oldLevel < level; } /** This method should be called every update tick */ public void onUpdate(EntityPlayer player) {} /** Writes mutable data to NBT. */ public abstract void writeToNBT(NBTTagCompound compound); /** Reads mutable data from NBT. */ public abstract void readFromNBT(NBTTagCompound compound); /** Returns a new instance from NBT */ public abstract SkillBase loadFromNBT(NBTTagCompound compound); }