package com.dre.brewery.recipe; import com.dre.brewery.P; import com.dre.brewery.utility.StringParser; import com.dre.brewery.utility.Tuple; import org.bukkit.Color; import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.stream.Collectors; /** * A Recipe for the Base Potion coming out of the Cauldron. */ public class BCauldronRecipe { public static List recipes = new ArrayList<>(); public static int numConfigRecipes; public static List acceptedCustom = new ArrayList<>(); // All accepted custom and other items public static Set acceptedSimple = EnumSet.noneOf(Material.class); // All accepted simple items public static Set acceptedMaterials = EnumSet.noneOf(Material.class); // Fast cache for all accepted Materials private String name; private List ingredients; private PotionColor color; private List> particleColor = new ArrayList<>(); private List lore; private int cmData; // Custom Model Data private boolean saveInData; // If this recipe should be saved in data and loaded again when the server restarts. Applicable to non-config recipes /** * A New Cauldron Recipe with the given name. *

Use new BCauldronRecipe.Builder() for easier Cauldron Recipe Creation * * @param name Name of the Cauldron Recipe */ public BCauldronRecipe(String name) { this.name = name; color = PotionColor.CYAN; } @Nullable public static BCauldronRecipe fromConfig(ConfigurationSection cfg, String id) { String name = cfg.getString(id + ".name"); if (name != null) { name = P.p.color(name); } else { P.p.errorLog("Missing name for Cauldron-Recipe: " + id); return null; } BCauldronRecipe recipe = new BCauldronRecipe(name); recipe.ingredients = BRecipe.loadIngredients(cfg, id); if (recipe.ingredients == null || recipe.ingredients.isEmpty()) { P.p.errorLog("No ingredients for Cauldron-Recipe: " + recipe.name); return null; } String col = cfg.getString(id + ".color"); if (col != null) { recipe.color = PotionColor.fromString(col); } else { recipe.color = PotionColor.CYAN; } if (recipe.color == PotionColor.WATER && !col.equals("WATER")) { recipe.color = PotionColor.CYAN; // Don't throw error here as old mc versions will not know even the default colors //P.p.errorLog("Invalid Color '" + col + "' in Cauldron-Recipe: " + recipe.name); //return null; } for (String entry : cfg.getStringList(id + ".cookParticles")) { String[] split = entry.split("/"); int minute; if (split.length == 1) { minute = 10; } else if (split.length == 2) { minute = P.p.parseInt(split[1]); } else { P.p.errorLog("cookParticle: '" + entry + "' in: " + recipe.name); return null; } if (minute < 1) { P.p.errorLog("cookParticle: '" + entry + "' in: " + recipe.name); return null; } PotionColor partCol = PotionColor.fromString(split[0]); if (partCol == PotionColor.WATER && !split[0].equals("WATER")) { P.p.errorLog("Color of cookParticle: '" + entry + "' in: " + recipe.name); return null; } recipe.particleColor.add(new Tuple<>(minute, partCol.getColor())); } if (!recipe.particleColor.isEmpty()) { // Sort by minute recipe.particleColor.sort(Comparator.comparing(Tuple::first)); } List> lore = BRecipe.loadQualityStringList(cfg, id + ".lore", StringParser.ParseType.LORE); if (lore != null && !lore.isEmpty()) { recipe.lore = lore.stream().map(Tuple::second).collect(Collectors.toList()); } recipe.cmData = cfg.getInt(id + ".customModelData", 0); return recipe; } // Getter @NotNull public String getName() { return name; } @NotNull public List getIngredients() { return ingredients; } @NotNull public PotionColor getColor() { return color; } @NotNull public List> getParticleColor() { return particleColor; } @Nullable public List getLore() { return lore; } public boolean isSaveInData() { return saveInData; } // Setter /** * When Changing ingredients, Accepted Lists have to be updated in BCauldronRecipe */ public void setIngredients(@NotNull List ingredients) { this.ingredients = ingredients; } public void setColor(@NotNull PotionColor color) { this.color = color; } public void setLore(List lore) { this.lore = lore; } /** * Get the Custom Model Data */ public int getCmData() { return cmData; } public void setSaveInData(boolean saveInData) { this.saveInData = saveInData; } /** * Find how much these ingredients match the given ones from 0-10. *

If any ingredient is missing, returns 0 *
Any included item that is not in the recipe, will drive the number down most heavily. *
More Amount of any item, will logarithmically raise the number *
Difference in Amount to what the recipe expects will make a tiny difference on the number *

So apart from unexpected items, more amount of the correct item will make the number go up, * with a little dip for difference in expected amount. * *

The thought behind this is, that a given list of ingredients matches this recipe most, when: *
1. It is not missing ingredients, *
2. It has no unexpected ingredients *
3. It has a lot of the matching ingredients, so that for two recipes, both having the same * amount of unexpected ingredients, the one matching the item with the highest amounts wins. *
For Example | Recipe_1: (Wheat*1), Recipe_2: (Sugar*1) | Ingredients: (Wheat*10, Sugar*5), Recipe_1 should win, * even though the difference in expected amount (1) is lower for Recipe_2 *
4. It has the least difference in expected ingredient amount. */ public float getIngredientMatch(List items) { if (items.size() < ingredients.size()) { return 0; } float match = 10; search: for (RecipeItem recipeIng : ingredients) { for (Ingredient ing : items) { if (recipeIng.matches(ing)) { double difference = Math.abs(recipeIng.getAmount() - ing.getAmount()); if (difference >= 1000) { return 0; } // The Item Amount is the determining part here, the higher the better. // But let the difference in amount to what the recipe expects have a tiny factor as well. // This way for the same amount, the recipe with the lower difference wins. double factor = ing.getAmount() * (1.0 - (difference / 1000.0)) ; //double mod = 0.1 + (0.9 * Math.exp(-0.03 * difference)); // logarithmic curve from 1 to 0.1 double mod = 1 + (0.9 * -Math.exp(-0.03 * factor)); // logarithmic curve from 0.1 to 1, small for a low factor match *= mod; continue search; } } return 0; } if (items.size() > ingredients.size()) { // If there are too many items in the List, multiply the match by 0.1 per Item thats too much // So that even if every other ingredient is perfect, a recipe that expects all these items will fare better float tooMuch = items.size() - ingredients.size(); double mod = Math.pow(0.1, tooMuch); match *= mod; } P.p.debugLog("Match for Cauldron Recipe " + name + ": " + match); return match; } public void updateAcceptedLists() { for (RecipeItem ingredient : getIngredients()) { if (ingredient.hasMaterials()) { BCauldronRecipe.acceptedMaterials.addAll(ingredient.getMaterials()); } if (ingredient instanceof SimpleItem) { BCauldronRecipe.acceptedSimple.add(((SimpleItem) ingredient).getMaterial()); } else { // Add it as acceptedCustom if (!BCauldronRecipe.acceptedCustom.contains(ingredient)) { BCauldronRecipe.acceptedCustom.add(ingredient); } } } } @Override public String toString() { return "BCauldronRecipe{" + name + '}'; } @Nullable public static BCauldronRecipe get(String name) { for (BCauldronRecipe recipe : recipes) { if (recipe.name.equalsIgnoreCase(name)) { return recipe; } } return null; } /** * Gets a Modifiable Sublist of the CauldronRecipes that are loaded by config. *

Changes are directly reflected by the main list of all recipes *
Changes to the main List of all CauldronRecipes will make the reference to this sublist invalid * *

After adding or removing elements, CauldronRecipes.numConfigRecipes MUST be updated! */ public static List getConfigRecipes() { return recipes.subList(0, numConfigRecipes); } /** * Gets a Modifiable Sublist of the CauldronRecipes that are added by plugins. *

Changes are directly reflected by the main list of all recipes *
Changes to the main List of all CauldronRecipes will make the reference to this sublist invalid */ public static List getAddedRecipes() { return recipes.subList(numConfigRecipes, recipes.size()); } /** * Gets the main List of all CauldronRecipes. */ public static List getAllRecipes() { return recipes; } /*public static boolean acceptItem(ItemStack item) { if (acceptedMaterials.contains(item.getType())) { // Extremely fast way to check for most items return true; } if (!item.hasItemMeta()) { return false; } // If the Item is not on the list, but customized, we have to do more checks ItemMeta meta = item.getItemMeta(); assert meta != null; if (meta.hasDisplayName() || meta.hasLore()) { for (BItem bItem : acceptedCustom) { if (bItem.matches(item)) { return true; } } } return false; } @Nullable public static RecipeItem acceptItem(ItemStack item) { if (!acceptedMaterials.contains(item.getType()) && !item.hasItemMeta()) { // Extremely fast way to check for most items return null; } // If the Item is on the list, or customized, we have to do more checks for (RecipeItem rItem : acceptedItems) { if (rItem.matches(item)) { return rItem; } } return null; }*/ /** * Builder to easily create BCauldron recipes. */ public static class Builder { private BCauldronRecipe recipe; public Builder(String name) { recipe = new BCauldronRecipe(name); } public Builder addIngredient(RecipeItem... item) { if (recipe.ingredients == null) { recipe.ingredients = new ArrayList<>(); } Collections.addAll(recipe.ingredients, item); return this; } public Builder addIngredient(ItemStack... item) { if (recipe.ingredients == null) { recipe.ingredients = new ArrayList<>(); } for (ItemStack i : item) { recipe.ingredients.add(new CustomItem(i)); } return this; } public Builder color(String colorString) { recipe.color = PotionColor.fromString(colorString); return this; } public Builder color(PotionColor color) { recipe.color = color; return this; } public Builder color(Color color) { recipe.color = PotionColor.fromColor(color); return this; } public Builder addParticleColor(int atMinute, Color color) { recipe.particleColor.add(new Tuple<>(atMinute, color)); return this; } public Builder addLore(String line) { if (recipe.lore == null) { recipe.lore = new ArrayList<>(); } recipe.lore.add(line); return this; } public BCauldronRecipe get() { if (recipe.name == null) { throw new IllegalArgumentException("CauldronRecipe name is null"); } if (BCauldronRecipe.get(recipe.getName()) != null) { throw new IllegalArgumentException("CauldronRecipe with name " + recipe.getName() + " already exists"); } if (recipe.color == null) { throw new IllegalArgumentException("CauldronRecipe has no color"); } if (recipe.ingredients == null || recipe.ingredients.isEmpty()) { throw new IllegalArgumentException("CauldronRecipe has no ingredients"); } if (!recipe.particleColor.isEmpty()) { // Sort particleColor by minute recipe.particleColor.sort(Comparator.comparing(Tuple::first)); } return recipe; } } }