diff --git a/build.gradle b/build.gradle index abdf6f2..238640d 100644 --- a/build.gradle +++ b/build.gradle @@ -95,6 +95,7 @@ dependencies { minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}" //21 compileOnly(fg.deobf("curse.maven:quarkoddities-301051:3575623")) compileOnly(fg.deobf("curse.maven:quark-243121:3919164")) + compileOnly(fg.deobf("curse.maven:kintsugi-908206:5229196")) implementation(fg.deobf("curse.maven:cupboard-326652:4669193")) annotationProcessor "org.spongepowered:mixin:0.8.5:processor" } diff --git a/src/main/java/com/cursery/config/CommonConfiguration.java b/src/main/java/com/cursery/config/CommonConfiguration.java index 3720c85..bc39f80 100644 --- a/src/main/java/com/cursery/config/CommonConfiguration.java +++ b/src/main/java/com/cursery/config/CommonConfiguration.java @@ -18,13 +18,21 @@ public class CommonConfiguration implements ICommonConfig { - public List excludedCurses = new ArrayList<>(); - public boolean excludeTreasure = false; public boolean debugTries = false; public boolean showDesc = true; + + public List excludedCurses = new ArrayList<>(); + public boolean excludeTreasure = false; + + public boolean onlyUnEnchanted = false; public boolean visualSuccess = true; - public boolean onlynotechanted = false; - public int basecursechance = 5; + + public boolean curseChanceScales = true; + public int baseCurseChance = 5; + public int maxCurseChance = 75; + + public int curseEveryXLevels = 0; + public CommonConfiguration() { @@ -35,59 +43,81 @@ public JsonObject serialize() { final JsonObject root = new JsonObject(); - final JsonObject entry = new JsonObject(); - entry.addProperty("desc:", "Should enchanted books show a hint for curse magic, default:true"); - entry.addProperty("showDesc", showDesc); - root.add("showDesc", entry); + final JsonObject entry0 = new JsonObject(); + entry0.addProperty("desc:", "Whether to log debug messages about curse chances being rolled. Default: false"); + entry0.addProperty("debugTries", debugTries); + root.add("debugTries", entry0); final JsonObject entry1 = new JsonObject(); - entry1.addProperty("desc:", "Add a curse id here to exclude it from beeing applied. " - + "To put multiple values seperate them by commas like this: [\"minecraft:curse\", \"mod:curse;\"] "); + entry1.addProperty("desc:", "Should enchanted books show a hint for curse magic. Default: true"); + entry1.addProperty("showDesc", showDesc); + root.add("showDesc", entry1); + + final JsonObject entry2 = new JsonObject(); + entry2.addProperty("desc:", "Add a curse id here to exclude it from being applied. " + + "To put multiple values separate them by commas like this: [\"minecraft:curse\", \"mod:curse;\"] "); final JsonArray list1 = new JsonArray(); for (final String name : excludedCurses) { list1.add(name); } - entry1.add("excludedCurses", list1); - root.add("excludedCurses", entry1); - - final JsonObject entry2 = new JsonObject(); - entry2.addProperty("desc:", "Should applying treasure enchants be excluded, default:false"); - entry2.addProperty("excludeTreasure", excludeTreasure); - root.add("excludeTreasure", entry2); - - final JsonObject entry7 = new JsonObject(); - entry7.addProperty("desc:", "Should curses only be applied on enchanting unenchanted items, recommended to increase base chance when enabling, default:false"); - entry7.addProperty("onlynotechanted", onlynotechanted); - root.add("onlynotechanted", entry7); + entry2.add("excludedCurses", list1); + root.add("excludedCurses", entry2); final JsonObject entry3 = new JsonObject(); - entry3.addProperty("desc:", "Base curse application chance, scales up the more enchants the item has. Default:5 %"); - entry3.addProperty("basecursechance", basecursechance); - root.add("basecursechance", entry3); + entry3.addProperty("desc:", "Should applying treasure enchants be excluded. Default: false"); + entry3.addProperty("excludeTreasure", excludeTreasure); + root.add("excludeTreasure", entry3); final JsonObject entry4 = new JsonObject(); - entry4.addProperty("desc:", "Whether to log debug messages about curse chances beeing rolled, default = false"); - entry4.addProperty("debugTries", debugTries); - root.add("debugTries", entry4); + entry4.addProperty("desc:", "Should curses only be applied on enchanting unenchanted items, recommended to increase base chance when enabling. Default: false"); + entry4.addProperty("onlyUnEnchanted", onlyUnEnchanted); + root.add("onlyUnEnchanted", entry4); final JsonObject entry5 = new JsonObject(); - entry5.addProperty("desc:", "Should enchanting success play a sound and show particles, default:true"); + entry5.addProperty("desc:", "Should enchanting success play a sound and show particles. Default: true"); entry5.addProperty("visualSuccess", visualSuccess); root.add("visualSuccess", entry5); + final JsonObject entry6 = new JsonObject(); + entry6.addProperty("desc:", "Whether curse chance should scale the more enchantment levels an item has, " + + "If FALSE, curseChance = baseCurseChance - item enchantability. Default: true"); + entry6.addProperty("curseChanceScales", curseChanceScales); + root.add("curseChanceScales", entry6); + + final JsonObject entry7 = new JsonObject(); + entry7.addProperty("desc:", "Base curse application chance, varies with item enchantability " + + "(minCurseChance = base - enchantability). Default: 5 %"); + entry7.addProperty("baseCurseChance", baseCurseChance); + root.add("baseCurseChance", entry7); + + final JsonObject entry8 = new JsonObject(); + entry8.addProperty("desc:", "Maximum curse application chance, ignored if curseChanceScales is FALSE. Default: 75 %"); + entry8.addProperty("maxCurseChance", maxCurseChance); + root.add("maxCurseChance", entry8); + + final JsonObject entry9 = new JsonObject(); + entry9.addProperty("desc:", "Applies a curse every X enchantment levels, " + + "no other curses are applied by curseChance each time this occurs. Disabled if X = 0. Default: 0 "); + entry9.addProperty("curseEveryXLevels", curseEveryXLevels); + root.add("curseEveryXLevels", entry9); + + return root; } public void deserialize(JsonObject data) { + debugTries = data.get("debugTries").getAsJsonObject().get("debugTries").getAsBoolean(); showDesc = data.get("showDesc").getAsJsonObject().get("showDesc").getAsBoolean(); + excludedCurses = new ArrayList<>(); excludeTreasure = data.get("excludeTreasure").getAsJsonObject().get("excludeTreasure").getAsBoolean(); - debugTries = data.get("debugTries").getAsJsonObject().get("debugTries").getAsBoolean(); + onlyUnEnchanted = data.get("onlyUnEnchanted").getAsJsonObject().get("onlyUnEnchanted").getAsBoolean(); visualSuccess = data.get("visualSuccess").getAsJsonObject().get("visualSuccess").getAsBoolean(); - basecursechance = data.get("basecursechance").getAsJsonObject().get("basecursechance").getAsInt(); - onlynotechanted = data.get("onlynotechanted").getAsJsonObject().get("onlynotechanted").getAsBoolean(); - excludedCurses = new ArrayList<>(); + curseChanceScales = data.get("curseChanceScales").getAsJsonObject().get("curseChanceScales").getAsBoolean(); + baseCurseChance = data.get("baseCurseChance").getAsJsonObject().get("baseCurseChance").getAsInt(); + maxCurseChance = data.get("maxCurseChance").getAsJsonObject().get("maxCurseChance").getAsInt(); + curseEveryXLevels = data.get("curseEveryXLevels").getAsJsonObject().get("curseEveryXLevels").getAsInt(); for (final JsonElement element : data.get("excludedCurses").getAsJsonObject().get("excludedCurses").getAsJsonArray()) { excludedCurses.add(element.getAsString()); @@ -131,15 +161,29 @@ public void parseConfig() CurseEnchantmentHelper.curseWeightMap.put(enchantmentEntry.getValue(), weight); } else - { Cursery.LOGGER.info("Excluding curse: " + ForgeRegistries.ENCHANTMENTS.getKey(enchantmentEntry.getValue()) + " as config disables it"); - } } } if (totalCurseWeight == 0) - { Cursery.LOGGER.error("Unable to retrieve curses from registry"); + + if (baseCurseChance < 0 || baseCurseChance > 100) + { + Cursery.LOGGER.warn(String.format("BaseCurseChance was set to '%d' yet must be within the interval 0 <= X <= 100. Setting back to 5.", baseCurseChance)); + baseCurseChance = 5; + } + + if (maxCurseChance < 0 || maxCurseChance > 100) + { + Cursery.LOGGER.warn(String.format("MaxCurseChance was set to '%d' yet must be within the interval 0 <= X <= 100. Setting back to 75.", maxCurseChance)); + maxCurseChance = 75; + } + + if (curseEveryXLevels < 0) + { + Cursery.LOGGER.warn(String.format("CurseEveryXLevels was set to '%d' yet must be within interval X >= 0. Setting back to 0.", curseEveryXLevels)); + curseEveryXLevels = 0; } } } diff --git a/src/main/java/com/cursery/enchant/CurseEnchantmentHelper.java b/src/main/java/com/cursery/enchant/CurseEnchantmentHelper.java index 057fe2a..4360863 100644 --- a/src/main/java/com/cursery/enchant/CurseEnchantmentHelper.java +++ b/src/main/java/com/cursery/enchant/CurseEnchantmentHelper.java @@ -10,6 +10,7 @@ import net.minecraft.world.item.enchantment.Enchantment; import net.minecraftforge.registries.ForgeRegistries; +import java.util.function.Supplier; import java.util.*; /** @@ -60,7 +61,7 @@ public static boolean checkForRandomCurse(final ItemStack stack, final Map addedLevels = new ArrayList<>(); - boolean appliedCurse = false; + boolean isCurseApplied = false; // Compare enchants for (final Map.Entry newEnchant : newEnchants.entrySet()) { @@ -103,12 +104,12 @@ public static boolean checkForRandomCurse(final ItemStack stack, final Map newEnchants) { + + boolean isCurseApplied = false; + int guaranteedCurseInterval = Cursery.config.getCommonConfig().curseEveryXLevels; + + // Checks how many curses in the interval have been passed. + // Only makes a difference when curseEveryXLevels is enabled (i.e > 0) + int existingGuaranteedCursesPassed = guaranteedCurseInterval == 0 ? 0 : (int) Math.ceil((double) (levelSum + 1) / guaranteedCurseInterval); + int totalGuaranteedCursesPassed = guaranteedCurseInterval == 0 ? 0 : (int) Math.floor((double) (levelSum + newLevel) / guaranteedCurseInterval); + int guaranteedCursesToApply = guaranteedCurseInterval == 0 ? 0 : totalGuaranteedCursesPassed - existingGuaranteedCursesPassed + 1; + + if (guaranteedCursesToApply == 0) + { + if (Cursery.config.getCommonConfig().debugTries) + Cursery.LOGGER.info("CurseEveryXLevels override is FALSE."); + } + else + { + if (Cursery.config.getCommonConfig().debugTries) + Cursery.LOGGER.info("CurseEveryXLevels override is TRUE."); + + for (int i = 0; i < guaranteedCursesToApply; i++) + { + if (Cursery.config.getCommonConfig().debugTries) + { + Cursery.LOGGER.info("Rolling new curse for " + stack + " guaranteedCursesToApply: " + guaranteedCursesToApply + + " totalEnchantLevels: " + levelSum + " curseChance overridden by curseEveryXLevels"); + } + isCurseApplied = applyCurseTo(stack, newEnchants); + } + } + + // Makes sure the item is not cursed again for each level the guaranteed curse was applied. + int levelsLeftToApply = newLevel - guaranteedCursesToApply; + + Supplier curseChance; + int minCurseChance = Cursery.config.getCommonConfig().baseCurseChance - (stack.getEnchantmentValue() >> 1); + int curseChanceRange = Cursery.config.getCommonConfig().maxCurseChance - Cursery.config.getCommonConfig().baseCurseChance; + + if (Cursery.config.getCommonConfig().curseChanceScales) + { + // Scaling rate varies with the marked number. + // Ideally should be kept between -0.0125 <= X <= -0.0175 + // Anything greater or lower will lead to extremely fast or slow scaling rates respectively. + curseChance = () -> (int) Math.ceil( + minCurseChance + curseChanceRange * (1 - Math.exp(/*Important*/-0.015/*Important*/ * levelSum)) + ); + } + else + curseChance = () -> minCurseChance; + + // Each level has the same chance, so its the same to apply enchant V vs I to V - boolean appliedCurse = false; - for (int i = 0; i < newLevel; i++) + for (int i = 0; i < levelsLeftToApply; i++) { if (Cursery.config.getCommonConfig().debugTries) + Cursery.LOGGER.info("Rolling new curse for " + stack + " addedEnchLevels: " + levelsLeftToApply + + " totalEnchantLevels: " + levelSum + " chance:" + curseChance.get()); + + if (rand.nextInt(100) < curseChance.get()) + isCurseApplied = applyCurseTo(stack, newEnchants); + } + return isCurseApplied; + } + + /** + * Applies curse to item. Helper method to rollAndApplyCurseTo() + * + * @param stack item + * @param newEnchants new enchantments on the stack + * @return true if curse is applied. + */ + private static boolean applyCurseTo( + final ItemStack stack, + final Map newEnchants) + { + if (Cursery.config.getCommonConfig().debugTries) + Cursery.LOGGER.info("Trying to apply curse to: " + stack); + + for (int j = 0; j < 15; j++) + { + final Enchantment curse = getRandomCurse(); + if (curse == null) { - Cursery.LOGGER.info("Rolling new curse for " + stack + " addedEnchLevels: " + newLevel + " totalEnchantLevels: " + levelSum + " chance:" + Math.min(75, - Cursery.config.getCommonConfig().basecursechance + levelSum - (stack.getEnchantmentValue() >> 1))); + continue; } - if (rand.nextInt(100) < Math.min(75, Cursery.config.getCommonConfig().basecursechance + levelSum - (stack.getEnchantmentValue() >> 1))) + final int currentLevel = newEnchants.getOrDefault(curse, 0); + if (currentLevel < curse.getMaxLevel() && curse.canEnchant(stack) && isCompatibleWithAll(curse, newEnchants)) { if (Cursery.config.getCommonConfig().debugTries) - { - Cursery.LOGGER.info("Trying to apply curse to: " + stack); - } + Cursery.LOGGER.info("Applying curse " + ForgeRegistries.ENCHANTMENTS.getKey(curse) + " to: " + stack); - for (int j = 0; j < 15; j++) - { - final Enchantment curse = getRandomCurse(); - if (curse == null) - { - continue; - } - - final int currentLevel = newEnchants.getOrDefault(curse, 0); - if (currentLevel < curse.getMaxLevel() && curse.canEnchant(stack) && isCompatibleWithAll(curse, newEnchants)) - { - if (Cursery.config.getCommonConfig().debugTries) - { - Cursery.LOGGER.info("Applying curse " + ForgeRegistries.ENCHANTMENTS.getKey(curse) + " to: " + stack); - } - - enchantManually(stack, curse, currentLevel + 1); - newEnchants.put(curse, currentLevel + 1); - appliedCurse = true; - break; - } - } + enchantManually(stack, curse, currentLevel + 1); + newEnchants.put(curse, currentLevel + 1); + return true; } } - - return appliedCurse; + return false; } /** @@ -199,13 +255,15 @@ private static boolean isCompatibleWithAll(final Enchantment enchantment, final { for (final Map.Entry entry : newEnchants.entrySet()) { + // Makes sure to break (and hence return true) if an existing curse is rolled. + if (enchantment == entry.getKey()) + break; + if (!entry.getKey().isCompatibleWith(enchantment)) { if (Cursery.config.getCommonConfig().debugTries) - { - Cursery.LOGGER.info( - "Curse " + ForgeRegistries.ENCHANTMENTS.getKey(enchantment) + " is not compatible with " + ForgeRegistries.ENCHANTMENTS.getKey(entry.getKey())); - } + Cursery.LOGGER.info("Curse " + ForgeRegistries.ENCHANTMENTS.getKey(enchantment) + + " is not compatible with " + ForgeRegistries.ENCHANTMENTS.getKey(entry.getKey())); return false; } @@ -258,9 +316,28 @@ public static void enchantManually(final ItemStack stack, Enchantment enchantmen } ListTag listnbt = stack.getTag().getList("Enchantments", 10); + + // Makes sure to remove nbt tag of the existing enchantment with its previous level + if (level > 1) + { + if (Cursery.config.getCommonConfig().debugTries) { + Cursery.LOGGER.info("Removing enchantment: " + enchantment.getDescriptionId() + + ",\n with level: " + (level - 1) + " from item to replace it with level: " + level); + } + CompoundTag compoundnbt = new CompoundTag(); + compoundnbt.putString("id", String.valueOf((Object) ForgeRegistries.ENCHANTMENTS.getKey(enchantment))); + compoundnbt.putShort("lvl", (short) ((byte) level - 1)); + listnbt.remove(compoundnbt); + } + + if (Cursery.config.getCommonConfig().debugTries) { + Cursery.LOGGER.info("Adding back enchantment: " + enchantment.getDescriptionId() + + ",\n with level: " + level); + } CompoundTag compoundnbt = new CompoundTag(); compoundnbt.putString("id", String.valueOf((Object) ForgeRegistries.ENCHANTMENTS.getKey(enchantment))); compoundnbt.putShort("lvl", (short) ((byte) level)); + listnbt.add(compoundnbt); } diff --git a/src/main/java/com/cursery/mixin/KintsugiRemixEnchantmentMenuMixin.java b/src/main/java/com/cursery/mixin/KintsugiRemixEnchantmentMenuMixin.java new file mode 100644 index 0000000..8e31004 --- /dev/null +++ b/src/main/java/com/cursery/mixin/KintsugiRemixEnchantmentMenuMixin.java @@ -0,0 +1,62 @@ +package com.cursery.mixin; + +import com.cursery.Cursery; +import com.cursery.enchant.CurseEnchantmentHelper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.infinitelimit.kintsugi.menus.RemixEnchantmentMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Map; + +@Mixin(RemixEnchantmentMenu.class) +public abstract class KintsugiRemixEnchantmentMenuMixin +{ + private Map previousEnchants; + + @Inject(method = "calculateResultItem", at = @At("HEAD"), remap = false) + private void delayApplyingCurse(final ItemStack itemStack, final ItemStack fuelStack, final CallbackInfo ci) { + CurseEnchantmentHelper.delayNext = true; + CurseEnchantmentHelper.delayItem = itemStack.getItem(); + previousEnchants = itemStack.getAllEnchantments(); + } + + @Inject(method = "onTake", at = @At("HEAD"), remap = false) + private void curseItemOnTake(final Player pPlayer, final ItemStack pStack, final CallbackInfo ci) { + if (Cursery.config.getCommonConfig().debugTries) { + Cursery.LOGGER.info("calculateResultItem: delayed the curse."); + Cursery.LOGGER.info("Previous Enchantments:" + formatEnchantments(previousEnchants)); + Cursery.LOGGER.info("onTake: Retrieving new enchantments."); + } + + Map existingEnchants = pStack.getAllEnchantments(); + + if (Cursery.config.getCommonConfig().debugTries) { + Cursery.LOGGER.info("New Enchantments:" + formatEnchantments(existingEnchants)); + Cursery.LOGGER.info("Checking for random curse for stack: " + pStack); + } + + boolean isCurseApplied = CurseEnchantmentHelper.checkForRandomCurse(pStack, previousEnchants, existingEnchants); + + if (Cursery.config.getCommonConfig().debugTries) { + Cursery.LOGGER.info(isCurseApplied ? "Curse applied!" : "Curse was not applied."); + } + } + + + /** + * Helper method to format enchantments map into a readable string. + */ + private String formatEnchantments(Map enchantments) { + if (enchantments == null || enchantments.isEmpty()) { + return " None"; + } + StringBuilder sb = new StringBuilder(); + enchantments.forEach((enchant, level) -> sb.append("\n - ").append(enchant.getDescriptionId()).append(": Level ").append(level)); + return sb.toString(); + } + +} diff --git a/src/main/resources/cursery.mixins.json b/src/main/resources/cursery.mixins.json index 8cea302..f53f05f 100644 --- a/src/main/resources/cursery.mixins.json +++ b/src/main/resources/cursery.mixins.json @@ -8,6 +8,7 @@ "AttributeModifierManagerMixin", "EnchantmentHelperMixin", "ItemStackMixin", + "KintsugiRemixEnchantmentMenuMixin", "QuarkEnchanterCompat", "QuarkEnchanterTECompat" ],