package net.knarcraft.blacksmith.util;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* A helper class for deciding the salvage returned if salvaging an item
*/
public final class SalvageHelper {
private static final Random random = new Random();
/**
* Gets whether the given item has a valid crafting recipe
*
* @param item
The item to check
* @return True if the item has a valid crafting recipe
*/
public static boolean hasRecipe(@NotNull ItemStack item) {
List recipes = Bukkit.getRecipesFor(new ItemStack(item.getType(), item.getAmount()));
for (Recipe recipe : recipes) {
if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) {
return true;
}
}
return false;
}
/**
* Gets the sum of all enchantment levels for the given item
*
* @param item The item to check
* @return The total amount of enchantment levels on the item
*/
public static int getTotalEnchantmentLevels(@NotNull ItemStack item) {
int expLevels = 0;
for (Enchantment enchantment : item.getEnchantments().keySet()) {
expLevels += item.getEnchantmentLevel(enchantment);
}
return expLevels;
}
/**
* Gets the items to return if salvaging the given item stack
*
* Note: Only items craft-able in a crafting table are salvageable. Netherite gear is therefore not salvageable.
*
* @param server The server to get recipes from
* @param salvagedItem The item stack to salvage
* @param ignoredSalvage Any material which should not be returned as part of the salvage.
* @param extended Whether to enable extended salvage, ignoring the repairable restriction
* @return The items to return to the user, or null if not salvageable
*/
public static @Nullable List getSalvage(@NotNull Server server, @Nullable ItemStack salvagedItem,
@NotNull Collection ignoredSalvage, boolean extended) {
if (salvagedItem == null || salvagedItem.getAmount() < 1 ||
(!extended && !ItemHelper.isRepairable(salvagedItem))) {
return null;
}
for (Recipe recipe : server.getRecipesFor(new ItemStack(salvagedItem.getType(), salvagedItem.getAmount()))) {
if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) {
List salvage = getRecipeSalvage(recipe, salvagedItem, ignoredSalvage);
if (salvage != null && !salvage.isEmpty()) {
return salvage;
}
}
}
return null;
}
/**
* Gets the salvage resulting from the given recipe and the given item
*
* @param recipe The recipe to get salvage for
* @param salvagedItem The item to be salvaged
* @param ignoredSalvage Any material which should not be returned as part of the salvage.
* @return A list of items, or null if not a valid type of recipe
*/
private static @Nullable List getRecipeSalvage(@NotNull Recipe recipe, @NotNull ItemStack salvagedItem,
@NotNull Collection ignoredSalvage) {
List ingredients;
if (recipe instanceof ShapedRecipe shapedRecipe) {
ingredients = getIngredients(shapedRecipe);
} else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
ingredients = shapelessRecipe.getIngredientList();
} else {
//Recipes other than crafting shouldn't be considered for salvaging
return null;
}
//Make things easier by eliminating identical stacks
ingredients = combineStacks(ingredients);
//Purge any ignored salvage to only calculate salvage using the remaining items
ingredients.removeIf((item) -> ignoredSalvage.contains(item.getType()));
return combineStacks(getSalvage(copyItems(ingredients), salvagedItem));
}
/**
* Copies a list of items
*
* Note: This does not copy any metadata. It only copies the item type and the amount.
*
* @param itemsToCopy The items to make a copy of
* @return A copy of the given items
*/
private static @NotNull List copyItems(@NotNull List itemsToCopy) {
List copies = new ArrayList<>(itemsToCopy.size());
for (ItemStack itemStack : itemsToCopy) {
copies.add(new ItemStack(itemStack.getType(), itemStack.getAmount()));
}
return copies;
}
/**
* Gets the salvage resulting from the given item, and the given recipe items
*
* @param recipeItems All items required for crafting the item to salvage
* @param salvagedItem The item to be salvaged
* @return The items to be returned to the user as salvage
*/
@NotNull
private static List getSalvage(@NotNull List recipeItems,
@NotNull ItemStack salvagedItem) {
int durability = ItemHelper.getDurability(salvagedItem);
int maxDurability = ItemHelper.getMaxDurability(salvagedItem);
// Prevent divide by zero for items that don't have a set max durability
if (maxDurability == 0) {
maxDurability = 1;
durability = 1;
}
double percentageRemaining = (double) durability / maxDurability;
int totalItems = totalItemAmount(recipeItems);
//Get the amount of recipe items to be returned
int itemsToReturn = (int) Math.floor(percentageRemaining * totalItems);
int bound = recipeItems.size();
List salvage = new ArrayList<>();
for (int i = 0; i < itemsToReturn; i++) {
int itemIndex = SalvageHelper.random.nextInt(bound);
ItemStack itemStack = recipeItems.get(itemIndex);
//Make sure to never give more of one item than the amount which exists in the recipe
if (itemStack.getAmount() <= 0) {
i--;
continue;
}
itemStack.setAmount(itemStack.getAmount() - 1);
salvage.add(new ItemStack(itemStack.getType(), 1));
}
return salvage;
}
/**
* Gets the total sum of items in the given list of items
*
* @param items The items to get the sum of
* @return The total number of items
*/
private static int totalItemAmount(@NotNull List items) {
int sum = 0;
for (ItemStack itemStack : items) {
sum += itemStack.getAmount();
}
return sum;
}
/**
* Combines all items of the same type in the given list
*
* Basically, if the input is two item stacks containing one diamond each, the output will be an item stack with
* two diamonds instead.
*
* @param items The items to combine
* @return The given items, but combined
*/
private static @NotNull List combineStacks(@NotNull List items) {
Map itemAmounts = new HashMap<>();
for (ItemStack item : items) {
Material itemType = item.getType();
itemAmounts.put(itemType, itemAmounts.getOrDefault(itemType, 0) + 1);
}
List combined = new ArrayList<>();
for (Material material : itemAmounts.keySet()) {
combined.add(new ItemStack(material, itemAmounts.get(material)));
}
return combined;
}
/**
* Gets all ingredients contained in the given shaped recipe
*
* @param shapedRecipe The shaped recipe to get ingredients for
* @return The items contained in the recipe
*/
@NotNull
private static List getIngredients(@NotNull ShapedRecipe shapedRecipe) {
List ingredients = new ArrayList<>();
Map ingredientMap = shapedRecipe.getIngredientMap();
//The shape is a list of the three rows' strings. Each string contains 3 characters.
String[] shape = shapedRecipe.getShape();
for (String row : shape) {
for (int column = 0; column < row.length(); column++) {
ItemStack item = ingredientMap.get(row.charAt(column));
if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) {
ingredients.add(item);
}
}
}
return ingredients;
}
}