Added ranges to numerical stats randomization

This commit is contained in:
Jules 2023-10-14 23:34:09 +02:00
parent 56359e0b61
commit 4a38952310
2 changed files with 717 additions and 597 deletions

View File

@ -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,21 +20,24 @@ import java.util.Random;
* @author indyuce
*/
public class NumericStatFormula implements RandomStatData<DoubleData>, UpdatableRandomStatData<DoubleData> {
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.####");
public static final NumericStatFormula ZERO = new NumericStatFormula(0, 0, 0, 0);
private final static double DEFAULT_MAX_SPREAD = .3;
/**
* 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.
*
* <p>
* Throws an IAE either if the format is not good or if the object does not
* have the right type
* have the right type.
*
* @param object Object to read data from.
*/
@ -41,41 +45,91 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
Validate.notNull(object, "Config must not be null");
if (object instanceof String) {
// 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]);
}
// 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;
return;
hasMin = split.length > 4;
hasMax = split.length > 5;
min = hasMin ? Double.parseDouble(split[4]) : 0;
max = hasMax ? Double.parseDouble(split[5]) : 0;
uniform = false;
}
}
if (object instanceof Number) {
// Constant
else if (object instanceof Number) {
base = Double.parseDouble(object.toString());
scale = 0;
spread = 0;
maxSpread = 0;
return;
uniform = false;
hasMin = false;
hasMax = false;
min = 0;
max = 0;
}
if (object instanceof ConfigurationSection) {
// 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", .3);
Validate.isTrue(spread >= 0, "Spread must be positive");
Validate.isTrue(maxSpread >= 0, "Max spread must be positive");
return;
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");
}
// 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");
}
/**
* Formula for numeric statistics. These formulas allow stats to 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);
}
/**
* 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);
}
/**
* Formula for numerical statistics. These formulas allow stats to scale
* accordingly to the item level but also have a more or less important
* gaussian based random factor
* gaussian/uniform based random factor.
*
* @param base Base value
* @param scale Value which scales with the item level
@ -86,35 +140,52 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
* @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) {
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;
}
/**
* @return The 'Base' number of the item. This chooses the peak of the Gaussian Distribution.
* @see #getScale()
*/
public double getBase() { return base; }
public double getBase() {
return base;
}
/**
* @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; }
public double getScale() {
return scale;
}
/**
* @return Standard Deviation of the Gaussian Distribution
*/
public double getSpread() { return spread; }
public double getSpread() {
return spread;
}
/**
* @return For gaussian distributions, there always is that INSANELY SMALL chance of getting an INSANELY LARGE number.
* @return For gaussian distributions, there always is that INSANELY SMALL
* chance of getting an INSANELY LARGE number.
* <p>
* For example: At base atk dmg 10, and standard deviation 1:
* <p>68% of rolls will fall between 9 and 11;
* </p>95% of rolls will fall between 8 and 12;
@ -123,9 +194,31 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
* <p></p>
* Whatever, this constrains to a minimum and maximum of output.
*/
public double getMaxSpread() { return maxSpread; }
public double getMaxSpread() {
return maxSpread;
}
public static boolean useRelativeSpread;
public boolean isUniform() {
return uniform;
}
public boolean hasMin() {
return hasMin;
}
public boolean hasMax() {
return hasMax;
}
public double getMin() {
return min;
}
public double getMax() {
return max;
}
public static boolean RELATIVE_SPREAD;
/**
* Applies the formula for a given input x.
@ -133,7 +226,6 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
* @param levelScalingFactor When choosing the mean of the distribution,
* the formula is <code>base + (scale*level)</code>.
* This is the <code>level</code>
*
* @return <b>Legacy formula: ???</b><br>
* Let A = {base} + {scale} * lvl, then the returned value is a
* random value taken in respect to a gaussian distribution
@ -148,19 +240,25 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
public double calculate(double levelScalingFactor) {
// Calculate yes
return calculate(levelScalingFactor, RANDOM.nextGaussian());
return calculate(levelScalingFactor, uniform ? RANDOM.nextDouble() : RANDOM.nextGaussian());
}
/**
* @param levelScalingFactor Level to scale the scale with
* @param scaleFactor Level to scale the scale with
* @param random Result of <code>RANDOM.nextGaussian()</code> or whatever other
* value that you actually want to pass.
* @return The calculated value
* 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 levelScalingFactor, double random) {
public double calculate(double scaleFactor, double random) {
double value;
if (uniform) {
value = min + (max - min) * random;
} else {
// The mean, the center of the distribution
final double actualBase = base + (scale * levelScalingFactor);
final double actualBase = base + (scale * scaleFactor);
/*
* This is one pick from a gaussian distribution at mean 0, and
@ -169,7 +267,12 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
*/
final double spreadCoef = Math.min(Math.max(random * spread, -maxSpread), maxSpread);
return useRelativeSpread ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef;
value = RELATIVE_SPREAD ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef;
if (hasMin) value = Math.max(min, value);
if (hasMax) value = Math.max(max, value);
}
return value;
}
@Override
@ -179,24 +282,33 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
/**
* 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)
* 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(ConfigurationSection config, String path, FormulaSaveOption option) {
public void fillConfigurationSection(@NotNull ConfigurationSection config, @NotNull String path, @NotNull FormulaSaveOption option) {
if (path == null)
throw new NullPointerException("Path was empty");
throw new NullPointerException("Path is empty");
if (scale == 0 && spread == 0 && maxSpread == 0)
if (scale == 0 && spread == 0 && maxSpread == 0 && !uniform) {
config.set(path, base == 0 && option == FormulaSaveOption.DELETE_IF_ZERO ? null : base);
else {
config.set(path + ".base", base);
config.set(path + ".scale", scale);
config.set(path + ".spread", spread);
config.set(path + ".max-spread", maxSpread);
return;
}
config.createSection(path);
config = config.getConfigurationSection(path);
if (!uniform) {
config.set("base", base);
config.set("scale", scale);
config.set("spread", spread);
config.set("max-spread", maxSpread);
}
if (hasMin) config.set("min", min);
if (hasMax) config.set("max", max);
}
public void fillConfigurationSection(ConfigurationSection config, String path) {
@ -217,7 +329,9 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
+ (maxSpread != 0 ? ",Max=" + maxSpread : "") + "}";
}
public static void reload() { useRelativeSpread = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false); }
public static void reload() {
RELATIVE_SPREAD = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false);
}
@NotNull
@SuppressWarnings("unchecked")
@ -228,7 +342,7 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
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 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
@ -242,7 +356,8 @@ public class NumericStatFormula implements RandomStatData<DoubleData>, Updatable
//UPGRD//MMOItems.log("\u00a7a +\u00a77 Acceptable Range --- kept");
// Just clone I guess
return original.cloneData(); }
return original.cloneData();
}
}
public enum FormulaSaveOption {

View File

@ -75,7 +75,9 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
* Usually, a greater magnitude of stat benefits the player (more health, more attack damage).
* <p>However, its not impossible for a stat to be evil instead, who knows?
*/
public boolean moreIsBetter() { return moreIsBetter; }
public boolean moreIsBetter() {
return moreIsBetter;
}
@Override
public NumericStatFormula whenInitialized(Object object) {
@ -96,7 +98,9 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
double value = data.getValue();
// Cancel if it its NEGATIVE and this doesn't support negative stats.
if (value < 0 && !handleNegativeStats()) { return; }
if (value < 0 && !handleNegativeStats()) {
return;
}
// Identify the upgrade amount
double upgradeShift = 0;
@ -113,11 +117,13 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
DoubleData uData = (DoubleData) hist.recalculateUnupgraded();
// Calculate Difference
upgradeShift = value - uData.getValue(); } }
upgradeShift = value - uData.getValue();
}
}
// Display in lore
if (value != 0 || upgradeShift != 0) {
String loreInsert = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter, value * multiplyWhenDisplaying());
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);
@ -129,7 +135,9 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
* 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)); }
if (data.getValue() != 0) {
item.addItemTag(getAppliedNBT(data));
}
}
@NotNull
@ -177,8 +185,12 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
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; }
if (techMaximum < 0 && !handleNegativeStats()) {
return;
}
if (techMinimum < 0 && !handleNegativeStats()) {
techMinimum = 0;
}
// Add NBT Path
item.addItemTag(getAppliedNBT(currentData));
@ -187,22 +199,30 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
String builtRange;
if (SilentNumbers.round(techMinimum, 2) == SilentNumbers.round(techMaximum, 2)) {
builtRange = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter(), techMaximum * multiplyWhenDisplaying());
builtRange = formatPath(getId(), getGeneralStatFormat(), 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); }
builtRange = formatPath(getId(), getGeneralStatFormat(), moreIsBetter(), techMinimum * multiplyWhenDisplaying(), techMaximum * multiplyWhenDisplaying());
}
@NotNull public static String getColorPrefix(boolean isNegative) {
// 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 ""; }
@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).
@ -241,7 +261,7 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
relevantTags.add(ItemTag.getTagAtPath(getNBTPath(), mmoitem.getNBT(), SupportedNBTTagValues.DOUBLE));
// Use that
DoubleData bakedData = (DoubleData) getLoadedNBT(relevantTags);
DoubleData bakedData = getLoadedNBT(relevantTags);
// Valid?
if (bakedData != null) {
@ -250,6 +270,7 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
mmoitem.setData(this, bakedData);
}
}
@Override
@Nullable
public DoubleData getLoadedNBT(@NotNull ArrayList<ItemTag> storedTags) {
@ -282,62 +303,31 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
@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);
}
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 {" + base + " - " + scale + " - " + spread
+ " - " + maxSpread + "}");
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<String> lore, Optional<NumericStatFormula> 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 + ")" : ""));
+ (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 + "---");
@ -347,7 +337,8 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
}
@Override
@NotNull public DoubleData getClearStatData() {
@NotNull
public DoubleData getClearStatData() {
return new DoubleData(0D);
}
@ -406,7 +397,8 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
}
public static class DoubleUpgradeInfo implements UpgradeInfo {
@NotNull PlusMinusPercent pmp;
@NotNull
PlusMinusPercent pmp;
/**
* Generate a <code>DoubleUpgradeInfo</code> from this <code><b>String</b></code>
@ -414,10 +406,12 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
* <p></p>
* To keep older MMOItems versions working the same way, instead of having no prefix
* to use the <i>set</i> function of the PMP, one must use an <b><code>s</code></b> prefix.
*
* @param obj A <code><u>String</u></code> 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 {
@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"));
@ -430,7 +424,12 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
}
// Adapt to PMP format
char c = str.charAt(0); if (c == 's') { str = str.substring(1); } else if (c != '+' && c != '-' && c != 'n') { str = '+' + str; }
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());
@ -444,12 +443,18 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
return new DoubleUpgradeInfo(pmpRead);
}
public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) { this.pmp = pmp; }
public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) {
this.pmp = pmp;
}
/**
* The operation every level will perform.
*
* @see PlusMinusPercent
*/
@NotNull public PlusMinusPercent getPMP() { return pmp; }
@NotNull
public PlusMinusPercent getPMP() {
return pmp;
}
}
}