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 00000000..9bd25c58 Binary files /dev/null and b/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png differ