diff --git a/pom.xml b/pom.xml index f19d5a6..6a952f7 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,12 @@ + + junit + junit + 4.13.2 + test + local gson diff --git a/src/main/java/net/tfminecraft/advancedcrafting/cache/Cache.java b/src/main/java/net/tfminecraft/advancedcrafting/cache/Cache.java index ca81af3..6444fe5 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/cache/Cache.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/cache/Cache.java @@ -18,6 +18,8 @@ public class Cache { public static double alloyForgeBaseSuccess = 2.0; public static double alloyForgeBonusPerSqrtValue = 4.0; public static double alloyForgeMaxSuccess = 85.0; + public static double gemstoneStatBaseChance = 55.0; + public static double gemstoneStatBonusPerValue = 5.0; /** Flat success% added when the ingredient (lowercase id) is the alloy base. Catalysts never get it. */ public static Map alloyForgeBaseBonus = new HashMap<>(); diff --git a/src/main/java/net/tfminecraft/advancedcrafting/loaders/ConfigLoader.java b/src/main/java/net/tfminecraft/advancedcrafting/loaders/ConfigLoader.java index 1fbd356..1ea3c26 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/loaders/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/loaders/ConfigLoader.java @@ -37,6 +37,8 @@ public void load(File configFile) { Cache.alloyForgeBaseSuccess = clampPercent(config.getDouble("alloy-forge.base-success-percent", 2.0)); Cache.alloyForgeBonusPerSqrtValue = Math.max(0.0, config.getDouble("alloy-forge.success-bonus-per-sqrt-value", 4.0)); Cache.alloyForgeMaxSuccess = clampPercent(config.getDouble("alloy-forge.max-success-percent", 85.0)); + Cache.gemstoneStatBaseChance = clampPercent(config.getDouble("alloy-forge.gem-stat-base-percent", 55.0)); + Cache.gemstoneStatBonusPerValue = Math.max(0.0, config.getDouble("alloy-forge.gem-stat-bonus-per-value", 5.0)); if (Cache.alloyForgeMaxSuccess < Cache.alloyForgeBaseSuccess) { Cache.alloyForgeMaxSuccess = Cache.alloyForgeBaseSuccess; } diff --git a/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java b/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java index cad22c0..db04763 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForger.java @@ -24,6 +24,7 @@ import net.tfminecraft.advancedcrafting.lifecycle.CraftLifecycle; public class AlloyForger { + private static final String GEMSTONE_PATH_PREFIX = "m.gemstones."; private int value; private AlloyStation station; private StatData stats; @@ -181,11 +182,23 @@ public void mergeStats() { } List merge = new ArrayList<>(); for(Ingredient i : station.getCatalysts()) { + // Gemstones span metal and crystal ingredient types; their item paths identify them consistently. + boolean gemstone = isGemstonePath(i.getPath()); for(StatModifier m : i.getIngredientData().getStatData().getModifiers()) { if(baseItem.getIngredientData().statIsProtected(m)) continue; - if(base.containsKey(m.getType())) { - StatModifier n = new StatModifier(m.getType(), m.getAmount()-base.get(m.getType()).getAmount()); - merge.add(n); + StatModifier baseModifier = base.get(m.getType()); + if(gemstone) { + if(Math.random()*100 >= gemstoneInheritanceChance(i.getIngredientData().getValue())) continue; + merge.add(modifierForMerge(m, baseModifier, true)); + if(baseModifier == null) { + StatModifier limit = m.copy(); + limit.setAmount(limit.getAmount()*Cache.maxFactor); + // A later, weaker gem with the same stat must not lower an earlier gem's cap. + max.merge(m.getType(), limit, (current, candidate) -> + current.getAmount() >= candidate.getAmount() ? current : candidate); + } + } else if(baseModifier != null) { + merge.add(modifierForMerge(m, baseModifier, false)); } else { if(Math.random()*100 < (20+i.getIngredientData().getValue()*3)) { merge.add(m.copy()); @@ -213,6 +226,20 @@ public void mergeStats() { } } } + + static boolean isGemstonePath(String path) { + return path != null && path.regionMatches(true, 0, GEMSTONE_PATH_PREFIX, 0, GEMSTONE_PATH_PREFIX.length()); + } + + static double gemstoneInheritanceChance(int ingredientValue) { + return Math.max(0.0, Math.min(100.0, + Cache.gemstoneStatBaseChance + Cache.gemstoneStatBonusPerValue * ingredientValue)); + } + + static StatModifier modifierForMerge(StatModifier catalyst, StatModifier base, boolean gemstone) { + if(gemstone || base == null) return catalyst.copy(); + return new StatModifier(catalyst.getType(), catalyst.getAmount() - base.getAmount()); + } private void merge(HashMap base, HashMap max, List list) { for(StatModifier m : list) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ba6b51c..6e96fe2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,6 +11,9 @@ alloy-forge: base-success-percent: 2.0 success-bonus-per-sqrt-value: 4.0 max-success-percent: 85.0 + # Gemstone catalyst stat roll, before the total-value merge roll. Defaults also apply to existing configs without these keys. + gem-stat-base-percent: 55.0 + gem-stat-bonus-per-value: 5.0 # Flat success% added when this ingredient is the base. Catalysts never get it; unlisted ingredients get 0. base-bonus-percent: steel_ingot: 8.0 diff --git a/src/test/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForgerTest.java b/src/test/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForgerTest.java new file mode 100644 index 0000000..90c9a6c --- /dev/null +++ b/src/test/java/net/tfminecraft/advancedcrafting/objects/alloys/AlloyForgerTest.java @@ -0,0 +1,64 @@ +package net.tfminecraft.advancedcrafting.objects.alloys; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.Test; + +import net.tfminecraft.advancedcrafting.objects.stats.StatModifier; + +public class AlloyForgerTest { + @Test + public void everyConfiguredGemstoneUsesTheGemstoneRule() throws IOException { + long gemstoneCount = 0; + for(String line : Files.readAllLines(Path.of("src/main/resources/ingredients.yml"))) { + String trimmed = line.trim(); + if(!trimmed.startsWith("path:")) continue; + String path = trimmed.substring("path:".length()).trim(); + if(path.startsWith("m.gemstones.")) { + assertTrue(AlloyForger.isGemstonePath(path)); + gemstoneCount++; + } else { + assertFalse(AlloyForger.isGemstonePath(path)); + } + } + assertEquals(40, gemstoneCount); + assertFalse(AlloyForger.isGemstonePath(null)); + assertFalse(AlloyForger.isGemstonePath("m.gemstones_fake.ruby")); + } + + @Test + public void gemstoneChanceUsesTheConfiguredIngredientValue() { + assertEquals(60.0, AlloyForger.gemstoneInheritanceChance(1), 0.0); + assertEquals(65.0, AlloyForger.gemstoneInheritanceChance(2), 0.0); + assertEquals(70.0, AlloyForger.gemstoneInheritanceChance(3), 0.0); + assertEquals(75.0, AlloyForger.gemstoneInheritanceChance(4), 0.0); + assertEquals(100.0, AlloyForger.gemstoneInheritanceChance(10), 0.0); + } + + @Test + public void gemstoneWithMatchingStatAddsItsOwnPositiveValue() { + StatModifier steelArmor = new StatModifier("armor", 0.5); + StatModifier jasperArmor = new StatModifier("armor", 0.05); + StatModifier gemContribution = AlloyForger.modifierForMerge(jasperArmor, steelArmor, true); + + assertEquals(0.05, gemContribution.getAmount(), 0.0); + assertNotSame(jasperArmor, gemContribution); + assertEquals(0.5, steelArmor.getAmount(), 0.0); + assertEquals(-0.45, AlloyForger.modifierForMerge(jasperArmor, steelArmor, false).getAmount(), 0.000001); + } + + @Test + public void gemstoneWithAnyOtherMatchingStatAlsoAddsItsOwnValue() { + StatModifier baseHealth = new StatModifier("max_health", 2.0); + StatModifier gemHealth = new StatModifier("max_health", 0.5); + assertEquals(0.5, AlloyForger.modifierForMerge(gemHealth, baseHealth, true).getAmount(), 0.0); + assertEquals(0.5, AlloyForger.modifierForMerge(gemHealth, null, true).getAmount(), 0.0); + } +}