From 8301a24afe42fc0e0984eaed07f150484b18aa70 Mon Sep 17 00:00:00 2001 From: Electroblob77 <35599699+Electroblob77@users.noreply.github.com> Date: Wed, 15 Apr 2020 23:01:45 +0100 Subject: [PATCH] Just enough wizardry! - Adds JEI support for arcane workbench 'recipes' (charging, upgrading, and scroll enchantment) - Adds an item transfer handler for said recipes, including bookshelf virtual slot support - Prevents conjured items from showing up in JEI --- .../java/electroblob/wizardry/Settings.java | 19 +- .../client/gui/GuiArcaneWorkbench.java | 3 +- .../jei/ArcaneWorkbenchRecipe.java | 119 ++++++++ .../jei/ArcaneWorkbenchRecipeCategory.java | 269 ++++++++++++++++++ .../jei/ArcaneWorkbenchTransferHandler.java | 52 ++++ .../integration/jei/WizardryJEIPlugin.java | 56 ++++ .../inventory/ContainerArcaneWorkbench.java | 22 +- .../wizardry/item/IWorkbenchItem.java | 19 +- .../electroblob/wizardry/item/ItemWand.java | 88 +++--- .../wizardry/item/ItemWizardArmour.java | 41 ++- .../wizardry/registry/WizardryRecipes.java | 31 +- .../arcane_workbench_jei_background.png | Bin 0 -> 10281 bytes 12 files changed, 641 insertions(+), 78 deletions(-) create mode 100644 src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java create mode 100644 src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java create mode 100644 src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java create mode 100644 src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java create mode 100644 src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png diff --git a/src/main/java/electroblob/wizardry/Settings.java b/src/main/java/electroblob/wizardry/Settings.java index d58ff358..952d9062 100644 --- a/src/main/java/electroblob/wizardry/Settings.java +++ b/src/main/java/electroblob/wizardry/Settings.java @@ -241,10 +241,11 @@ public final class Settings { public String[] damageSourceBlacklist = {}; /** [Server-only] Whether to print compatibility warnings to the console. */ public boolean compatibilityWarnings = true; + // TODO: Should these really be server-only? /** [Server-only] Whether Baubles integration features are enabled. */ public boolean baublesIntegration = true; -// /** [Server-only] Whether JEI integration features are enabled. */ -// public boolean jeiIntegration = true; + /** [Server-only] Whether JEI integration features are enabled. */ + public boolean jeiIntegration = true; /** [Server-only] Whether Antique Atlas integration features are enabled. */ public boolean antiqueAtlasIntegration = true; /** [Server-only] Whether global markers for wizard towers are added to antique atlases. */ @@ -1111,13 +1112,13 @@ public final class Settings { baublesIntegration = property.getBoolean(); propOrder.add(property.getName()); -// property = config.get(COMPATIBILITY_CATEGORY, "jeiIntegration", true, -// "If JEI (Just Enough Items) is installed, controls whether JEI integration features are enabled. If this is disabled, wizardry will always behave as if JEI is not installed."); -// property.setLanguageKey("config." + Wizardry.MODID + ".jei_integration"); -// property.setRequiresMcRestart(true); -// Wizardry.proxy.setToNamedBooleanEntry(property); -// jeiIntegration = property.getBoolean(); -// propOrder.add(property.getName()); + property = config.get(COMPATIBILITY_CATEGORY, "jeiIntegration", true, + "If JEI (Just Enough Items) is installed, controls whether JEI integration features are enabled. If this is disabled, wizardry will always behave as if JEI is not installed."); + property.setLanguageKey("config." + Wizardry.MODID + ".jei_integration"); + property.setRequiresMcRestart(true); + Wizardry.proxy.setToNamedBooleanEntry(property); + jeiIntegration = property.getBoolean(); + propOrder.add(property.getName()); property = config.get(COMPATIBILITY_CATEGORY, "antiqueAtlasIntegration", true, "If Antique Atlas is installed, controls whether Antique Atlas integration features are enabled. If this is disabled, wizardry will always behave as if Antique Atlas is not installed."); diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java index afc9ebe4..8b0ea1e8 100644 --- a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java +++ b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java @@ -113,7 +113,7 @@ public class GuiArcaneWorkbench extends GuiContainer { public void initGui(){ this.mc.player.openContainer = this.inventorySlots; - this.guiLeft = (this.width - this.xSize) / 2; + this.guiLeft = (this.width - MAIN_GUI_WIDTH) / 2; // Use MAIN_GUI_WIDTH, not xSize, otherwise JEI messes with it this.guiTop = (this.height - this.ySize) / 2; Keyboard.enableRepeatEvents(true); @@ -132,6 +132,7 @@ public class GuiArcaneWorkbench extends GuiContainer { this.searchField.setCanLoseFocus(false); this.searchField.setFocused(true); + this.tooltipElements.clear(); this.tooltipElements.add(new TooltipElementItemName(new Style().setColor(TextFormatting.WHITE), LINE_SPACING_WIDE)); this.tooltipElements.add(new TooltipElementManaReadout(LINE_SPACING_WIDE)); this.tooltipElements.add(new TooltipElementProgressionBar(LINE_SPACING_WIDE)); diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java new file mode 100644 index 00000000..9adea78d --- /dev/null +++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java @@ -0,0 +1,119 @@ +package electroblob.wizardry.integration.jei; + +import electroblob.wizardry.client.DrawingUtils; +import electroblob.wizardry.constants.Constants; +import electroblob.wizardry.constants.Element; +import electroblob.wizardry.inventory.ContainerArcaneWorkbench; +import electroblob.wizardry.item.IWorkbenchItem; +import electroblob.wizardry.registry.WizardryItems; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeWrapper; +import net.minecraft.client.Minecraft; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.MathHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a 'recipe' for the arcane workbench. + * + * @author Electroblob + * @since Wizardry 4.3 + */ +public class ArcaneWorkbenchRecipe implements IRecipeWrapper { + + private final ItemStack centreStack; + private final List books; + private final List crystals; + private final List upgrades; + private final ItemStack result; + + private final int bookSlots; + + private final List> inputs; + + public ArcaneWorkbenchRecipe(ItemStack centreStack, List books, List crystals, List upgrades, ItemStack result){ + + this.centreStack = centreStack; + this.books = books; // CAUTION! This list is an OUTER LIST! + this.crystals = crystals; + this.upgrades = upgrades; + this.result = result; + + this.inputs = new ArrayList<>(); + for(ItemStack book : books) this.inputs.add(Collections.singletonList(book)); + this.inputs.add(crystals); + this.inputs.add(Collections.singletonList(centreStack)); + this.inputs.add(upgrades); + + if(centreStack.getItem() instanceof IWorkbenchItem){ + bookSlots = ((IWorkbenchItem)centreStack.getItem()).getSpellSlotCount(centreStack); + }else{ + bookSlots = 0; + } + + } + + public ArcaneWorkbenchRecipe(ItemStack centreStack, List books, int mana, List upgrades, ItemStack result){ + this(centreStack, books, generateCrystalStacks(mana), upgrades, result); + } + + @Override + public void getIngredients(IIngredients ingredients){ + ingredients.setInputLists(VanillaTypes.ITEM, inputs); + ingredients.setOutput(VanillaTypes.ITEM, result); + } + + @Override + public void drawInfo(Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY){ + // Can't do this in IRecipeCategory#drawExtras because we have no access to the recipe there! + // ArcaneWorkbenchRecipeCategory.TEXTURE is already bound at this point + for(int i = 0; i < bookSlots; i++){ + int x = ArcaneWorkbenchRecipeCategory.CENTRE_SLOT_X + ContainerArcaneWorkbench.getBookSlotXOffset(i, bookSlots) - 9; + int y = ArcaneWorkbenchRecipeCategory.CENTRE_SLOT_Y + ContainerArcaneWorkbench.getBookSlotYOffset(i, bookSlots) - 9; + DrawingUtils.drawTexturedRect(x, y, 0, ArcaneWorkbenchRecipeCategory.HEIGHT, 36, 36, 256, 256); + } + } + + /** + * Returns a list of item stacks, one for each type of crystal (regular, elemental, grand and shards), each with + * the minimum quantity needed to supply the given amount of mana. Types of crystal for which more than the max. + * stack size would be needed are ignored. + */ + public static List generateCrystalStacks(int mana){ + + if(mana < 0) throw new IllegalArgumentException("Cannot create an arcane workbench recipe with negative mana!"); + + if(mana == 0) return Collections.emptyList(); + + List crystalStacks = new ArrayList<>(); + + int count = MathHelper.ceil((float)mana / Constants.MANA_PER_CRYSTAL); + // A stack of crystals will almost certainly be enough mana, but you never know! + // Using ItemStack.EMPTY to avoid deprecated method; crystals' stack size is not stack-sensitive so it doesn't matter + if(count <= WizardryItems.magic_crystal.getItemStackLimit(ItemStack.EMPTY)){ + for(int meta = 0; meta < Element.values().length; meta++){ + crystalStacks.add(new ItemStack(WizardryItems.magic_crystal, count, meta)); + } + } + + count = MathHelper.ceil((float)mana / Constants.MANA_PER_SHARD); + + if(count <= WizardryItems.crystal_shard.getItemStackLimit(ItemStack.EMPTY)){ + crystalStacks.add(new ItemStack(WizardryItems.crystal_shard, count)); + } + + count = MathHelper.ceil((float)mana / Constants.GRAND_CRYSTAL_MANA); + + if(count <= WizardryItems.grand_crystal.getItemStackLimit(ItemStack.EMPTY)){ + crystalStacks.add(new ItemStack(WizardryItems.grand_crystal, count)); + } + + return crystalStacks; + + } + +} diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java new file mode 100644 index 00000000..ad5896b8 --- /dev/null +++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java @@ -0,0 +1,269 @@ +package electroblob.wizardry.integration.jei; + +import com.google.common.collect.Streams; +import electroblob.wizardry.Wizardry; +import electroblob.wizardry.constants.Constants; +import electroblob.wizardry.constants.Element; +import electroblob.wizardry.inventory.ContainerArcaneWorkbench; +import electroblob.wizardry.item.*; +import electroblob.wizardry.registry.WizardryItems; +import electroblob.wizardry.registry.WizardryRecipes; +import electroblob.wizardry.spell.Spell; +import electroblob.wizardry.util.WandHelper; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IGuiItemStackGroup; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeCategory; +import mezz.jei.api.recipe.IRecipeCategoryRegistration; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.NonNullList; +import net.minecraft.util.ResourceLocation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * JEI recipe category implementation for all 'recipes' in the arcane workbench (of course, the arcane workbench + * doesn't have 'recipes' in the normal sense, but that's how they're displayed in JEI). + * + * @author Electroblob + * @since Wizardry 4.3 + */ +public class ArcaneWorkbenchRecipeCategory implements IRecipeCategory { + + static final String UID = "ebwizardry:arcane_workbench"; + static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/gui/container/arcane_workbench_jei_background.png"); + + static final int WIDTH = 166; + static final int HEIGHT = 126; + // Annoyingly, these seem to be for 18x18 slots rather than the actual highlighted area, unlike container slots + static final int CENTRE_SLOT_X = 74; + static final int CENTRE_SLOT_Y = 54; + static final int CRYSTAL_SLOT_X = 7; + static final int CRYSTAL_SLOT_Y = 91; + static final int UPGRADE_SLOT_X = 141; + static final int UPGRADE_SLOT_Y = 7; + static final int OUTPUT_SLOT_X = 141; + static final int OUTPUT_SLOT_Y = 101; + + private final IDrawable background; + + public ArcaneWorkbenchRecipeCategory(IRecipeCategoryRegistration registry){ + IGuiHelper helper = registry.getJeiHelpers().getGuiHelper(); + background = helper.createDrawable(TEXTURE, 0, 0, WIDTH, HEIGHT); + } + + @Override + public String getUid(){ + return UID; + } + + @Override + public String getTitle(){ + // JEI is client-side, so client classes can safely be used here + return I18n.format("integration.jei.category." + UID); + } + + @Override + public String getModName(){ + return Wizardry.NAME; + } + + @Override + public IDrawable getBackground(){ + return background; + } + + @Override + public void setRecipe(IRecipeLayout recipeLayout, ArcaneWorkbenchRecipe recipeWrapper, IIngredients ingredients){ + + // Okay, they're not technically *slots* but to all intents and purposes, that's how they behave + IGuiItemStackGroup slots = recipeLayout.getItemStacks(); + + List> inputs = ingredients.getInputs(VanillaTypes.ITEM); + List> outputs = ingredients.getOutputs(VanillaTypes.ITEM); + + ItemStack centreStack = inputs.get(inputs.size() - 2).get(0); + + int bookSlots = 0; + + if(centreStack.getItem() instanceof IWorkbenchItem){ + bookSlots = ((IWorkbenchItem)centreStack.getItem()).getSpellSlotCount(centreStack); + } + + // Slot initialisation + int i = 0; + + while(i < bookSlots){ + int x = CENTRE_SLOT_X + ContainerArcaneWorkbench.getBookSlotXOffset(i, bookSlots); + int y = CENTRE_SLOT_Y + ContainerArcaneWorkbench.getBookSlotYOffset(i, bookSlots); + slots.init(i++, true, x, y); + } + + // Add dummy slots for the hidden book slots so the transfer handler works correctly + // Sure, we COULD use an IRecipeTransferHandler, but this is far less effort! + while(i < ContainerArcaneWorkbench.CRYSTAL_SLOT){ + slots.init(i++, true, 0, 0); + } + + slots.init(i++, true, CRYSTAL_SLOT_X, CRYSTAL_SLOT_Y); + slots.init(i++, true, CENTRE_SLOT_X, CENTRE_SLOT_Y); + slots.init(i++, true, UPGRADE_SLOT_X, UPGRADE_SLOT_Y); + + slots.init(i++, false, OUTPUT_SLOT_X, OUTPUT_SLOT_Y); + + // Assign ingredients to slots + // The number of books we actually have is inputs.size() - 3, probably less than the number of book slots + for(int j = 0; j < Math.min(bookSlots, inputs.size() - 3); j++){ + slots.set(j, inputs.get(j)); + } + + slots.set(ContainerArcaneWorkbench.CRYSTAL_SLOT, inputs.get(inputs.size() - 3)); + slots.set(ContainerArcaneWorkbench.CENTRE_SLOT, inputs.get(inputs.size() - 2)); + slots.set(ContainerArcaneWorkbench.UPGRADE_SLOT, inputs.get(inputs.size() - 1)); + + for(int k = 0; k < outputs.size(); k++) slots.set(11 + k, outputs.get(k)); + + } + + /** Called to generate all of wizardry's arcane workbench 'recipes' for JEI. */ + public static Collection generateRecipes(){ + + List recipes = new ArrayList<>(); + + recipes.addAll(generateUpgradeRecipes()); // Probably nicest to have these first, they're the most useful + recipes.addAll(generateChargingRecipes()); + recipes.addAll(generateScrollRecipes()); + + return recipes; + + } + + private static Collection generateUpgradeRecipes(){ + + List recipes = new ArrayList<>(); + + List upgrades = new ArrayList<>(); + + for(Item item : Item.REGISTRY){ + if(item instanceof ItemArcaneTome || item instanceof ItemArmourUpgrade){ + NonNullList variants = NonNullList.create(); + item.getSubItems(item.getCreativeTab(), variants); + upgrades.addAll(variants); + } + } + + // Condense all special upgrades into one ingredient in an effort to reduce the number of separate recipes + List specialUpgrades = new ArrayList<>(); + + for(Item item : WandHelper.getSpecialUpgrades()){ + NonNullList variants = NonNullList.create(); + item.getSubItems(item.getCreativeTab(), variants); + specialUpgrades.addAll(variants); + } + + for(Item item : Item.REGISTRY){ + + if(item instanceof IWorkbenchItem){ + + ItemStack original = new ItemStack(item); + + for(ItemStack upgrade : upgrades){ + // Copy both input stacks to ignore any modifications to them during the upgrading process + ItemStack result = ((IWorkbenchItem)item).applyUpgrade(null, original.copy(), upgrade.copy()); + // It's only a valid 'recipe' if something actually changed + if(!ItemStack.areItemStacksEqual(original, result)){ + recipes.add(new ArcaneWorkbenchRecipe(original, Collections.emptyList(), Collections.emptyList(), + Collections.singletonList(upgrade), result)); + } + } + + List applicableSpecialUpgrades = new ArrayList<>(); + + for(ItemStack upgrade : specialUpgrades){ + // Copy both input stacks to ignore any modifications to them during the upgrading process + ItemStack result = ((IWorkbenchItem)item).applyUpgrade(null, original.copy(), upgrade.copy()); + // It's only a valid 'recipe' if something actually changed + if(!ItemStack.areItemStacksEqual(original, result)){ + applicableSpecialUpgrades.add(upgrade); + } + } + + recipes.add(new ArcaneWorkbenchRecipe(original, Collections.emptyList(), Collections.emptyList(), + applicableSpecialUpgrades, original)); // Wands with special upgrades look no different anyway + + } + } + + return recipes; + + } + + private static Collection generateChargingRecipes(){ + + List recipes = new ArrayList<>(); + + List crystals = new ArrayList<>(); + for(int meta = 0; meta < Element.values().length; meta++) crystals.add(new ItemStack(WizardryItems.magic_crystal, 1, meta)); + List shard = Collections.singletonList(new ItemStack(WizardryItems.crystal_shard)); + List grandCrystal = Collections.singletonList(new ItemStack(WizardryItems.grand_crystal)); + + for(Item chargeable : WizardryRecipes.getChargeableItems()){ + + if(!(chargeable instanceof IManaStoringItem)) throw new IllegalArgumentException("Item to be charged must be an instance of IManaStoringItem"); + + ItemStack input = new ItemStack(chargeable); + ((IManaStoringItem)chargeable).setMana(input, 0); + + ItemStack result = new ItemStack(chargeable); + ((IManaStoringItem)chargeable).setMana(result, Constants.MANA_PER_CRYSTAL); + recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), crystals, Collections.emptyList(), result)); + + result = new ItemStack(chargeable); + ((IManaStoringItem)chargeable).setMana(result, Constants.MANA_PER_SHARD); + recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), shard, Collections.emptyList(), result)); + + result = new ItemStack(chargeable); + ((IManaStoringItem)chargeable).setMana(result, Constants.GRAND_CRYSTAL_MANA); + recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), grandCrystal, Collections.emptyList(), result)); + } + + return recipes; + + } + + private static Collection generateScrollRecipes(){ + + List recipes = new ArrayList<>(); + + ItemStack blankScroll = new ItemStack(WizardryItems.blank_scroll); + + // We need not make people register these manually since spells already have control over what they can be put on + List spellBooks = Streams.stream(Item.REGISTRY).filter(i -> i instanceof ItemSpellBook).collect(Collectors.toList()); + List scrolls = Streams.stream(Item.REGISTRY).filter(i -> i instanceof ItemScroll).collect(Collectors.toList()); + + for(Spell spell : Spell.getAllSpells()){ + for(Item spellBook : spellBooks){ + for(Item scroll : scrolls){ + if(spell.applicableForItem(spellBook) && spell.applicableForItem(scroll)){ + List books = Collections.singletonList(new ItemStack(WizardryItems.spell_book, 1, spell.metadata())); + ItemStack result = new ItemStack(WizardryItems.scroll, 1, spell.metadata()); + recipes.add(new ArcaneWorkbenchRecipe(blankScroll, books, spell.getCost(), Collections.emptyList(), result)); + } + } + } + } + + return recipes; + + } + +} diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java new file mode 100644 index 00000000..a911bd1a --- /dev/null +++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java @@ -0,0 +1,52 @@ +package electroblob.wizardry.integration.jei; + +import electroblob.wizardry.inventory.ContainerArcaneWorkbench; +import mezz.jei.api.recipe.transfer.IRecipeTransferInfo; +import net.minecraft.inventory.Slot; + +import java.util.ArrayList; +import java.util.List; + +/** + * JEI recipe transfer handler for the arcane workbench. This differs from a standard recipe handler in that it returns + * the active bookshelf (virtual) slots from the workbench as part of the inventory slots, and does not require complete + * sets of ingredients. + * + * @author Electroblob + * @since Wizardry 4.3 + */ +public class ArcaneWorkbenchTransferHandler implements IRecipeTransferInfo { + + @Override + public Class getContainerClass(){ + return ContainerArcaneWorkbench.class; + } + + @Override + public String getRecipeCategoryUid(){ + return ArcaneWorkbenchRecipeCategory.UID; + } + + @Override + public boolean canHandle(ContainerArcaneWorkbench container){ + return true; + } + + @Override + public boolean requireCompleteSets(){ + return false; // Arcane workbench maths doesn't work like crafting maths! + } + + @Override + public List getRecipeSlots(ContainerArcaneWorkbench container){ + return container.inventorySlots.subList(0, ContainerArcaneWorkbench.UPGRADE_SLOT + 1); + } + + @Override + public List getInventorySlots(ContainerArcaneWorkbench container){ + List slots = new ArrayList<>(container.inventorySlots.subList(ContainerArcaneWorkbench.UPGRADE_SLOT + 1, ContainerArcaneWorkbench.UPGRADE_SLOT + 37)); + slots.addAll(container.getBookshelfSlots()); + return slots; + } + +} diff --git a/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java b/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java new file mode 100644 index 00000000..a5b050f2 --- /dev/null +++ b/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java @@ -0,0 +1,56 @@ +package electroblob.wizardry.integration.jei; + +import electroblob.wizardry.Wizardry; +import electroblob.wizardry.item.IConjuredItem; +import electroblob.wizardry.registry.WizardryBlocks; +import mezz.jei.api.IModPlugin; +import mezz.jei.api.IModRegistry; +import mezz.jei.api.ISubtypeRegistry; +import mezz.jei.api.JEIPlugin; +import mezz.jei.api.ingredients.IIngredientBlacklist; +import mezz.jei.api.recipe.IRecipeCategoryRegistration; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +@JEIPlugin +public class WizardryJEIPlugin implements IModPlugin { + + @Override + public void registerCategories(IRecipeCategoryRegistration registry){ + + if(!Wizardry.settings.jeiIntegration) return; + + registry.addRecipeCategories(new ArcaneWorkbenchRecipeCategory(registry)); + } + + @Override + public void register(IModRegistry registry){ + + if(!Wizardry.settings.jeiIntegration) return; + + // Add arcane workbench as the item required to use arcane workbench recipes + registry.addRecipeCatalyst(new ItemStack(WizardryBlocks.arcane_workbench), ArcaneWorkbenchRecipeCategory.UID); + + registry.addRecipes(ArcaneWorkbenchRecipeCategory.generateRecipes(), ArcaneWorkbenchRecipeCategory.UID); + + registry.getRecipeTransferRegistry().addRecipeTransferHandler(new ArcaneWorkbenchTransferHandler()); + + // Hide conjured items from JEI + IIngredientBlacklist blacklist = registry.getJeiHelpers().getIngredientBlacklist(); + for(Item item : Item.REGISTRY){ + if(item instanceof IConjuredItem) blacklist.addIngredientToBlacklist(new ItemStack(item)); + } + + } + + @Override + public void registerItemSubtypes(ISubtypeRegistry subtypeRegistry){ + + if(!Wizardry.settings.jeiIntegration) return; + + // TODO: Probably need to distinguish between normal/legendary armour, at the very least + // ... that being said, we may well be changing that mechanic anyway + + } + +} diff --git a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java index 3ef290d7..771d3146 100644 --- a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java +++ b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java @@ -197,12 +197,8 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl // Show however many spell book slots are necessary for(int i = 0; i < spellSlots; i++){ - - float angle = i * (2 * (float)Math.PI)/spellSlots; - int x = centreX + Math.round(SLOT_RADIUS * MathHelper.sin(angle)); - // -cos because +y is downwards - int y = centreY + Math.round(SLOT_RADIUS * -MathHelper.cos(angle)); - + int x = centreX + getBookSlotXOffset(i, spellSlots); + int y = centreY + getBookSlotYOffset(i, spellSlots); showSlot(i, x, y); } @@ -216,6 +212,20 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl } } + /** Returns the x offset (relative to the central slot) of the ith book slot when the total number of book slots is + * equal to {@code bookSlotCount}. */ + public static int getBookSlotXOffset(int i, int bookSlotCount){ + float angle = i * (2 * (float)Math.PI) / bookSlotCount; + return Math.round(SLOT_RADIUS * MathHelper.sin(angle)); + } + + /** Returns the y offset (relative to the central slot) of the ith book slot when the total number of book slots is + * equal to {@code bookSlotCount}. */ + public static int getBookSlotYOffset(int i, int bookSlotCount){ + float angle = i * (2 * (float)Math.PI) / bookSlotCount; + return Math.round(SLOT_RADIUS * -MathHelper.cos(angle)); // -cos because +y is downwards + } + // FIXME: Shift-clicking a stack of special upgrades when in the arcane workbench causes the whole stack to be // transferred when it should be just one (this is a bug with vanilla as well - try putting a stack of // bottles into a brewing stand). I have at least made it so only one gets used now, so it has no impact on diff --git a/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java b/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java index e85fd314..146246e3 100644 --- a/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java +++ b/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java @@ -5,6 +5,8 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; +import javax.annotation.Nullable; + /** * Items that implement this interface may be placed in the central slot of the arcane workbench as long as * {@link IWorkbenchItem#canPlace(ItemStack)} returns true. The number of spell book slots displayed is also specified @@ -37,7 +39,7 @@ public interface IWorkbenchItem { * Can be 0, but must not be negative. */ int getSpellSlotCount(ItemStack stack); - + /** * Called when this item is in the central slot of an arcane workbench and the apply button is pressed. Items must * implement this method to define what happens when the apply button is pressed. Note that {@link SpellBindEvent} @@ -60,5 +62,20 @@ public interface IWorkbenchItem { * @return True if the workbench tooltip should be shown, false if not. */ boolean showTooltip(ItemStack stack); + + /** + * Applies the given upgrade to this wand. This method is responsible for all checks including tier, progression, + * upgrade stack limits, etc. Subclasses are responsible for calling this (usually from + * {@link IWorkbenchItem#onApplyButtonPressed(EntityPlayer, Slot, Slot, Slot, Slot[])}), but it has been extracted + * as an interface method here for use by JEI 'recipes'. + * @param player The player doing the upgrading, or null during JEI recipe lookup (mainly used for advancements) + * @param stack The stack being upgraded (it is guaranteed that {@code this == stack.getItem()}) + * @param upgrade The upgrade item stack being applied. This method is responsible for consuming it! + * @return The resulting upgraded wand stack. In many cases, this is simply the input {@code stack}, which has + * had its NBT modified. If the given upgrade cannot be applied, simply return the input {@code stack}. + */ + default ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack stack, ItemStack upgrade){ + return stack; + } } diff --git a/src/main/java/electroblob/wizardry/item/ItemWand.java b/src/main/java/electroblob/wizardry/item/ItemWand.java index c37878d4..8272098a 100644 --- a/src/main/java/electroblob/wizardry/item/ItemWand.java +++ b/src/main/java/electroblob/wizardry/item/ItemWand.java @@ -45,6 +45,7 @@ import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import javax.annotation.Nullable; import java.util.List; import java.util.Random; @@ -625,97 +626,110 @@ public class ItemWand extends Item implements IWorkbenchItem, ISpellCastingItem, } @Override - public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){ - - boolean changed = false; - + public ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack wand, ItemStack upgrade){ + // Upgrades wand if necessary. Damage is copied, preserving remaining durability, // and also the entire NBT tag compound. - if(upgrade.getStack().getItem() == WizardryItems.arcane_tome){ + if(upgrade.getItem() == WizardryItems.arcane_tome){ - Tier tier = Tier.values()[upgrade.getStack().getItemDamage()]; + Tier tier = Tier.values()[upgrade.getItemDamage()]; // Checks the wand upgrade is for the tier above the wand's tier, and that either the wand has enough // progression or the player is in creative mode. - // It is guaranteed that: this == centre.getStack().getItem() - if((player.isCreative() || Wizardry.settings.legacyWandLevelling - || WandHelper.getProgression(centre.getStack()) >= tier.progression) + if((player == null || player.isCreative() || Wizardry.settings.legacyWandLevelling + || WandHelper.getProgression(wand) >= tier.progression) && tier.ordinal() - 1 == this.tier.ordinal()){ // We're not carrying over excess progression for now, but if we do want to, this is how // if(!Wizardry.settings.legacyWandLevelling){ // // Easy way to carry excess progression over to the new stack -// WandHelper.setProgression(centre.getStack(), WandHelper.getProgression(centre.getStack()) - tier.progression); +// WandHelper.setProgression(wand, WandHelper.getProgression(wand) - tier.progression); // } - WandHelper.setProgression(centre.getStack(), 0); + WandHelper.setProgression(wand, 0); ItemStack newWand = new ItemStack(WizardryItems.getWand(tier, this.element)); - newWand.setTagCompound(centre.getStack().getTagCompound()); + newWand.setTagCompound(wand.getTagCompound()); // This needs to be done after copying the tag compound so the mana capacity for the new wand // takes storage upgrades into account // Note the usage of the new wand item and not 'this' to ensure the correct capacity is used - ((IManaStoringItem)newWand.getItem()).setMana(newWand, this.getMana(centre.getStack())); + ((IManaStoringItem)newWand.getItem()).setMana(newWand, this.getMana(wand)); - centre.putStack(newWand); - upgrade.decrStackSize(1); - - changed = true; + upgrade.shrink(1); + + return newWand; } - }else if(WandHelper.isWandUpgrade(upgrade.getStack().getItem())){ + }else if(WandHelper.isWandUpgrade(upgrade.getItem())){ // Special upgrades - Item specialUpgrade = upgrade.getStack().getItem(); + Item specialUpgrade = upgrade.getItem(); - if(WandHelper.getTotalUpgrades(centre.getStack()) < this.tier.upgradeLimit - && WandHelper.getUpgradeLevel(centre.getStack(), specialUpgrade) < Constants.UPGRADE_STACK_LIMIT){ + if(WandHelper.getTotalUpgrades(wand) < this.tier.upgradeLimit + && WandHelper.getUpgradeLevel(wand, specialUpgrade) < Constants.UPGRADE_STACK_LIMIT){ // Used to preserve existing mana when upgrading storage rather than creating free mana. - int prevMana = this.getMana(centre.getStack()); + int prevMana = this.getMana(wand); - WandHelper.applyUpgrade(centre.getStack(), specialUpgrade); + WandHelper.applyUpgrade(wand, specialUpgrade); // Special behaviours for specific upgrades if(specialUpgrade == WizardryItems.storage_upgrade){ - this.setMana(centre.getStack(), prevMana); - + this.setMana(wand, prevMana); + }else if(specialUpgrade == WizardryItems.attunement_upgrade){ - int newSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(centre.getStack(), + int newSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(wand, WizardryItems.attunement_upgrade); - - Spell[] spells = WandHelper.getSpells(centre.getStack()); + + Spell[] spells = WandHelper.getSpells(wand); Spell[] newSpells = new Spell[newSlotCount]; for(int i = 0; i < newSpells.length; i++){ newSpells[i] = i < spells.length && spells[i] != null ? spells[i] : Spells.none; } - WandHelper.setSpells(centre.getStack(), newSpells); + WandHelper.setSpells(wand, newSpells); - int[] cooldowns = WandHelper.getCooldowns(centre.getStack()); + int[] cooldowns = WandHelper.getCooldowns(wand); int[] newCooldowns = new int[newSlotCount]; if(cooldowns.length > 0){ System.arraycopy(cooldowns, 0, newCooldowns, 0, cooldowns.length); } - WandHelper.setCooldowns(centre.getStack(), newCooldowns); + WandHelper.setCooldowns(wand, newCooldowns); } - upgrade.decrStackSize(1); - WizardryAdvancementTriggers.special_upgrade.triggerFor(player); + upgrade.shrink(1); - if(WandHelper.getTotalUpgrades(centre.getStack()) == Tier.MASTER.upgradeLimit){ - WizardryAdvancementTriggers.max_out_wand.triggerFor(player); + if(player != null){ + + WizardryAdvancementTriggers.special_upgrade.triggerFor(player); + + if(WandHelper.getTotalUpgrades(wand) == Tier.MASTER.upgradeLimit){ + WizardryAdvancementTriggers.max_out_wand.triggerFor(player); + } } - - changed = true; + } } + return wand; + } + + @Override + public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){ + + boolean changed = false; // Used for advancements + + if(upgrade.getHasStack()){ + ItemStack original = centre.getStack().copy(); + centre.putStack(this.applyUpgrade(player, centre.getStack(), upgrade.getStack())); + changed = ItemStack.areItemStacksEqual(centre.getStack(), original); + } + // Reads NBT spell metadata array to variable, edits this, then writes it back to NBT. // Original spells are preserved; if a slot is left empty the existing spell binding will remain. // Accounts for spells which cannot be applied because they are above the wand's tier; these spells diff --git a/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java b/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java index e70301c1..9b5ff051 100644 --- a/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java +++ b/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java @@ -38,6 +38,7 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import javax.annotation.Nullable; import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -247,25 +248,35 @@ public class ItemWizardArmour extends ItemArmor implements IWorkbenchItem, IMana return 0; // Doesn't have any spell slots! } + @Override + public ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack stack, ItemStack upgrade){ + + // Applies legendary upgrade + if(upgrade.getItem() == WizardryItems.armour_upgrade){ + + if(!stack.hasTagCompound()){ + stack.setTagCompound(new NBTTagCompound()); + } + + if(!stack.getTagCompound().hasKey("legendary")){ + stack.getTagCompound().setBoolean("legendary", true); + upgrade.shrink(1); + if(player != null) WizardryAdvancementTriggers.legendary.triggerFor(player); + } + } + + return stack; + } + @Override public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){ boolean changed = false; - - // Applies legendary upgrade - if(upgrade.getStack().getItem() == WizardryItems.armour_upgrade){ - - if(!centre.getStack().hasTagCompound()){ - centre.getStack().setTagCompound(new NBTTagCompound()); - } - - if(!centre.getStack().getTagCompound().hasKey("legendary")){ - - centre.getStack().getTagCompound().setBoolean("legendary", true); - upgrade.decrStackSize(1); - WizardryAdvancementTriggers.legendary.triggerFor(player); - changed = true; - } + + if(upgrade.getHasStack()){ + ItemStack original = centre.getStack().copy(); + centre.putStack(this.applyUpgrade(player, centre.getStack(), upgrade.getStack())); + changed = ItemStack.areItemStacksEqual(centre.getStack(), original); } // Charges armour by appropriate amount diff --git a/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java b/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java index 869088ff..5d2b5e7b 100644 --- a/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java +++ b/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java @@ -14,8 +14,9 @@ import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.registries.IForgeRegistry; -import java.util.LinkedList; -import java.util.Queue; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Class responsible for defining and registering wizardry's non-JSON recipes (i.e. smelting recipes and dynamic @@ -29,14 +30,27 @@ public final class WizardryRecipes { private WizardryRecipes(){} // No instances! - private static final Queue chargingRecipeQueue = new LinkedList<>(); + private static final List chargeableItems = new ArrayList<>(); + + private static boolean registered; /** Adds the given item to the list of items that can be charged using mana flasks. Dynamic charging recipes * will be added for these items during {@code RegistryEvent.Register}. The item must implement * {@link IManaStoringItem} for the recipes to work correctly. This method should be called from the item's * constructor. */ public static void addToManaFlaskCharging(Item item){ - chargingRecipeQueue.offer(item); + + if(registered){ + Wizardry.logger.warn("Tried to add an item to mana flask charging after it was registered, this will do nothing!"); + return; + } + + chargeableItems.add(item); + } + + /** Returns an unmodifiable view of all registered items that can be charged with mana flasks. */ + public static List getChargeableItems(){ + return Collections.unmodifiableList(chargeableItems); } /** Now only deals with the dynamic crafting recipes and the smelting recipes. */ @@ -49,11 +63,7 @@ public final class WizardryRecipes { // Mana flask recipes - Item chargeable; - - while(!chargingRecipeQueue.isEmpty()){ - // Use remove() and not poll() because the queue shouldn't be empty in here - chargeable = chargingRecipeQueue.remove(); + for(Item chargeable : chargeableItems){ registry.register(new RecipeRechargeWithFlask(chargeable, (ItemManaFlask)WizardryItems.small_mana_flask) .setRegistryName(new ResourceLocation(Wizardry.MODID, "recipes/small_flask_" + chargeable.getRegistryName().getPath()))); @@ -65,6 +75,9 @@ public final class WizardryRecipes { .setRegistryName(new ResourceLocation(Wizardry.MODID, "recipes/large_flask_" + chargeable.getRegistryName().getPath()))); } + + registered = true; + } } diff --git a/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png b/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd25c580f7bda50feef7b665b6c2b43d7cd8bf8 GIT binary patch literal 10281 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX$%aKid7*IB|(Yh3I#>^X_+~x z3MG{VsS2qTnQ06R6}R5b3ZJH8KFRg}#;!=FEfIWE<{DeiczV9JBZYI5#;$a`-ILaw z>FFw9_pwPVDF6R_|JlFd^;x}FpI)kx-CFhU$wc=#e=huee*Ss&ho67{-k$&A>v#Y4 zyYubc_Nnh%e%}7d@2@LQzJB_fJ?dXo?cYDUvVVVeeiJYL>+QLCai3kqPukai|GJ)g z>5od=n!oRkU4NeTFVQ;w|NXC}H$VSee*gbG>4O%nua~}f+J7$h zh|GVKy=;nB{q!&D8;k43_SkpNogcq``F!VxHGiv||Hk(0i{pQKePgxYuR_;{zvtHf zYTxVh-TvQC#j>>Jv+tgNm$uGj)voovf45EjE}S3JVzjqEbf46J@w&YC*&nXne597# zo};&K8@IB{qFlvx;j|3}_rJ@Ud7t@s?AUjSa-a8G=S9gBPUaC>xISUy(usZEbLZr2 znpR<17wui~UG{z5Y+t#0ruRLK-L2D)->=;nzxT)cn%dnHW_p>k+~0q+tJL+X$?1S+ zU(DYtDIVOH6ctxm+Y3hf)+b*3hepGuj{ki>G!COCf zF(^K|*Lt+Yg{_8nWs6iz_r_KS8S9@j*VqJo>*i?*dc=9|1n*V94M^7cKwb~|tXW531MbRxGUU7b}bZMHx4t@djB zblpFC#r1Qyewb>Ss> zC7NC;*$Xu+)ish+4DJhbx90EsJ#Xj!GqbgS#-ED*v-ynW(s%!ri~UyqK6OX_^SACb zbAL*x?qylYEgC$ZCt-6;`kO!Oa{pg`_neZ)Q@df8?ejfH)Qb+En#-i^b#DIU?Lm9K zpH1GrBg|-*_3_x-JUTfk{$ju8)S3q5U)}ll?%MeIU&QQk&1aq7-sjD6XhEm@SrPZ# zjTLpi?XzxtUY7jwb+=aQI`3pF?Y}fwZg1-63yWg|?CxHTJeB+K zR%T!+i{AGcyYY_9uQ%EqDBwmXcStzST)w?6Xn5P~#${ z%SB&QqfMl?O!f7@YnbPMdB@XTYyB1{p0rMt%~{Tvwxv4!RPWm>$Ly~jnzq*2efEzp zMVDV4ykr}s)%yS6zujCvZ+pM{z5nB7&zMOPp$pf^U-4cvQ!49O&O3qc9!Hi`zxuzX zdinm(5z7wwmHd%5*gMTYn_v5Z)}!) zTX!v*xUB0^)Y@YzXQbQ$gLfScXD-TmGATWzd-?JU+wS>kB^7u63p(GE`+_e!{Yb>w z4#o#5)B3l$Ms4}@`tg}9f?uY6zuT1@eEQa>XHutci>Jtj+|vDfBhI0Kt5sBPn}ce8 z+-~k2Cy&`KJnMHMSniLnPMM6^w$x1Bxc`~e154bZ38gPF}y`Yt=VtsY~X}et_S*n>!H^^Sv$zrk8H|$1Y z(Wzq_4|wVdg)6MQHLLQs?SqL7≶rRB|g!#nVxJ-p{_O0OpNC=Ps@kySHz%^p;tn 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