From fb38e5897596226f3c1bb5889fe37e016ed559e0 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:31:02 +0200 Subject: [PATCH 1/3] fix: keep config precision for stats with a global offset movement_speed has a global offset of 100. applyScaledValue divided by the offset first and rounded the result to 2 decimals, so once the divided value reached 0.01 (1.0 in config units) the item lost every decimal: a greataxe at 1.4 movement was stored as 1, and every shield (-1.0 to -1.3) as -1. Values below 1.0 rounded to 3 decimals and lost the second config decimal. The factor is now applied and rounded in config units, and the result is divided by the offset without rounding again. The preview shows that config value directly. Stats without an offset are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../utils/StatTemplateMath.java | 28 ++++++++-------- .../advancedcrafting/MathCoverageTest.java | 32 +++++++++++++++++-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java b/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java index ea319de..87060f9 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) { diff --git a/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java index 1ff55f7..5083f14 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java @@ -52,18 +52,44 @@ 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)); + } + @Test void bucketAveragesCountMissingStatsAsZeroAndSumSeparateBuckets() throws Exception { ingredient("iron", "stats: ['armor(6)', 'speed(2)']"); From c0f41001809356b0cec528a83ac64e7fe0723724 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:31:31 +0200 Subject: [PATCH 2/3] Give the default greataxe the battleaxe-plus-shortsword movement Two-handed greataxes can't hold an off-hand shortsword, so their movement is now (0.1 + leather/wool) x 2: the same as a battleaxe plus a shortsword (0.2 base) made with the same leather and wool. Needs the offset rounding fix in the previous commit to keep the decimals. Co-Authored-By: Claude Opus 5.5 --- src/main/resources/stats.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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" From a9d03b123b3706ab77918c53c4203ab33067d2d4 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:55:25 +0200 Subject: [PATCH 3/3] fix: round stat values half-up on their decimal digits round() built its BigDecimal from the binary double, so 1.005 (stored as 1.00499...) rounded to 1.00 instead of 1.01. BigDecimal.valueOf uses the decimal form, which matters now that offset stats round in config units. Co-Authored-By: Claude Opus 5.5 --- .../tfminecraft/advancedcrafting/utils/StatTemplateMath.java | 2 +- .../net/tfminecraft/advancedcrafting/MathCoverageTest.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java b/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java index 87060f9..391bead 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/utils/StatTemplateMath.java @@ -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/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java index 5083f14..5fcb92d 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/MathCoverageTest.java @@ -88,6 +88,9 @@ void offsetStatsKeepTheirConfigPrecisionOnTheItem() throws Exception { // 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