diff --git a/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java b/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java index ea319de..391bead 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java @@ -1,6 +1,7 @@ package net.tfminecraft.advancedcrafting.utils; import java.math.BigDecimal; +import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashSet; @@ -27,26 +28,25 @@ public static double getFactor(StatTemplate template, String statId) { return template.getFactor(statId); } - public static double applyGlobalOffset(double raw, String statId) { + /** + * Value written to the item. A stat with a global offset (movement_speed: 100) is rounded in + * config units and then divided, so the item keeps the precision the config shows. Rounding + * after the division kept 2 decimals of the divided value, which turned 1.4 into 1. + */ + public static double applyScaledValue(double raw, String statId, StatTemplate template) { + double scaled = applyScaledValueForPreview(raw, statId, template); Double offset = Cache.globalStatOffsets.get(statId.toLowerCase()); if (offset == null || offset == 0) { - return raw; + return scaled; } - return round(raw / offset); - } - - public static double applyScaledValue(double raw, String statId, StatTemplate template) { - return applyFactor(applyGlobalOffset(raw, statId), getFactor(template, statId)); + return BigDecimal.valueOf(scaled) + .divide(BigDecimal.valueOf(offset), MathContext.DECIMAL64) + .doubleValue(); } - /** Config-scale value for converter preview (reverses global offset division). */ + /** Config-scale value for converter preview: the factor applied, before any global offset. */ public static double applyScaledValueForPreview(double raw, String statId, StatTemplate template) { - double scaled = applyScaledValue(raw, statId, template); - Double offset = Cache.globalStatOffsets.get(statId.toLowerCase()); - if (offset == null || offset == 0) { - return scaled; - } - return round(scaled * offset); + return applyFactor(raw, getFactor(template, statId)); } public static double applyFactor(double raw, double factor) { @@ -54,7 +54,7 @@ public static double applyFactor(double raw, double factor) { } private static double round(double value) { - BigDecimal bd = new BigDecimal(value); + BigDecimal bd = BigDecimal.valueOf(value); // decimal digits, so 1.005 rounds half-up to 1.01 double abs = Math.abs(value); if (abs >= 0.01 || abs == 0) { return bd.setScale(2, RoundingMode.HALF_UP).doubleValue(); diff --git a/src/main/resources/stats.yml b/src/main/resources/stats.yml index 12d3a2a..5882696 100644 --- a/src/main/resources/stats.yml +++ b/src/main/resources/stats.yml @@ -153,7 +153,7 @@ greataxe: base-stats: - attack_speed(1.2) - blunt_power(3) - - movement_speed(0.6) + - movement_speed(0.1) stats: - attack_damage - attack_speed @@ -165,6 +165,11 @@ greataxe: - lifesteal - movement_speed - max_item_damage + # Two-handed, so it never has an off-hand shortsword: movement is (0.1 + leather/wool) x 2, + # what a battleaxe plus an off-hand shortsword with a 0.2 movement base give with the same + # leather and wool. + factors: + movement_speed: 2.0 light_armor: name: "§fLight Armor" diff --git a/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java index 1ff55f7..5fcb92d 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java @@ -52,18 +52,47 @@ void templateScalingFiltersAddsBaseStatsAndRendersPreview() throws Exception { assertTrue(StatTemplateMath.formatPercentSuffix(1.001).contains("0%")); assertEquals(.005, StatTemplateMath.applyFactor(.01, .5)); assertEquals(0, StatTemplateMath.applyFactor(0, 3)); - assertEquals(2, StatTemplateMath.applyGlobalOffset(2, "missing")); + assertEquals(2, StatTemplateMath.applyScaledValue(2, "missing", template)); Cache.globalStatOffsets.put("attack", 0.); - assertEquals(2, StatTemplateMath.applyGlobalOffset(2, "attack")); + assertEquals(3, StatTemplateMath.applyScaledValue(2, "attack", template)); assertEquals(3, StatTemplateMath.applyScaledValueForPreview(2, "attack", template)); Cache.globalStatOffsets.put("attack", 100.); - assertEquals(.02, StatTemplateMath.applyGlobalOffset(2, "ATTACK")); + assertEquals(.03, StatTemplateMath.applyScaledValue(2, "ATTACK", template)); assertEquals(3, StatTemplateMath.applyScaledValueForPreview(2, "attack", template)); assertEquals(0, StatTemplateMath.getFactor(template, "attack") - 1.5); assertNotNull(new StatTemplate("invalid", yaml("icon: bad")).getName()); assertNotNull(new StatTemplate("default", yaml("")).getIcon()); } + @Test + void offsetStatsKeepTheirConfigPrecisionOnTheItem() throws Exception { + Cache.globalStatOffsets.put("movement_speed", 100.); + var axe = + new StatTemplate( + "greataxe", + yaml( + "stats: [movement_speed]\n" + + "factors:\n" + + " movement_speed: 2.0\n" + + "base-stats: ['movement_speed(0.1)']")); + var shield = + new StatTemplate( + "shield", yaml("stats: [movement_speed]\nbase-stats: ['movement_speed(-1.6)']")); + var leather = new StatData(List.of("movement_speed(0.6)")); + var mod = new StatModifier("movement_speed", 0); + // (0.1 + 0.6) x 2 = 1.4 movement; rounding after the division used to store 0.01 (1.0) + assertEquals(.014, StatTemplateMath.filterAndApply(leather, axe).getAmount(mod)); + assertEquals(-.01, StatTemplateMath.filterAndApply(leather, shield).getAmount(mod)); + var wool = new StatData(List.of("movement_speed(0.3)")); + assertEquals(-.013, StatTemplateMath.filterAndApply(wool, shield).getAmount(mod)); + // Below 1.0 the old 3-decimal rounding also lost the second config decimal (0.65 -> 0.7) + assertEquals(.0065, StatTemplateMath.applyScaledValue(.65, "movement_speed", shield)); + assertEquals(1.4, StatTemplateMath.applyScaledValueForPreview(.7, "movement_speed", axe)); + // Half-up on the decimal digits: 1.005 is 1.00499... as a binary double + assertEquals(1.01, StatTemplateMath.applyFactor(1.005, 1)); + assertEquals(.0101, StatTemplateMath.applyScaledValue(1.005, "movement_speed", shield)); + } + @Test void bucketAveragesCountMissingStatsAsZeroAndSumSeparateBuckets() throws Exception { ingredient("iron", "stats: ['armor(6)', 'speed(2)']");