diff --git a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/util/NumericStatFormula.java b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/util/NumericStatFormula.java index 6052a90e..39042014 100644 --- a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/util/NumericStatFormula.java +++ b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/api/util/NumericStatFormula.java @@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull; import java.text.DecimalFormat; import java.util.Random; +import java.util.regex.Pattern; /** * That Gaussian spread distribution thing that no one understands. @@ -19,245 +20,359 @@ import java.util.Random; * @author indyuce */ public class NumericStatFormula implements RandomStatData, UpdatableRandomStatData { - private final double base, scale, spread, maxSpread; + private final double base, scale, spread, maxSpread, min, max; + private final boolean uniform, hasMin, hasMax; - private static final Random RANDOM = new Random(); - private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####"); + private static final Random RANDOM = new Random(); + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####"); - public static final NumericStatFormula ZERO = new NumericStatFormula(0, 0, 0, 0); + public static final NumericStatFormula ZERO = new NumericStatFormula(0, 0, 0, 0); - /** - * When reading a numeric stat formula either from a config file - * (configuration section, or number) or when reading player input when a - * player edits a stat (string message). Although the string format would - * work in the config as well. - * - * Throws an IAE either if the format is not good or if the object does not - * have the right type - * - * @param object Object to read data from. - */ - public NumericStatFormula(Object object) { - Validate.notNull(object, "Config must not be null"); + private final static double DEFAULT_MAX_SPREAD = .3; - if (object instanceof String) { - String[] split = object.toString().split(" "); - base = Double.parseDouble(split[0]); - scale = split.length > 1 ? Double.parseDouble(split[1]) : 0; - spread = split.length > 2 ? Double.parseDouble(split[2]) : 0; - maxSpread = split.length > 3 ? Double.parseDouble(split[3]) : 0; - return; - } + /** + * When reading a numeric stat formula either from a config file + * (configuration section, or number) or when reading player input when a + * player edits a stat (string message). Although the string format would + * work in the config as well. + *

+ * Throws an IAE either if the format is not good or if the object does not + * have the right type. + * + * @param object Object to read data from. + */ + public NumericStatFormula(Object object) { + Validate.notNull(object, "Config must not be null"); - if (object instanceof Number) { - base = Double.parseDouble(object.toString()); - scale = 0; - spread = 0; - maxSpread = 0; - return; - } + if (object instanceof String) { - if (object instanceof ConfigurationSection) { - ConfigurationSection config = (ConfigurationSection) object; - base = config.getDouble("base"); - scale = config.getDouble("scale"); - spread = config.getDouble("spread"); - maxSpread = config.getDouble("max-spread", .3); + // Uniform range from string + if (object.toString().contains("->")) { + String[] split = object.toString().replace(" ", "").split(Pattern.quote("->")); + base = 0; + scale = 0; + spread = 0; + maxSpread = 0; + uniform = true; + hasMin = true; + hasMax = true; + min = Double.parseDouble(split[0]); + max = Double.parseDouble(split[1]); + } - Validate.isTrue(spread >= 0, "Spread must be positive"); - Validate.isTrue(maxSpread >= 0, "Max spread must be positive"); - return; - } + // Gaussian distribution from string + else { + String[] split = object.toString().split(" "); + base = Double.parseDouble(split[0]); + scale = split.length > 1 ? Double.parseDouble(split[1]) : 0; + spread = split.length > 2 ? Double.parseDouble(split[2]) : 0; + maxSpread = split.length > 3 ? Double.parseDouble(split[3]) : 0; + hasMin = split.length > 4; + hasMax = split.length > 5; + min = hasMin ? Double.parseDouble(split[4]) : 0; + max = hasMax ? Double.parseDouble(split[5]) : 0; + uniform = false; + } + } - throw new IllegalArgumentException("Must specify a config section, a string or a number"); - } + // Constant + else if (object instanceof Number) { + base = Double.parseDouble(object.toString()); + scale = 0; + spread = 0; + maxSpread = 0; + uniform = false; + hasMin = false; + hasMax = false; + min = 0; + max = 0; + } - /** - * Formula for numeric statistics. These formulas allow stats to scale - * accordingly to the item level but also have a more or less important - * gaussian based random factor - * - * @param base Base value - * @param scale Value which scales with the item level - * @param spread The relative standard deviation of a normal law centered - * on (base + scale * level). If it's set to 0.1, the - * standard deviation will be 10% of the stat value without - * the random factor. - * @param maxSpread The max amount of deviation you can have. If it's set to - * 0.3, let A = base + scale * level, then the final stat - * value will be in [0.7 * A, 1.3 * A] - */ - public NumericStatFormula(double base, double scale, double spread, double maxSpread) { - this.base = base; - this.scale = scale; - this.spread = spread; - this.maxSpread = maxSpread; - } + // Load from config section + else if (object instanceof ConfigurationSection) { + ConfigurationSection config = (ConfigurationSection) object; + base = config.getDouble("base"); + scale = config.getDouble("scale"); + spread = config.getDouble("spread"); + maxSpread = config.getDouble("max-spread", DEFAULT_MAX_SPREAD); + hasMin = config.contains("min"); + hasMax = config.contains("max"); + uniform = !config.contains("spread") && !config.contains("scale") && !config.contains("base") && hasMin && hasMax; + min = config.getDouble("min"); + max = config.getDouble("max"); + } - /** - * @return The 'Base' number of the item. This chooses the peak of the Gaussian Distribution. - * @see #getScale() - */ - public double getBase() { return base; } + // Error + else { + throw new IllegalArgumentException("Must specify a config section, a string or a number"); + } + // Validates + Validate.isTrue(spread >= 0, "Spread must be positive"); + Validate.isTrue(maxSpread >= 0, "Max spread must be positive"); + } - /** - * @return When the item has a certain level or tier, this is how much each level shifts - * the peak, so that it is centered at {@code base + scale*level} - * @see #getBase() - */ - public double getScale() { return scale; } + /** + * A gaussian distribution with spread-based boundaries + */ + public NumericStatFormula(double base, double scale, double spread, double maxSpread) { + this(base, scale, spread, maxSpread, false, false, 0, false, 0); + } - /** - * @return Standard Deviation of the Gaussian Distribution - */ - public double getSpread() { return spread; } + /** + * A gaussian distribution with constant boundaries + */ + public NumericStatFormula(double base, double scale, double spread, double min, double max) { + this(base, scale, spread, 100, false, true, min, true, max); + } - /** - * @return For gaussian distributions, there always is that INSANELY SMALL chance of getting an INSANELY LARGE number. - * For example: At base atk dmg 10, and standard deviation 1: - *

68% of rolls will fall between 9 and 11; - *

95% of rolls will fall between 8 and 12; - *

99.7% of rolls will fall between 7 and 13; - *

10E-42 of a roll that will give you an epic 300 dmg sword - *

- * Whatever, this constrains to a minimum and maximum of output. - */ - public double getMaxSpread() { return maxSpread; } + /** + * Formula for numerical statistics. These formulas allow stats to scale + * accordingly to the item level but also have a more or less important + * gaussian/uniform based random factor. + * + * @param base Base value + * @param scale Value which scales with the item level + * @param spread The relative standard deviation of a normal law centered + * on (base + scale * level). If it's set to 0.1, the + * standard deviation will be 10% of the stat value without + * the random factor. + * @param maxSpread The max amount of deviation you can have. If it's set to + * 0.3, let A = base + scale * level, then the final stat + * value will be in [0.7 * A, 1.3 * A] + * @param hasMin Should the value have a lower threshold + * @param min Lower bound for numerical value + * @param hasMax Should the value have an upper threshold + * @param min Upper bound for numerical value + */ + public NumericStatFormula(double base, double scale, double spread, double maxSpread, + boolean uniform, boolean hasMin, double min, boolean hasMax, double max) { + this.base = base; + this.scale = scale; + this.spread = spread; + this.maxSpread = maxSpread; + this.uniform = uniform; + this.hasMin = hasMin; + this.hasMax = hasMax; + this.min = min; + this.max = max; + } - public static boolean useRelativeSpread; + /** + * @return The 'Base' number of the item. This chooses the peak of the Gaussian Distribution. + * @see #getScale() + */ + public double getBase() { + return base; + } - /** - * Applies the formula for a given input x. - * - * @param levelScalingFactor When choosing the mean of the distribution, - * the formula is base + (scale*level). - * This is the level - * - * @return Legacy formula: ???
- * Let A = {base} + {scale} * lvl, then the returned value is a - * random value taken in respect to a gaussian distribution - * centered on A, with average spread of {spread}%, and with a - * maximum offset of {maxSpread}% (relative to average value) - *

- * Formula: Spread = Standard Deviation - * The mean, the peak is located at {base} + {scale}*lvl.
- * The 'spread' is the standard deviation of the distribution.
- * 'Max Spread' constrains the result of this operation at {mean}±{max spread} - */ - public double calculate(double levelScalingFactor) { + /** + * @return When the item has a certain level or tier, this is how much each level shifts + * the peak, so that it is centered at {@code base + scale*level} + * @see #getBase() + */ + public double getScale() { + return scale; + } - // Calculate yes - return calculate(levelScalingFactor, RANDOM.nextGaussian()); - } + /** + * @return Standard Deviation of the Gaussian Distribution + */ + public double getSpread() { + return spread; + } - /** - * @param levelScalingFactor Level to scale the scale with - * @param random Result of RANDOM.nextGaussian() or whatever other - * value that you actually want to pass. - * @return The calculated value - */ - public double calculate(double levelScalingFactor, double random) { + /** + * @return For gaussian distributions, there always is that INSANELY SMALL + * chance of getting an INSANELY LARGE number. + *

+ * For example: At base atk dmg 10, and standard deviation 1: + *

68% of rolls will fall between 9 and 11; + *

95% of rolls will fall between 8 and 12; + *

99.7% of rolls will fall between 7 and 13; + *

10E-42 of a roll that will give you an epic 300 dmg sword + *

+ * Whatever, this constrains to a minimum and maximum of output. + */ + public double getMaxSpread() { + return maxSpread; + } - // The mean, the center of the distribution - final double actualBase = base + (scale * levelScalingFactor); + public boolean isUniform() { + return uniform; + } - /* - * This is one pick from a gaussian distribution at mean 0, and - * standard deviation 1, multiplied by the spread chosen. - * Does it exceed the max spread (positive or negative)? Not anymore! - */ - final double spreadCoef = Math.min(Math.max(random * spread, -maxSpread), maxSpread); + public boolean hasMin() { + return hasMin; + } - return useRelativeSpread ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef; - } + public boolean hasMax() { + return hasMax; + } - @Override - public DoubleData randomize(MMOItemBuilder builder) { - return new DoubleData(calculate(builder.getLevel())); - } + public double getMin() { + return min; + } - /** - * Save some formula in a config file. This method is used when editing stat - * data in the edition GUI (when a player inputs a numeric formula) - * - * @param config The formula will be saved in that config file - * @param path The config path used to save the formula - */ - public void fillConfigurationSection(ConfigurationSection config, String path, FormulaSaveOption option) { - if (path == null) - throw new NullPointerException("Path was empty"); + public double getMax() { + return max; + } - if (scale == 0 && spread == 0 && maxSpread == 0) - config.set(path, base == 0 && option == FormulaSaveOption.DELETE_IF_ZERO ? null : base); + public static boolean RELATIVE_SPREAD; - else { - config.set(path + ".base", base); - config.set(path + ".scale", scale); - config.set(path + ".spread", spread); - config.set(path + ".max-spread", maxSpread); - } - } + /** + * Applies the formula for a given input x. + * + * @param levelScalingFactor When choosing the mean of the distribution, + * the formula is base + (scale*level). + * This is the level + * @return Legacy formula: ???
+ * Let A = {base} + {scale} * lvl, then the returned value is a + * random value taken in respect to a gaussian distribution + * centered on A, with average spread of {spread}%, and with a + * maximum offset of {maxSpread}% (relative to average value) + *

+ * Formula: Spread = Standard Deviation + * The mean, the peak is located at {base} + {scale}*lvl.
+ * The 'spread' is the standard deviation of the distribution.
+ * 'Max Spread' constrains the result of this operation at {mean}±{max spread} + */ + public double calculate(double levelScalingFactor) { - public void fillConfigurationSection(ConfigurationSection config, String path) { - fillConfigurationSection(config, path, FormulaSaveOption.DELETE_IF_ZERO); - } + // Calculate yes + return calculate(levelScalingFactor, uniform ? RANDOM.nextDouble() : RANDOM.nextGaussian()); + } - @Override - public String toString() { + /** + * @param scaleFactor Level to scale the scale with + * @param random Result of RANDOM.nextGaussian() or whatever other + * value that you actually want to pass. It can be any valuation of + * a random variable with mean 0 and variance 1. + * @return The calculated final numerical value + */ + public double calculate(double scaleFactor, double random) { - if (scale == 0 && spread == 0) - return DECIMAL_FORMAT.format(base); + double value; + if (uniform) { + value = min + (max - min) * random; + } else { - if (scale == 0) - return "[" + DECIMAL_FORMAT.format(base * (1 - maxSpread)) + " -> " + DECIMAL_FORMAT.format(base * (1 + maxSpread)) + "] (" + DECIMAL_FORMAT.format(spread * 100) - + "% Spread) (" + DECIMAL_FORMAT.format(base) + " Avg)"; + // The mean, the center of the distribution + final double actualBase = base + (scale * scaleFactor); - return "{Base=" + DECIMAL_FORMAT.format(base) + (scale != 0 ? ",Scale=" + DECIMAL_FORMAT.format(scale) : "") + (spread != 0 ? ",Spread=" + spread : "") - + (maxSpread != 0 ? ",Max=" + maxSpread : "") + "}"; - } + /* + * This is one pick from a gaussian distribution at mean 0, and + * standard deviation 1, multiplied by the spread chosen. + * Does it exceed the max spread (positive or negative)? Not anymore! + */ + final double spreadCoef = Math.min(Math.max(random * spread, -maxSpread), maxSpread); - public static void reload() { useRelativeSpread = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false); } + value = RELATIVE_SPREAD ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef; + if (hasMin) value = Math.max(min, value); + if (hasMax) value = Math.max(max, value); + } - @NotNull - @SuppressWarnings("unchecked") - @Override - public DoubleData reroll(@NotNull ItemStat stat, @NotNull DoubleData original, int determinedItemLevel) { + return value; + } - // Very well, chance checking is only available for NumericStatFormula class - final double expectedValue = getBase() + (getScale() * determinedItemLevel); - final double previousValue = original.getValue(); - final double shift = previousValue - expectedValue; - final double shiftSD = useRelativeSpread ? Math.abs(shift / (getSpread() * expectedValue)) : Math.abs(shift / getSpread()); - final double maxSD = getMaxSpread() / getSpread(); + @Override + public DoubleData randomize(MMOItemBuilder builder) { + return new DoubleData(calculate(builder.getLevel())); + } - // Greater than max spread? Or heck, 0.1% Chance or less wth - if (shiftSD > maxSD || shiftSD > 3.5) { + /** + * Save some formula in a config file. This method is used when editing stat + * data in the edition GUI (when a player inputs a numeric formula). + * + * @param config The formula will be saved in that config file + * @param path The config path used to save the formula + * @param option If zero formulas should be ignored + */ + public void fillConfigurationSection(@NotNull ConfigurationSection config, @NotNull String path, @NotNull FormulaSaveOption option) { + if (path == null) + throw new NullPointerException("Path is empty"); - // Just fully reroll value - return new DoubleData(calculate(determinedItemLevel)); + if (scale == 0 && spread == 0 && maxSpread == 0 && !uniform) { + config.set(path, base == 0 && option == FormulaSaveOption.DELETE_IF_ZERO ? null : base); + return; + } - // Data arguably fine tbh, just use previous - } else { - //UPGRD//MMOItems.log("\u00a7a +\u00a77 Acceptable Range --- kept"); + config.createSection(path); + config = config.getConfigurationSection(path); - // Just clone I guess - return original.cloneData(); } - } + if (!uniform) { + config.set("base", base); + config.set("scale", scale); + config.set("spread", spread); + config.set("max-spread", maxSpread); + } - public enum FormulaSaveOption { + if (hasMin) config.set("min", min); + if (hasMax) config.set("max", max); + } - /** - * When toggled on, if the formula is set to 0 then the configuration - * section will just be deleted. This option fixes a bug where ability - * modifiers with non null default values cannot take strictly null - * values because inputting 0 would just delete the config section. - */ - DELETE_IF_ZERO, + public void fillConfigurationSection(ConfigurationSection config, String path) { + fillConfigurationSection(config, path, FormulaSaveOption.DELETE_IF_ZERO); + } - /** - * No option used - */ - NONE; - } + @Override + public String toString() { + + if (scale == 0 && spread == 0) + return DECIMAL_FORMAT.format(base); + + if (scale == 0) + return "[" + DECIMAL_FORMAT.format(base * (1 - maxSpread)) + " -> " + DECIMAL_FORMAT.format(base * (1 + maxSpread)) + "] (" + DECIMAL_FORMAT.format(spread * 100) + + "% Spread) (" + DECIMAL_FORMAT.format(base) + " Avg)"; + + return "{Base=" + DECIMAL_FORMAT.format(base) + (scale != 0 ? ",Scale=" + DECIMAL_FORMAT.format(scale) : "") + (spread != 0 ? ",Spread=" + spread : "") + + (maxSpread != 0 ? ",Max=" + maxSpread : "") + "}"; + } + + public static void reload() { + RELATIVE_SPREAD = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false); + } + + @NotNull + @SuppressWarnings("unchecked") + @Override + public DoubleData reroll(@NotNull ItemStat stat, @NotNull DoubleData original, int determinedItemLevel) { + + // Very well, chance checking is only available for NumericStatFormula class + final double expectedValue = getBase() + (getScale() * determinedItemLevel); + final double previousValue = original.getValue(); + final double shift = previousValue - expectedValue; + final double shiftSD = RELATIVE_SPREAD ? Math.abs(shift / (getSpread() * expectedValue)) : Math.abs(shift / getSpread()); + final double maxSD = getMaxSpread() / getSpread(); + + // Greater than max spread? Or heck, 0.1% Chance or less wth + if (shiftSD > maxSD || shiftSD > 3.5) { + + // Just fully reroll value + return new DoubleData(calculate(determinedItemLevel)); + + // Data arguably fine tbh, just use previous + } else { + //UPGRD//MMOItems.log("\u00a7a +\u00a77 Acceptable Range --- kept"); + + // Just clone I guess + return original.cloneData(); + } + } + + public enum FormulaSaveOption { + + /** + * When toggled on, if the formula is set to 0 then the configuration + * section will just be deleted. This option fixes a bug where ability + * modifiers with non null default values cannot take strictly null + * values because inputting 0 would just delete the config section. + */ + DELETE_IF_ZERO, + + /** + * No option used + */ + NONE; + } } diff --git a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/stat/type/DoubleStat.java b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/stat/type/DoubleStat.java index f5579414..76b17d5c 100644 --- a/MMOItems-API/src/main/java/net/Indyuce/mmoitems/stat/type/DoubleStat.java +++ b/MMOItems-API/src/main/java/net/Indyuce/mmoitems/stat/type/DoubleStat.java @@ -39,7 +39,7 @@ import java.util.regex.Pattern; public class DoubleStat extends ItemStat implements Upgradable, Previewable { - private final boolean moreIsBetter; + private final boolean moreIsBetter; private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####"); @@ -57,399 +57,404 @@ public class DoubleStat extends ItemStat impleme this.moreIsBetter = moreIsBetter; } - /** - * @return If this stat supports negatives stat values - */ - public boolean handleNegativeStats() { - return true; - } - - /** - * @return For example knockback resistance, 0.01 = 1% so multiplies by 100 when displaying. - */ - public double multiplyWhenDisplaying() { - return 1; - } - - /** - * Usually, a greater magnitude of stat benefits the player (more health, more attack damage). - *

However, its not impossible for a stat to be evil instead, who knows? - */ - public boolean moreIsBetter() { return moreIsBetter; } - - @Override - public NumericStatFormula whenInitialized(Object object) { - - if (object instanceof Number) - return new NumericStatFormula(Double.parseDouble(object.toString()), 0, 0, 0); - - if (object instanceof ConfigurationSection) - return new NumericStatFormula(object); - - throw new IllegalArgumentException("Must specify a number or a config section"); - } - - @Override - public void whenApplied(@NotNull ItemStackBuilder item, @NotNull DoubleData data) { - - // Get Value - double value = data.getValue(); - - // Cancel if it its NEGATIVE and this doesn't support negative stats. - if (value < 0 && !handleNegativeStats()) { return; } - - // Identify the upgrade amount - double upgradeShift = 0; - - // Displaying upgrades? - if (UpgradeTemplate.isDisplayingUpgrades() && item.getMMOItem().getUpgradeLevel() != 0) { - - // Get stat history - StatHistory hist = item.getMMOItem().getStatHistory(this); - if (hist != null) { - - // Get as if it had never been upgraded - //HSY//MMOItems.log(" \u00a73-\u00a7a- \u00a77Stat Change Display Recalculation \u00a73-\u00a7a-\u00a73-"); - DoubleData uData = (DoubleData) hist.recalculateUnupgraded(); - - // Calculate Difference - upgradeShift = value - uData.getValue(); } } - - // Display in lore - if (value != 0 || upgradeShift != 0) { - String loreInsert = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter, value * multiplyWhenDisplaying()); - if (upgradeShift != 0) - loreInsert += UpgradeTemplate.getUpgradeChangeSuffix(plus(upgradeShift * multiplyWhenDisplaying()) + (MythicLib.plugin.getMMOConfig().decimals.format(upgradeShift * multiplyWhenDisplaying())), !isGood(upgradeShift * multiplyWhenDisplaying())); - item.getLore().insert(getPath(), loreInsert); - } - - /* - * Add NBT Data if it is not equal to ZERO, in which case it will just get removed. - * - * It is important that the tags are not excluded in getAppliedNBT() because the StatHistory does - * need that blanc tag information to remember when an Item did not initially have any of a stat. - */ - if (data.getValue() != 0) { item.addItemTag(getAppliedNBT(data)); } - } - - @NotNull - @Deprecated - public static String formatPath(@NotNull String format, boolean moreIsBetter, double value) { - return formatPath("ATTACK_DAMAGE", format, moreIsBetter, value); - } - - @NotNull - public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double value) { - final String valueFormatted = StatManager.format(stat, value); - final String colorPrefix = getColorPrefix(value < 0 && moreIsBetter); - return format - .replace("{value}", colorPrefix + (value > 0 ? "+" : "") + valueFormatted) // Replace conditional pluses with +value - .replace("{value}", colorPrefix + valueFormatted) // Replace loose pounds with the value - .replace("", (value > 0 ? "+" : "")); // Replace loose es - } - - @NotNull - @Deprecated - public static String formatPath(@NotNull String format, boolean moreIsBetter, double min, double max) { - return formatPath("ATTACK_DAMAGE", format, moreIsBetter, min, max); - } - - @NotNull - public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double min, double max) { - final String minFormatted = StatManager.format(stat, min), maxFormatted = StatManager.format(stat, max); - final String minPrefix = getColorPrefix(min < 0 && moreIsBetter), maxPrefix = getColorPrefix(max < 0 && moreIsBetter); - return format - .replace("", "") - .replace("{value}", - minPrefix + (min > 0 ? "+" : "") + minFormatted - + MMOItems.plugin.getConfig().getString("stats-displaying.range-dash", "⎓") + - maxPrefix + (min < 0 && max > 0 ? "+" : "") + maxFormatted); - } - - @Override - public void whenPreviewed(@NotNull ItemStackBuilder item, @NotNull DoubleData currentData, @NotNull NumericStatFormula templateData) throws IllegalArgumentException { - Validate.isTrue(currentData instanceof DoubleData, "Current Data is not Double Data"); - Validate.isTrue(templateData instanceof NumericStatFormula, "Template Data is not Numeric Stat Formula"); - - // Get Value - //SPRD//MMOItems.log("\u00a7c༺\u00a77 Calulating deviations of \u00a7b" + item.getMMOItem().getType().toString() + " " + item.getMMOItem().getId() + "\u00a77's \u00a7e" + getId()); - double techMinimum = templateData.calculate(0, -2.5); - double techMaximum = templateData.calculate(0, 2.5); - - // Cancel if it its NEGATIVE and this doesn't support negative stats. - if (techMaximum < 0 && !handleNegativeStats()) { return; } - if (techMinimum < 0 && !handleNegativeStats()) { techMinimum = 0; } - // Add NBT Path - item.addItemTag(getAppliedNBT(currentData)); - - // Display if not ZERO - if (techMinimum != 0 || techMaximum != 0) { - - String builtRange; - if (SilentNumbers.round(techMinimum, 2) == SilentNumbers.round(techMaximum, 2)) { - builtRange = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter(), techMaximum * multiplyWhenDisplaying()); - - } else { - builtRange = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter(), techMinimum * multiplyWhenDisplaying(), techMaximum * multiplyWhenDisplaying()); } - - // Just display normally - item.getLore().insert(getPath(), builtRange); } - } - - @NotNull public static String getColorPrefix(boolean isNegative) { - - // Get the base - return Objects.requireNonNull(MMOItems.plugin.getConfig().getString("stats-displaying.color-" + (isNegative ? "negative" : "positive"), "")); - } - - @NotNull String plus(double amount) { if (amount >= 0) { return "+"; } else return ""; } - - /** - * Usually, a greater magnitude of stat benefits the player (more health, more attack damage). - *

However, its not impossible for a stat to be evil instead, who knows? - *

- * This will return true if: - *

> The amount is positive, and more benefits the player - *

> The amount is negative, and more hurts the player - */ - public boolean isGood(double amount) { - return moreIsBetter() ? amount >= 0 : amount <= 0; - } - - @Override - @NotNull - public ArrayList getAppliedNBT(@NotNull DoubleData data) { - - // Create Fresh - ArrayList ret = new ArrayList<>(); - - // Add sole tag - ret.add(new ItemTag(getNBTPath(), data.getValue())); - - // Return thay - return ret; - } - - @Override - public void whenLoaded(@NotNull ReadMMOItem mmoitem) { - - // Get tags - ArrayList relevantTags = new ArrayList<>(); - - // Add sole tag - if (mmoitem.getNBT().hasTag(getNBTPath())) - relevantTags.add(ItemTag.getTagAtPath(getNBTPath(), mmoitem.getNBT(), SupportedNBTTagValues.DOUBLE)); - - // Use that - DoubleData bakedData = (DoubleData) getLoadedNBT(relevantTags); - - // Valid? - if (bakedData != null) { - - // Set - mmoitem.setData(this, bakedData); - } - } - @Override - @Nullable - public DoubleData getLoadedNBT(@NotNull ArrayList storedTags) { - - // You got a double righ - ItemTag tg = ItemTag.getTagAtPath(getNBTPath(), storedTags); - - // Found righ - if (tg != null) { - - // Thats it - return new DoubleData(SilentNumbers.round((Double) tg.getValue(), 4)); - } - - // Fail - return null; - } - - @Override - public void whenClicked(@NotNull EditionInventory inv, @NotNull InventoryClickEvent event) { - if (event.getAction() == InventoryAction.PICKUP_HALF) { - inv.getEditedSection().set(getPath(), null); - inv.registerTemplateEdition(); - inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + "Successfully removed " + getName() + ChatColor.GRAY + "."); - return; - } - new StatEdition(inv, this).enable("Write in the chat the numeric value you want.", - "Second Format: {Base} {Scaling Value} {Spread} {Max Spread}", "Third Format: {Min Value} -> {Max Value}"); - } - - @Override - public void whenInput(@NotNull EditionInventory inv, @NotNull String message, Object... info) { - double base, scale, spread, maxSpread; - - /* - * Supports the old RANGE formula with a minimum and a maximum value and - * automatically makes the conversion to the newest system. This way - * users can keep using the old system if they don't want to adapt to - * the complex gaussian stat calculation - */ - if (message.contains("->")) { - String[] split = message.replace(" ", "").split(Pattern.quote("->")); - Validate.isTrue(split.length > 1, "You must specify two (both min and max) values"); - - double min = Double.parseDouble(split[0]), max = Double.parseDouble(split[1]); - Validate.isTrue(max > min, "Max value must be greater than min value"); - - base = MMOUtils.truncation(min == -max ? (max - min) * .05 : (min + max) / 2, 3); - scale = 0; // No scale - maxSpread = MMOUtils.truncation((max - min) / (2 * base), 3); - spread = MMOUtils.truncation(.8 * maxSpread, 3); - } - - // Newest system with gaussian values calculation - else { - String[] split = message.split(" "); - base = MMOUtils.parseDouble(split[0]); - scale = split.length > 1 ? MMOUtils.parseDouble(split[1]) : 0; - spread = split.length > 2 ? MMOUtils.parseDouble(split[2]) : 0; - maxSpread = split.length > 3 ? MMOUtils.parseDouble(split[3]) : 0; - } - - // Save as a flat formula - if (scale == 0 && spread == 0 && maxSpread == 0) - inv.getEditedSection().set(getPath(), base); - - else { - inv.getEditedSection().set(getPath() + ".base", base); - inv.getEditedSection().set(getPath() + ".scale", scale == 0 ? null : scale); - inv.getEditedSection().set(getPath() + ".spread", spread == 0 ? null : spread); - inv.getEditedSection().set(getPath() + ".max-spread", maxSpread == 0 ? null : maxSpread); - } - - inv.registerTemplateEdition(); - inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + getName() + " successfully changed to {" + base + " - " + scale + " - " + spread - + " - " + maxSpread + "}"); - } - - @Override - public void whenDisplayed(List lore, Optional statData) { - if (statData.isPresent()) { - NumericStatFormula data = statData.get(); - lore.add(ChatColor.GRAY + "Base Value: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getBase()) - + (data.getScale() != 0 ? ChatColor.GRAY + " (+" + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getScale()) + ChatColor.GRAY + ")" : "")); - if (data.getSpread() > 0) - lore.add(ChatColor.GRAY + "Spread: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getSpread() * 100) + "%" + ChatColor.GRAY + " (Max: " - + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMaxSpread() * 100) + "%" + ChatColor.GRAY + ")"); - - } else - lore.add(ChatColor.GRAY + "Current Value: " + ChatColor.GREEN + "---"); - - lore.add(""); - lore.add(ChatColor.YELLOW + AltChar.listDash + " Left click to change this value."); - lore.add(ChatColor.YELLOW + AltChar.listDash + " Right click to remove this value."); - } - - @Override - @NotNull public DoubleData getClearStatData() { - return new DoubleData(0D); - } - - @NotNull - @Override - public UpgradeInfo loadUpgradeInfo(@Nullable Object obj) throws IllegalArgumentException { - - // Return result of thay - return DoubleUpgradeInfo.GetFrom(obj); - } - - @NotNull - @Override - public StatData apply(@NotNull StatData original, @NotNull UpgradeInfo info, int level) { - - // Must be DoubleData - int i = level; - if (original instanceof DoubleData && info instanceof DoubleUpgradeInfo) { - - // Get value - double value = ((DoubleData) original).getValue(); - - // If leveling up - if (i > 0) { - - // While still positive - while (i > 0) { - - // Apply PMP Operation Positively - value = ((DoubleUpgradeInfo) info).getPMP().apply(value); - - // Decrease - i--; - } - - // Degrading the item - } else if (i < 0) { - - // While still negative - while (i < 0) { - - // Apply PMP Operation Reversibly - value = ((DoubleUpgradeInfo) info).getPMP().reverse(value); - - // Decrease - i++; - } - } - - // Update - ((DoubleData) original).setValue(value); - } - - // Upgraded - return original; - } - - public static class DoubleUpgradeInfo implements UpgradeInfo { - @NotNull PlusMinusPercent pmp; - - /** - * Generate a DoubleUpgradeInfo from this String - * that represents a {@link PlusMinusPercent}. - *

- * To keep older MMOItems versions working the same way, instead of having no prefix - * to use the set function of the PMP, one must use an s prefix. - * @param obj A String that encodes for a PMP. - * @throws IllegalArgumentException If any part of the operation goes wrong (including reading the PMP). - */ - @NotNull public static DoubleUpgradeInfo GetFrom(@Nullable Object obj) throws IllegalArgumentException { - - // Shall not be null - Validate.notNull(obj, FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation must not be null")); - - // Does the string exist? - String str = obj.toString(); - if (str.isEmpty()) { - throw new IllegalArgumentException( - FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation is empty")); - } - - // Adapt to PMP format - char c = str.charAt(0); if (c == 's') { str = str.substring(1); } else if (c != '+' && c != '-' && c != 'n') { str = '+' + str; } - - // Is it a valid plus minus percent? - FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get()); - PlusMinusPercent pmpRead = PlusMinusPercent.getFromString(str, ffp); - if (pmpRead == null) { - throw new IllegalArgumentException( - ffp.getFeedbackOf(FriendlyFeedbackCategory.ERROR).get(0).forConsole(ffp.getPalette())); - } - - // Success - return new DoubleUpgradeInfo(pmpRead); - } - - public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) { this.pmp = pmp; } - - /** - * The operation every level will perform. - * @see PlusMinusPercent - */ - @NotNull public PlusMinusPercent getPMP() { return pmp; } - } + /** + * @return If this stat supports negatives stat values + */ + public boolean handleNegativeStats() { + return true; + } + + /** + * @return For example knockback resistance, 0.01 = 1% so multiplies by 100 when displaying. + */ + public double multiplyWhenDisplaying() { + return 1; + } + + /** + * Usually, a greater magnitude of stat benefits the player (more health, more attack damage). + *

However, its not impossible for a stat to be evil instead, who knows? + */ + public boolean moreIsBetter() { + return moreIsBetter; + } + + @Override + public NumericStatFormula whenInitialized(Object object) { + + if (object instanceof Number) + return new NumericStatFormula(Double.parseDouble(object.toString()), 0, 0, 0); + + if (object instanceof ConfigurationSection) + return new NumericStatFormula(object); + + throw new IllegalArgumentException("Must specify a number or a config section"); + } + + @Override + public void whenApplied(@NotNull ItemStackBuilder item, @NotNull DoubleData data) { + + // Get Value + double value = data.getValue(); + + // Cancel if it its NEGATIVE and this doesn't support negative stats. + if (value < 0 && !handleNegativeStats()) { + return; + } + + // Identify the upgrade amount + double upgradeShift = 0; + + // Displaying upgrades? + if (UpgradeTemplate.isDisplayingUpgrades() && item.getMMOItem().getUpgradeLevel() != 0) { + + // Get stat history + StatHistory hist = item.getMMOItem().getStatHistory(this); + if (hist != null) { + + // Get as if it had never been upgraded + //HSY//MMOItems.log(" \u00a73-\u00a7a- \u00a77Stat Change Display Recalculation \u00a73-\u00a7a-\u00a73-"); + DoubleData uData = (DoubleData) hist.recalculateUnupgraded(); + + // Calculate Difference + upgradeShift = value - uData.getValue(); + } + } + + // Display in lore + if (value != 0 || upgradeShift != 0) { + String loreInsert = formatPath(getId(), getGeneralStatFormat(), moreIsBetter, value * multiplyWhenDisplaying()); + if (upgradeShift != 0) + loreInsert += UpgradeTemplate.getUpgradeChangeSuffix(plus(upgradeShift * multiplyWhenDisplaying()) + (MythicLib.plugin.getMMOConfig().decimals.format(upgradeShift * multiplyWhenDisplaying())), !isGood(upgradeShift * multiplyWhenDisplaying())); + item.getLore().insert(getPath(), loreInsert); + } + + /* + * Add NBT Data if it is not equal to ZERO, in which case it will just get removed. + * + * It is important that the tags are not excluded in getAppliedNBT() because the StatHistory does + * need that blanc tag information to remember when an Item did not initially have any of a stat. + */ + if (data.getValue() != 0) { + item.addItemTag(getAppliedNBT(data)); + } + } + + @NotNull + @Deprecated + public static String formatPath(@NotNull String format, boolean moreIsBetter, double value) { + return formatPath("ATTACK_DAMAGE", format, moreIsBetter, value); + } + + @NotNull + public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double value) { + final String valueFormatted = StatManager.format(stat, value); + final String colorPrefix = getColorPrefix(value < 0 && moreIsBetter); + return format + .replace("{value}", colorPrefix + (value > 0 ? "+" : "") + valueFormatted) // Replace conditional pluses with +value + .replace("{value}", colorPrefix + valueFormatted) // Replace loose pounds with the value + .replace("", (value > 0 ? "+" : "")); // Replace loose es + } + + @NotNull + @Deprecated + public static String formatPath(@NotNull String format, boolean moreIsBetter, double min, double max) { + return formatPath("ATTACK_DAMAGE", format, moreIsBetter, min, max); + } + + @NotNull + public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double min, double max) { + final String minFormatted = StatManager.format(stat, min), maxFormatted = StatManager.format(stat, max); + final String minPrefix = getColorPrefix(min < 0 && moreIsBetter), maxPrefix = getColorPrefix(max < 0 && moreIsBetter); + return format + .replace("", "") + .replace("{value}", + minPrefix + (min > 0 ? "+" : "") + minFormatted + + MMOItems.plugin.getConfig().getString("stats-displaying.range-dash", "⎓") + + maxPrefix + (min < 0 && max > 0 ? "+" : "") + maxFormatted); + } + + @Override + public void whenPreviewed(@NotNull ItemStackBuilder item, @NotNull DoubleData currentData, @NotNull NumericStatFormula templateData) throws IllegalArgumentException { + Validate.isTrue(currentData instanceof DoubleData, "Current Data is not Double Data"); + Validate.isTrue(templateData instanceof NumericStatFormula, "Template Data is not Numeric Stat Formula"); + + // Get Value + //SPRD//MMOItems.log("\u00a7c༺\u00a77 Calulating deviations of \u00a7b" + item.getMMOItem().getType().toString() + " " + item.getMMOItem().getId() + "\u00a77's \u00a7e" + getId()); + double techMinimum = templateData.calculate(0, -2.5); + double techMaximum = templateData.calculate(0, 2.5); + + // Cancel if it its NEGATIVE and this doesn't support negative stats. + if (techMaximum < 0 && !handleNegativeStats()) { + return; + } + if (techMinimum < 0 && !handleNegativeStats()) { + techMinimum = 0; + } + // Add NBT Path + item.addItemTag(getAppliedNBT(currentData)); + + // Display if not ZERO + if (techMinimum != 0 || techMaximum != 0) { + + String builtRange; + if (SilentNumbers.round(techMinimum, 2) == SilentNumbers.round(techMaximum, 2)) { + builtRange = formatPath(getId(), getGeneralStatFormat(), moreIsBetter(), techMaximum * multiplyWhenDisplaying()); + + } else { + builtRange = formatPath(getId(), getGeneralStatFormat(), moreIsBetter(), techMinimum * multiplyWhenDisplaying(), techMaximum * multiplyWhenDisplaying()); + } + + // Just display normally + item.getLore().insert(getPath(), builtRange); + } + } + + @NotNull + public static String getColorPrefix(boolean isNegative) { + + // Get the base + return Objects.requireNonNull(MMOItems.plugin.getConfig().getString("stats-displaying.color-" + (isNegative ? "negative" : "positive"), "")); + } + + @NotNull + String plus(double amount) { + if (amount >= 0) { + return "+"; + } else return ""; + } + + /** + * Usually, a greater magnitude of stat benefits the player (more health, more attack damage). + *

However, its not impossible for a stat to be evil instead, who knows? + *

+ * This will return true if: + *

> The amount is positive, and more benefits the player + *

> The amount is negative, and more hurts the player + */ + public boolean isGood(double amount) { + return moreIsBetter() ? amount >= 0 : amount <= 0; + } + + @Override + @NotNull + public ArrayList getAppliedNBT(@NotNull DoubleData data) { + + // Create Fresh + ArrayList ret = new ArrayList<>(); + + // Add sole tag + ret.add(new ItemTag(getNBTPath(), data.getValue())); + + // Return thay + return ret; + } + + @Override + public void whenLoaded(@NotNull ReadMMOItem mmoitem) { + + // Get tags + ArrayList relevantTags = new ArrayList<>(); + + // Add sole tag + if (mmoitem.getNBT().hasTag(getNBTPath())) + relevantTags.add(ItemTag.getTagAtPath(getNBTPath(), mmoitem.getNBT(), SupportedNBTTagValues.DOUBLE)); + + // Use that + DoubleData bakedData = getLoadedNBT(relevantTags); + + // Valid? + if (bakedData != null) { + + // Set + mmoitem.setData(this, bakedData); + } + } + + @Override + @Nullable + public DoubleData getLoadedNBT(@NotNull ArrayList storedTags) { + + // You got a double righ + ItemTag tg = ItemTag.getTagAtPath(getNBTPath(), storedTags); + + // Found righ + if (tg != null) { + + // Thats it + return new DoubleData(SilentNumbers.round((Double) tg.getValue(), 4)); + } + + // Fail + return null; + } + + @Override + public void whenClicked(@NotNull EditionInventory inv, @NotNull InventoryClickEvent event) { + if (event.getAction() == InventoryAction.PICKUP_HALF) { + inv.getEditedSection().set(getPath(), null); + inv.registerTemplateEdition(); + inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + "Successfully removed " + getName() + ChatColor.GRAY + "."); + return; + } + new StatEdition(inv, this).enable("Write in the chat the numeric value you want.", + "Second Format: {Base} {Scaling Value} {Spread} {Max Spread}", "Third Format: {Min Value} -> {Max Value}"); + } + + @Override + public void whenInput(@NotNull EditionInventory inv, @NotNull String message, Object... info) { + final NumericStatFormula formula = new NumericStatFormula(message); + formula.fillConfigurationSection(inv.getEditedSection(), getPath(), NumericStatFormula.FormulaSaveOption.DELETE_IF_ZERO); + inv.registerTemplateEdition(); + inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + getName() + " successfully changed to {" + + formula.getBase() + " - " + formula.getScale() + " - " + formula.getSpread() + " - " + formula.getMaxSpread() + " - (" + + formula.getMin() + " -> " + formula.getMax() + ") }"); + } + + @Override + public void whenDisplayed(List lore, Optional statData) { + if (statData.isPresent()) { + NumericStatFormula data = statData.get(); + if (data.isUniform()) { + lore.add(ChatColor.GRAY + "Uniform: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMin()) + ChatColor.GRAY + " -> " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMax())); + } else { + lore.add(ChatColor.GRAY + "Base Value: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getBase()) + + (data.getScale() != 0 ? ChatColor.GRAY + " (+" + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getScale()) + ChatColor.GRAY + "/Lvl)" : "")); + if (data.getSpread() > 0) + lore.add(ChatColor.GRAY + "Spread: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getSpread() * 100) + "%" + ChatColor.GRAY + " (Max: " + + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMaxSpread() * 100) + "%" + ChatColor.GRAY + ")"); + if (data.hasMin()) + lore.add(ChatColor.GRAY + "Min: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMin())); + if (data.hasMax()) + lore.add(ChatColor.GRAY + "Max: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMax())); + } + } else + lore.add(ChatColor.GRAY + "Current Value: " + ChatColor.GREEN + "---"); + + lore.add(""); + lore.add(ChatColor.YELLOW + AltChar.listDash + " Left click to change this value."); + lore.add(ChatColor.YELLOW + AltChar.listDash + " Right click to remove this value."); + } + + @Override + @NotNull + public DoubleData getClearStatData() { + return new DoubleData(0D); + } + + @NotNull + @Override + public UpgradeInfo loadUpgradeInfo(@Nullable Object obj) throws IllegalArgumentException { + + // Return result of thay + return DoubleUpgradeInfo.GetFrom(obj); + } + + @NotNull + @Override + public StatData apply(@NotNull StatData original, @NotNull UpgradeInfo info, int level) { + + // Must be DoubleData + int i = level; + if (original instanceof DoubleData && info instanceof DoubleUpgradeInfo) { + + // Get value + double value = ((DoubleData) original).getValue(); + + // If leveling up + if (i > 0) { + + // While still positive + while (i > 0) { + + // Apply PMP Operation Positively + value = ((DoubleUpgradeInfo) info).getPMP().apply(value); + + // Decrease + i--; + } + + // Degrading the item + } else if (i < 0) { + + // While still negative + while (i < 0) { + + // Apply PMP Operation Reversibly + value = ((DoubleUpgradeInfo) info).getPMP().reverse(value); + + // Decrease + i++; + } + } + + // Update + ((DoubleData) original).setValue(value); + } + + // Upgraded + return original; + } + + public static class DoubleUpgradeInfo implements UpgradeInfo { + @NotNull + PlusMinusPercent pmp; + + /** + * Generate a DoubleUpgradeInfo from this String + * that represents a {@link PlusMinusPercent}. + *

+ * To keep older MMOItems versions working the same way, instead of having no prefix + * to use the set function of the PMP, one must use an s prefix. + * + * @param obj A String that encodes for a PMP. + * @throws IllegalArgumentException If any part of the operation goes wrong (including reading the PMP). + */ + @NotNull + public static DoubleUpgradeInfo GetFrom(@Nullable Object obj) throws IllegalArgumentException { + + // Shall not be null + Validate.notNull(obj, FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation must not be null")); + + // Does the string exist? + String str = obj.toString(); + if (str.isEmpty()) { + throw new IllegalArgumentException( + FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation is empty")); + } + + // Adapt to PMP format + char c = str.charAt(0); + if (c == 's') { + str = str.substring(1); + } else if (c != '+' && c != '-' && c != 'n') { + str = '+' + str; + } + + // Is it a valid plus minus percent? + FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get()); + PlusMinusPercent pmpRead = PlusMinusPercent.getFromString(str, ffp); + if (pmpRead == null) { + throw new IllegalArgumentException( + ffp.getFeedbackOf(FriendlyFeedbackCategory.ERROR).get(0).forConsole(ffp.getPalette())); + } + + // Success + return new DoubleUpgradeInfo(pmpRead); + } + + public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) { + this.pmp = pmp; + } + + /** + * The operation every level will perform. + * + * @see PlusMinusPercent + */ + @NotNull + public PlusMinusPercent getPMP() { + return pmp; + } + } }