Files
KauriV3-mirror/src/main/java/dev/brighten/ac/utils/XPotion.java
T
2022-08-10 14:42:22 -04:00

434 lines
15 KiB
Java

/*
* 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.
* <p>
* 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.
* <p>
* 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<XPotion> 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 <code>values.length + 1</code> 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.<br>
* 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<XPotion> 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.
* <br>
* Format: <b>Potion, Duration (in seconds), Amplifier (level) [%chance]</b>
* <pre>
* WEAKNESS, 30, 1
* SLOWNESS 200 10
* 1, 10000, 100 %50
* </pre>
* 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<XPotion> 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<String> 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<Effect> parseEffects(@Nullable List<String> effectsString) {
if (effectsString == null || effectsString.isEmpty()) return new ArrayList<>();
List<Effect> 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.
* <p>
* An invocation of this method yields exactly the same result as the expression:
* <p>
* <blockquote>
* {@link #getPotionEffectType()} != null
* </blockquote>
*
* @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<String, XPotion> 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;
}
}
}