/* * The MIT License (MIT) * * Copyright (c) 2022 Crypto Morin * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package dev.brighten.ac.utils; import com.google.common.base.Strings; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.commons.lang.WordUtils; import org.apache.commons.lang.math.NumberUtils; import org.bukkit.Material; import org.bukkit.entity.LivingEntity; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionType; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.ThreadLocalRandom; /** * Potion type support for multiple aliases. * Uses EssentialsX potion list for aliases. *

* Duration: The duration of the effect in ticks. Values 0 or lower are treated as 1. Optional, and defaults to 1 tick. * Amplifier: The amplifier of the effect, with level I having value 0. Optional, and defaults to level I. *

* EssentialsX Potions: https://github.com/EssentialsX/Essentials/blob/2.x/Essentials/src/com/earth2me/essentials/Potions.java * Status Effect: https://minecraft.gamepedia.com/Status_effect * Potions: https://minecraft.gamepedia.com/Potion * * @author Crypto Morin * @version 3.1.0 * @see PotionEffect * @see PotionEffectType * @see PotionType */ public enum XPotion { ABSORPTION("ABSORB"), BAD_OMEN("OMEN_BAD", "PILLAGER"), BLINDNESS("BLIND"), CONDUIT_POWER("CONDUIT", "POWER_CONDUIT"), CONFUSION("NAUSEA", "SICKNESS", "SICK"), DAMAGE_RESISTANCE("RESISTANCE", "ARMOR", "DMG_RESIST", "DMG_RESISTANCE"), DARKNESS, DOLPHINS_GRACE("DOLPHIN", "GRACE"), FAST_DIGGING("HASTE", "SUPER_PICK", "DIGFAST", "DIG_SPEED", "QUICK_MINE", "SHARP"), FIRE_RESISTANCE("FIRE_RESIST", "RESIST_FIRE", "FIRE_RESISTANCE"), GLOWING("GLOW", "SHINE", "SHINY"), HARM("INJURE", "DAMAGE", "HARMING", "INFLICT", "INSTANT_DAMAGE"), HEAL("HEALTH", "INSTA_HEAL", "INSTANT_HEAL", "INSTA_HEALTH", "INSTANT_HEALTH"), HEALTH_BOOST("BOOST_HEALTH", "BOOST", "HP"), HERO_OF_THE_VILLAGE("HERO", "VILLAGE_HERO"), HUNGER("STARVE", "HUNGRY"), INCREASE_DAMAGE("STRENGTH", "BULL", "STRONG", "ATTACK"), INVISIBILITY("INVISIBLE", "VANISH", "INVIS", "DISAPPEAR", "HIDE"), JUMP("LEAP", "JUMP_BOOST"), LEVITATION("LEVITATE"), LUCK("LUCKY"), NIGHT_VISION("VISION", "VISION_NIGHT"), POISON("VENOM"), REGENERATION("REGEN"), SATURATION("FOOD"), SLOW("SLOWNESS", "SLUGGISH"), SLOW_DIGGING("FATIGUE", "DULL", "DIGGING", "SLOW_DIG", "DIG_SLOW"), SLOW_FALLING("SLOW_FALL", "FALL_SLOW"), SPEED("SPRINT", "RUNFAST", "SWIFT", "FAST"), UNLUCK("UNLUCKY"), WATER_BREATHING("WATER_BREATH", "UNDERWATER_BREATHING", "UNDERWATER_BREATH", "AIR"), WEAKNESS("WEAK"), WITHER("DECAY"); /** * Cached list of {@link XPotion#values()} to avoid allocating memory for * calling the method every time. * * @since 1.0.0 */ public static final XPotion[] VALUES = values(); /** * An unmodifiable set of "bad" potion effects. * * @since 1.1.0 */ public static final Set DEBUFFS = Collections.unmodifiableSet(EnumSet.of( BAD_OMEN, BLINDNESS, CONFUSION, HARM, HUNGER, LEVITATION, POISON, SLOW, SLOW_DIGGING, UNLUCK, WEAKNESS, WITHER) ); /** * Efficient mapping to get {@link XPotion} from a {@link PotionEffectType} * Note that values.length + 1 is intentional as it allocates one useless space since IDs start from 1 */ private static final XPotion[] POTIONEFFECTTYPE_MAPPING = new XPotion[VALUES.length + 1]; static { for (XPotion pot : VALUES) if (pot.type != null) //noinspection deprecation POTIONEFFECTTYPE_MAPPING[pot.type.getId()] = pot; } private final PotionEffectType type; XPotion(@Nonnull String... aliases) { this.type = PotionEffectType.getByName(this.name()); Data.NAMES.put(this.name(), this); for (String legacy : aliases) Data.NAMES.put(legacy, this); } /** * Attempts to build the string like an enum name.
* Removes all the spaces, numbers and extra non-English characters. Also removes some config/in-game based strings. * While this method is hard to maintain, it's extremely efficient. It's approximately more than x5 times faster than * the normal RegEx + String Methods approach for both formatted and unformatted material names. * * @param name the potion effect type name to format. * * @return an enum name. * @since 1.0.0 */ @Nonnull private static String format(@Nonnull String name) { int len = name.length(); char[] chs = new char[len]; int count = 0; boolean appendUnderline = false; for (int i = 0; i < len; i++) { char ch = name.charAt(i); if (!appendUnderline && count != 0 && (ch == '-' || ch == ' ' || ch == '_') && chs[count] != '_') appendUnderline = true; else { if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) { if (appendUnderline) { chs[count++] = '_'; appendUnderline = false; } chs[count++] = (char) (ch & 0x5f); } } } return new String(chs, 0, count); } /** * Parses a potion effect type from the given string. * Supports type IDs. * * @param potion the type of the type's ID of the potion effect type. * * @return a potion effect type. * @since 1.0.0 */ @Nonnull public static Optional matchXPotion(@Nonnull String potion) { Validate.notEmpty(potion, "Cannot match XPotion of a null or empty potion effect type"); PotionEffectType idType = fromId(potion); if (idType != null) { XPotion type = Data.NAMES.get(idType.getName()); if (type == null) throw new NullPointerException("Unsupported potion effect type ID: " + idType); return Optional.of(type); } return Optional.ofNullable(Data.NAMES.get(format(potion))); } /** * Parses the XPotion for this potion effect. * * @param type the potion effect type. * * @return the XPotion of this potion effect. * @throws IllegalArgumentException may be thrown as an unexpected exception. * @since 1.0.0 */ @SuppressWarnings("deprecation") @Nonnull public static XPotion matchXPotion(@Nonnull PotionEffectType type) { Objects.requireNonNull(type, "Cannot match XPotion of a null potion effect type"); return POTIONEFFECTTYPE_MAPPING[type.getId()]; } /** * Parses the type ID if available. * * @param type the ID of the potion effect type. * * @return a potion effect type from the ID, or null if it's not an ID or the effect is not found. * @since 1.0.0 */ @Nullable @SuppressWarnings("deprecation") private static PotionEffectType fromId(@Nonnull String type) { try { int id = Integer.parseInt(type); return PotionEffectType.getById(id); } catch (NumberFormatException ex) { return null; } } /** * Parse a {@link PotionEffect} from a string, usually from config. * Supports potion type IDs. *
* Format: Potion, Duration (in seconds), Amplifier (level) [%chance] *

     *     WEAKNESS, 30, 1
     *     SLOWNESS 200 10
     *     1, 10000, 100 %50
     * 
* The last argument (the amplifier can also have a chance which if not met, returns null. * * @param potion the potion string to parse. * * @return a potion effect, or null if the potion type is wrong. * @see #buildPotionEffect(int, int) * @since 1.0.0 */ @Nullable public static Effect parseEffect(@Nullable String potion) { if (Strings.isNullOrEmpty(potion) || potion.equalsIgnoreCase("none")) return null; String[] split = StringUtils.split(StringUtils.deleteWhitespace(potion), ','); if (split.length == 0) split = StringUtils.split(potion, ' '); double chance = 100; int chanceIndex = 0; if (split.length > 2) { chanceIndex = split[2].indexOf('%'); if (chanceIndex != -1) chance = NumberUtils.toDouble(split[2].substring(chanceIndex + 1), 100); } Optional typeOpt = matchXPotion(split[0]); if (!typeOpt.isPresent()) return null; PotionEffectType type = typeOpt.get().type; if (type == null) return null; int duration = 2400; // 20 ticks * 60 seconds * 2 minutes int amplifier = 0; if (split.length > 1) { duration = NumberUtils.toInt(split[1]) * 20; if (split.length > 2) amplifier = NumberUtils.toInt(chanceIndex <= 0 ? split[2] : split[2].substring(0, chanceIndex)) - 1; } return new Effect(new PotionEffect(type, duration, amplifier), chance); } /** * Add a list of potion effects to an entity from a string list, usually from config. * * @param entity the entity to add potion effects to. * @param effects the list of potion effects to parse and add to the entity. * * @see #parseEffect(String) * @since 1.0.0 */ public static void addEffects(@Nonnull LivingEntity entity, @Nullable List effects) { Objects.requireNonNull(entity, "Cannot add potion effects to null entity"); for (Effect effect : parseEffects(effects)) effect.apply(entity); } /** * @param effectsString a list of effects with a format following {@link #parseEffect(String)} * * @return a list of parsed effets. * @since 3.0.0 */ public static List parseEffects(@Nullable List effectsString) { if (effectsString == null || effectsString.isEmpty()) return new ArrayList<>(); List effects = new ArrayList<>(effectsString.size()); for (String effectStr : effectsString) { Effect effect = parseEffect(effectStr); if (effect != null) effects.add(effect); } return effects; } /** * Checks if a material can have potion effects. * This method does not check for {@code LEGACY} materials. * You should avoid using them or use XMaterial instead. * * @param material the material to check. * * @return true if the material is a potion, otherwise false. * @since 1.0.0 */ public static boolean canHaveEffects(@Nullable Material material) { return material != null && (material.name().endsWith("POTION") || material.name().startsWith("TIPPED_ARROW")); } /** * Parses the potion effect type. * * @return the parsed potion effect type. * @see #getPotionType() * @since 1.0.0 */ @Nullable public PotionEffectType getPotionEffectType() { return this.type; } /** * Checks if this potion is supported in the current Minecraft version. *

* An invocation of this method yields exactly the same result as the expression: *

*

* {@link #getPotionEffectType()} != null *
* * @return true if the current version has this potion effect type, otherwise false. * @since 1.0.0 */ public boolean isSupported() { return this.type != null; } /** * Gets the PotionType from this PotionEffectType. * Usually for potion items. * * @return a potion type for potions. * @see #getPotionEffectType() * @since 1.0.0 * @deprecated not for removal, but use {@link PotionEffectType} instead. */ @Nullable @Deprecated public PotionType getPotionType() { return type == null ? null : PotionType.getByEffect(type); } /** * Builds a potion effect with the given duration and amplifier. * * @param duration the duration of the potion effect. * @param amplifier the amplifier of the potion effect. * * @return a potion effect. * @see #parseEffect(String) * @since 1.0.0 */ @Nullable public PotionEffect buildPotionEffect(int duration, int amplifier) { return type == null ? null : new PotionEffect(type, duration, amplifier); } /** * In most cases you should be using {@link #name()} instead. * * @return a friendly readable string name. */ @Override public String toString() { return WordUtils.capitalize(this.name().replace('_', ' ').toLowerCase(Locale.ENGLISH)); } /** * Used for data that need to be accessed during enum initialization. * * @since 2.0.0 */ private static final class Data { private static final Map NAMES = new HashMap<>(); } /** * For now, this merely acts as a chance wrapper for potion effects. * * @since 3.0.0 */ public static class Effect { private PotionEffect effect; private double chance; public Effect(PotionEffect effect, double chance) { this.effect = effect; this.chance = chance; } public XPotion getXPotion() { return XPotion.matchXPotion(effect.getType()); } public double getChance() { return chance; } public boolean hasChance() { return chance >= 100 || ThreadLocalRandom.current().nextDouble(0, 100) <= chance; } public void setChance(double chance) { this.chance = chance; } public void apply(LivingEntity entity) { if (hasChance()) entity.addPotionEffect(effect); } public PotionEffect getEffect() { return effect; } public void setEffect(PotionEffect effect) { this.effect = effect; } } }