diff --git a/src/main/java/electroblob/wizardry/block/BlockBookshelf.java b/src/main/java/electroblob/wizardry/block/BlockBookshelf.java index b4e23861..9fc4c90a 100644 --- a/src/main/java/electroblob/wizardry/block/BlockBookshelf.java +++ b/src/main/java/electroblob/wizardry/block/BlockBookshelf.java @@ -50,16 +50,6 @@ public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvid return new BlockStateContainer.Builder(this).add(FACING).add(BOOKS).build(); } - @Override - public boolean isOpaqueCube(IBlockState state){ - return false; - } - - @Override - public boolean isFullCube(IBlockState state){ - return false; - } - @Override public BlockFaceShape getBlockFaceShape(IBlockAccess world, IBlockState state, BlockPos pos, EnumFacing face){ return state.getValue(FACING).getAxis() == face.getAxis() ? BlockFaceShape.UNDEFINED : BlockFaceShape.SOLID; diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java index 7d8be0a3..28901598 100644 --- a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java +++ b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java @@ -6,6 +6,8 @@ import electroblob.wizardry.constants.Element; import electroblob.wizardry.constants.Tier; import electroblob.wizardry.data.SpellGlyphData; import electroblob.wizardry.data.WizardData; +import electroblob.wizardry.inventory.ContainerArcaneWorkbench; +import electroblob.wizardry.inventory.SlotBookList; import electroblob.wizardry.item.IManaStoringItem; import electroblob.wizardry.item.ISpellCastingItem; import electroblob.wizardry.item.IWorkbenchItem; @@ -14,23 +16,25 @@ import electroblob.wizardry.packet.PacketControlInput; import electroblob.wizardry.packet.WizardryPacketHandler; import electroblob.wizardry.registry.WizardrySounds; import electroblob.wizardry.spell.Spell; -import electroblob.wizardry.inventory.ContainerArcaneWorkbench; import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench; import electroblob.wizardry.util.WandHelper; import net.minecraft.client.Minecraft; import net.minecraft.client.audio.PositionedSoundRecord; import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.GlStateManager.DestFactor; import net.minecraft.client.renderer.GlStateManager.SourceFactor; import net.minecraft.client.resources.I18n; import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.inventory.ClickType; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.Slot; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.MathHelper; import net.minecraft.util.text.TextFormatting; import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraftforge.fml.common.Mod; @@ -38,6 +42,10 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.relauncher.Side; import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import java.io.IOException; +import java.util.Locale; @Mod.EventBusSubscriber(Side.CLIENT) public class GuiArcaneWorkbench extends GuiContainer { @@ -46,12 +54,15 @@ public class GuiArcaneWorkbench extends GuiContainer { public static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/arcane_workbench.png"); - private IInventory playerInventory; + private InventoryPlayer playerInventory; private IInventory arcaneWorkbenchInventory; + private ContainerArcaneWorkbench arcaneWorkbenchContainer; - private static final int TOOLTIP_WIDTH = 164; + private static final int TOOLTIP_WIDTH = 144; private static final int TOOLTIP_TEXT_INSET = 6; + private static final int BOOKSHELF_UI_WIDTH = 122; + /** We report the actual size of the GUI to Minecraft when a wand is in so JEI doesn't overdraw it. * For calculations, we use the size without the tooltip, which is stored in this constant. */ private static final int MAIN_GUI_WIDTH = 176; @@ -61,25 +72,69 @@ public class GuiArcaneWorkbench extends GuiContainer { private static final int RUNE_WIDTH = 100; private static final int RUNE_HEIGHT = 100; + private static final int SCROLL_BAR_LEFT = 102; + private static final int SCROLL_BAR_TOP = 34; + private static final int SCROLL_BAR_WIDTH = 12; + private static final int SCROLL_BAR_HEIGHT = 178; + private static final int SCROLL_HANDLE_HEIGHT = 15; + private static final int PROGRESSION_BAR_WIDTH = 152; private static final int PROGRESSION_BAR_HEIGHT = 3; private static final int HALO_DIAMETER = 156; private static final int TEXTURE_WIDTH = 512; - private static final int TEXTURE_HEIGHT = 256; + private static final int TEXTURE_HEIGHT = 512; private int animationTimer = 0; private static final int ANIMATION_DURATION = 20; + private GuiTextField searchField; + private boolean searchNeedsClearing; + + private float scroll = 0; + private boolean scrolling = false; + public GuiArcaneWorkbench(InventoryPlayer invPlayer, TileEntityArcaneWorkbench entity){ super(new ContainerArcaneWorkbench(invPlayer, entity)); + this.arcaneWorkbenchContainer = (ContainerArcaneWorkbench)inventorySlots; this.playerInventory = invPlayer; this.arcaneWorkbenchInventory = entity; xSize = MAIN_GUI_WIDTH; ySize = 220; } + @Override + public void initGui(){ + + this.mc.player.openContainer = this.inventorySlots; + this.guiLeft = (this.width - this.xSize) / 2; + this.guiTop = (this.height - this.ySize) / 2; + + Keyboard.enableRepeatEvents(true); + + this.buttonList.clear(); + this.buttonList.add(this.applyBtn = new GuiButtonApply(0, this.width / 2 + 64, this.height / 2 + 3)); + this.buttonList.add(new GuiButtonSort(1, this.guiLeft - 44, this.guiTop + 8, ContainerArcaneWorkbench.SortType.TIER)); + this.buttonList.add(new GuiButtonSort(2, this.guiLeft - 31, this.guiTop + 8, ContainerArcaneWorkbench.SortType.ELEMENT)); + this.buttonList.add(new GuiButtonSort(3, this.guiLeft - 18, this.guiTop + 8, ContainerArcaneWorkbench.SortType.ALPHABETICAL)); + + this.searchField = new GuiTextField(0, this.fontRenderer, this.guiLeft - 113, this.guiTop + 22, 104, this.fontRenderer.FONT_HEIGHT); + this.searchField.setMaxStringLength(50); + this.searchField.setEnableBackgroundDrawing(false); + this.searchField.setVisible(true); + this.searchField.setTextColor(16777215); + this.searchField.setCanLoseFocus(false); + this.searchField.setFocused(true); + + } + + @Override + public void onGuiClosed(){ + super.onGuiClosed(); + Keyboard.enableRepeatEvents(false); + } + // Huh, didn't realise this method existed. Pretty neat. @Override public void updateScreen(){ @@ -93,18 +148,33 @@ public class GuiArcaneWorkbench extends GuiContainer { GlStateManager.color(1, 1, 1, 1); // Just in case + boolean mouseHeld = Mouse.isButtonDown(0); + + if(!scrolling && mouseHeld && getMaxScrollRows() > 0 && isPointInRegion(SCROLL_BAR_LEFT - BOOKSHELF_UI_WIDTH, + SCROLL_BAR_TOP, SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT, mouseX, mouseY)){ + scrolling = true; + } + + if(!mouseHeld || getMaxScrollRows() == 0) scrolling = false; + + if(scrolling){ + scroll = MathHelper.clamp((float)(mouseY - SCROLL_BAR_TOP - SCROLL_HANDLE_HEIGHT/2 - guiTop) + /(SCROLL_BAR_HEIGHT - SCROLL_HANDLE_HEIGHT), 0, 1); + arcaneWorkbenchContainer.scrollTo((int)(getMaxScrollRows() * scroll + 0.5f)); + } + Slot slot = this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT); // Tests if there is a wand in the workbench and edits the positioning accordingly if(slot.getHasStack() && slot.getStack().getItem() instanceof IWorkbenchItem && ((IWorkbenchItem)slot.getStack().getItem()).showTooltip(slot.getStack())){ xSize = MAIN_GUI_WIDTH + TOOLTIP_WIDTH; - guiLeft = (this.width - this.xSize) / 2; - this.applyBtn.x = (this.width - TOOLTIP_WIDTH) / 2 + 64; +// guiLeft = (this.width - this.xSize) / 2; +// this.applyBtn.x = (this.width - TOOLTIP_WIDTH) / 2 + 64; }else{ xSize = MAIN_GUI_WIDTH; - guiLeft = (this.width - this.xSize) / 2; - this.applyBtn.x = this.width / 2 + 64; +// guiLeft = (this.width - this.xSize) / 2; +// this.applyBtn.x = this.width / 2 + 64; } this.applyBtn.enabled = slot.getHasStack(); @@ -218,6 +288,15 @@ public class GuiArcaneWorkbench extends GuiContainer { } } + // Bookshelf interface + DrawingUtils.drawTexturedRect(guiLeft - BOOKSHELF_UI_WIDTH, guiTop, 0, 256, BOOKSHELF_UI_WIDTH, ySize, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + // Scroll bar + DrawingUtils.drawTexturedRect(guiLeft - BOOKSHELF_UI_WIDTH + SCROLL_BAR_LEFT, + guiTop + SCROLL_BAR_TOP + (int)(scroll * (SCROLL_BAR_HEIGHT - SCROLL_HANDLE_HEIGHT) + 0.5f), + getMaxScrollRows() > 0 ? 30 : 30 + SCROLL_BAR_WIDTH, 476, + SCROLL_BAR_WIDTH, SCROLL_HANDLE_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); + // Tooltip only drawn if there is a wand if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getHasStack()){ @@ -302,11 +381,16 @@ public class GuiArcaneWorkbench extends GuiContainer { null); x += 18; GlStateManager.disableDepth(); + GlStateManager.disableLighting(); // Whyyyyyy? } } } } + this.searchField.drawTextBox(); // Easier to do this last, then we don't need to re-bind the GUI texture twice + + GlStateManager.color(1, 1, 1, 1); + Minecraft.getMinecraft().renderEngine.bindTexture(texture); // Fixes the bug that caused the slot highlight to render opaque. I don't know why it works, it just works! @@ -324,6 +408,7 @@ public class GuiArcaneWorkbench extends GuiContainer { : I18n.format(this.arcaneWorkbenchInventory.getName()), 8, 6, 4210752); this.fontRenderer.drawString(this.playerInventory.hasCustomName() ? this.playerInventory.getName() : I18n.format(this.playerInventory.getName()), 8, this.ySize - 96 + 2, 4210752); + this.fontRenderer.drawString(I18n.format("container." + Wizardry.MODID + ":arcane_workbench.bookshelves"), 8 - BOOKSHELF_UI_WIDTH, 6, 4210752); if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getHasStack()){ @@ -421,28 +506,18 @@ public class GuiArcaneWorkbench extends GuiContainer { } } } + + this.buttonList.forEach(b -> b.drawButtonForegroundLayer(mouseX, mouseY)); } - @Override - public void initGui(){ - this.mc.player.openContainer = this.inventorySlots; - this.guiLeft = (this.width - this.xSize) / 2; - this.guiTop = (this.height - this.ySize) / 2; - Keyboard.enableRepeatEvents(true); - this.buttonList.clear(); - this.buttonList.add(this.applyBtn = new GuiButtonApply(0, this.width / 2 + 64, this.height / 2 + 3)); - } - - @Override - public void onGuiClosed(){ - super.onGuiClosed(); - Keyboard.enableRepeatEvents(false); - } + // Controls @Override protected void actionPerformed(GuiButton button){ + if(button.enabled){ - if(button.id == 0){ + + if(button == applyBtn){ // Packet building IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.APPLY_BUTTON); WizardryPacketHandler.net.sendToServer(msg); @@ -452,10 +527,106 @@ public class GuiArcaneWorkbench extends GuiContainer { // Animation animationTimer = 20; } + + if(button instanceof GuiButtonSort) this.arcaneWorkbenchContainer.setSortType(((GuiButtonSort)button).sortType); } } - private class GuiButtonApply extends GuiButton { + private int getMaxScrollRows(){ + return Math.max(0, MathHelper.ceil((float)arcaneWorkbenchContainer.getActiveBookshelfSlots().size() + / ContainerArcaneWorkbench.BOOKSHELF_SLOTS_X) - ContainerArcaneWorkbench.BOOKSHELF_SLOTS_Y); + } + + @Override + public void handleMouseInput() throws IOException{ + + super.handleMouseInput(); + int scrollDist = -Mouse.getEventDWheel(); + + if(scrollDist != 0 && getMaxScrollRows() > 0){ + + if(scrollDist > 0) this.scroll += 1f / getMaxScrollRows(); + if(scrollDist < 0) this.scroll -= 1f / getMaxScrollRows(); + + scroll = MathHelper.clamp(scroll, 0, 1); + + arcaneWorkbenchContainer.scrollTo((int)(scroll * getMaxScrollRows() + 0.5f)); + } + } + + @Override + protected void handleMouseClick(Slot slot, int slotId, int mouseButton, ClickType type){ + + searchNeedsClearing = true; + + // Click type behaves weirdly, don't use it! Query the item stack held by the cursor instead + if(slot instanceof SlotBookList && ((SlotBookList)slot).hasDelegate() && playerInventory.getItemStack().isEmpty()){ + // If no item is currently being moved, divert book list slots to send the virtual slot through instead + // See explanation in ContainerArcaneWorkbench + Slot virtualSlot = ((SlotBookList)slot).getDelegate(); + super.handleMouseClick(virtualSlot, virtualSlot.slotNumber, mouseButton, type); + }else{ + super.handleMouseClick(slot, slotId, mouseButton, type); + } + + arcaneWorkbenchContainer.updateActiveBookshelfSlots(); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + + if(this.searchNeedsClearing){ + this.searchNeedsClearing = false; + this.searchField.setText(""); + } + + if(this.searchField.textboxKeyTyped(typedChar, keyCode)){ + arcaneWorkbenchContainer.setSearchText(searchField.getText().toLowerCase(Locale.ROOT)); + }else{ + super.keyTyped(typedChar, keyCode); + } + } + + // Nested classes + + private class GuiButtonSort extends GuiButton { + + private final ContainerArcaneWorkbench.SortType sortType; + + public GuiButtonSort(int id, int x, int y, ContainerArcaneWorkbench.SortType sortType){ + super(id, x, y, 10, 10, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.sort_" + sortType.name)); + this.sortType = sortType; + } + + @Override + public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){ + + if(this.visible){ + + // Whether the button is highlighted + this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height; + + int k = 0; + int l = 476 + this.sortType.ordinal() * this.height; + + if(sortType == GuiArcaneWorkbench.this.arcaneWorkbenchContainer.getSortType()){ + k += this.width; + if(GuiArcaneWorkbench.this.arcaneWorkbenchContainer.isSortDescending()) k += this.width; + } + + DrawingUtils.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + } + } + + @Override + public void drawButtonForegroundLayer(int mouseX, int mouseY){ + if(hovered) drawHoveringText(this.displayString, mouseX - guiLeft, mouseY - guiTop); + } + + } + + private static class GuiButtonApply extends GuiButton { public GuiButtonApply(int id, int x, int y){ super(id, x, y, 16, 16, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.apply")); @@ -481,7 +652,7 @@ public class GuiArcaneWorkbench extends GuiContainer { //colour = 10526880; } - DrawingUtils.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, 512, 256); + DrawingUtils.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, TEXTURE_WIDTH, TEXTURE_HEIGHT); //this.drawCenteredString(minecraft.fontRenderer, this.displayString, this.x + this.width / 2, // this.y + (this.height - 8) / 2, colour); } diff --git a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java index be8297c6..43c4a0de 100644 --- a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java +++ b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java @@ -1,27 +1,52 @@ package electroblob.wizardry.inventory; import electroblob.wizardry.Wizardry; +import electroblob.wizardry.block.BlockBookshelf; import electroblob.wizardry.event.SpellBindEvent; import electroblob.wizardry.item.IWorkbenchItem; import electroblob.wizardry.item.ItemSpellBook; import electroblob.wizardry.registry.WizardryAdvancementTriggers; import electroblob.wizardry.registry.WizardryItems; +import electroblob.wizardry.spell.Spell; import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench; import electroblob.wizardry.util.WandHelper; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.inventory.ClickType; import net.minecraft.inventory.Container; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.Slot; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraftforge.common.MinecraftForge; -import java.util.HashSet; -import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; +/** + * The container for the arcane workbench GUI. + *

+ * The virtual slot system works as follows:

+ * - The container has two types of slots: {@link SlotBookList} and {@link VirtualSlot}.
+ * - The {@code SlotBookList}s are the ones that actually get displayed. They essentially delegate all their + * functions to the relevant {@code VirtualSlot}.
+ * - Each {@code VirtualSlot} refers to a specific slot in another {@link IInventory} nearby, but is not displayed + * directly on the GUI. They are sorted and filtered according to the GUI input via + * {@link ContainerArcaneWorkbench#getActiveBookshelfSlots()}.
+ * - When a stack is taken from a {@code SlotBookList} (or its current stack is queried), the {@code VirtualSlot} + * it delegates to depends on the current search term, sort order and state of the linked bookshelves.
+ * - When a stack is inserted into a {@code SlotBookList}, the {@code VirtualSlot} it delegates to depends + * instead on where the stack came from originally and which virtual slots are free.
+ * - Finally, the {@code SlotBookList}s are only really used on the client side (though they are included on the server + * for consistency, just in case). The bookshelf slot delegates to a virtual slot on the client side (which is where + * the search and sorting is done), and then the click is sent to the server. + */ public class ContainerArcaneWorkbench extends Container { /** The arcane workbench tile entity associated with this container. */ @@ -36,6 +61,19 @@ public class ContainerArcaneWorkbench extends Container { public static final int SLOT_RADIUS = 42; + public static final int BOOKSHELF_SLOTS_X = 5; + public static final int BOOKSHELF_SLOTS_Y = 10; + + public static final int PLAYER_INVENTORY_SIZE = 36; + + private List bookshelfSlots = new ArrayList<>(); + private List activeBookshelfSlots = new ArrayList<>(); + + private int scroll = 0; + private SortType sortType = SortType.TIER; + private boolean sortDescending = false; + private String searchText = ""; + public ContainerArcaneWorkbench(IInventory inventory, TileEntityArcaneWorkbench tileentity){ this.tileentity = tileentity; @@ -70,6 +108,15 @@ public class ContainerArcaneWorkbench extends Container { } } + for(int y = 0; y < BOOKSHELF_SLOTS_Y; y++){ + for(int x = 0; x < BOOKSHELF_SLOTS_X; x++){ + int index = x + y * BOOKSHELF_SLOTS_X; + this.addSlotToContainer(new SlotBookList(tileentity, UPGRADE_SLOT + 1 + index, -114 + x * 18, 34 + y * 18, this, index)); + } + } + + refreshBookshelfSlots(); // Must be done last + this.onSlotChanged(CENTRE_SLOT, wand, null); } @@ -181,38 +228,42 @@ public class ContainerArcaneWorkbench extends Container { ItemStack stack = slot.getStack(); // The stack that was there originally remainder = stack.copy(); // A copy of that stack - // Workbench -> inventory + // Workbench -> inventory/bookshelves if(clickedSlotId <= UPGRADE_SLOT){ - // Tries to move the stack into the player's inventory. If this fails... - if(!this.mergeItemStack(stack, UPGRADE_SLOT + 1, this.inventorySlots.size(), true)){ - return ItemStack.EMPTY; // ...nothing else happens. + // Try to move the stack into the bookshelves. If this fails... + if(getBookshelfSlots().isEmpty() || !this.mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, + getBookshelfSlots().get(getBookshelfSlots().size()-1).slotNumber + 1, false)){ + // ...try to move the stack into the player's inventory. If this fails... + if(!this.mergeItemStack(stack, UPGRADE_SLOT + 1, UPGRADE_SLOT + 1 + PLAYER_INVENTORY_SIZE, true)){ + return ItemStack.EMPTY; // ...nothing else happens. + } } } - // Inventory -> workbench - else{ - // The following logic prevents shift-clicking transferring the items to the wrong slot. - int minSlotId = 0; - int maxSlotId = UPGRADE_SLOT; + // Bookshelves -> workbench/inventory + else if(getSlot(clickedSlotId) instanceof VirtualSlot){ - if(stack.getItem() instanceof ItemSpellBook){ - minSlotId = 0; - maxSlotId = CRYSTAL_SLOT - 1; - }else if(getSlot(CRYSTAL_SLOT).isItemValid(stack)){ - minSlotId = CRYSTAL_SLOT; - maxSlotId = CRYSTAL_SLOT; - }else if(getSlot(CENTRE_SLOT).isItemValid(stack)){ - minSlotId = CENTRE_SLOT; - maxSlotId = CENTRE_SLOT; - }else if(getSlot(UPGRADE_SLOT).isItemValid(stack)){ - minSlotId = UPGRADE_SLOT; - maxSlotId = UPGRADE_SLOT; - }else{ - // If none of the above cases were true, then the item won't fit in the workbench. - return ItemStack.EMPTY; + int[] slotRange = findSlotRangeForItem(stack); + + // Try to move the stack into the workbench. If this fails... + if(slotRange == null || !this.mergeItemStack(stack, slotRange[0], slotRange[1] + 1, false)){ + // ...try to move the stack into the player's inventory. If this fails... + if(!this.mergeItemStack(stack, UPGRADE_SLOT + 1, UPGRADE_SLOT + 1 + PLAYER_INVENTORY_SIZE, true)){ + return ItemStack.EMPTY; // ...nothing else happens. + } } + } + // Inventory -> workbench/bookshelves + else{ - if(!this.mergeItemStack(stack, minSlotId, maxSlotId + 1, false)){ - return ItemStack.EMPTY; + int[] slotRange = findSlotRangeForItem(stack); + + // Try to move the stack into the workbench. If this fails... + if(slotRange == null || !this.mergeItemStack(stack, slotRange[0], slotRange[1] + 1, false)){ + // ...try to move the stack into the bookshelves. If this fails... + if(getBookshelfSlots().isEmpty() || !this.mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, + getBookshelfSlots().get(getBookshelfSlots().size()-1).slotNumber + 1, false)){ + return ItemStack.EMPTY; // ...nothing else happens. + } } } @@ -232,18 +283,59 @@ public class ContainerArcaneWorkbench extends Container { return remainder; } - // Overridden to stop stacks merging into 'removed' slots. - @Override - protected boolean mergeItemStack(ItemStack stack, int minSlotID, int maxSlotID, boolean p_75135_4_){ + /** + * Returns the minimum and maximum IDs (inclusive) of the workbench slots that are appropriate for the given stack, + * or null if no slots are appropriate. Note that this does mean the stack will fit, only that it is valid + * for all of the slots in the given range, and will fit if there is space for it. + * @param stack The stack to find a slot for + * @param excludeBookshelves Whether to exclude the bookshelf slots (useful when transferring from them) + * @return A 2-element int array of the minimum and maximum slot IDs respectively + */ + @Nullable + private int[] findSlotRangeForItem(ItemStack stack){ - for(int i = minSlotID; i < maxSlotID; i++){ - // System.out.println(this.getSlot(i).xDisplayPosition); - if(this.getSlot(i).xPos >= 0 && this.getSlot(i).yPos >= 0 && !this.getSlot(i).getHasStack()){ - return super.mergeItemStack(stack, minSlotID, maxSlotID, p_75135_4_); + if(this.getSlot(0).isItemValid(stack)){ // Spell books + + ItemStack centreStack = getSlot(CENTRE_SLOT).getStack(); + + if(centreStack.getItem() instanceof IWorkbenchItem){ + // Restrict the range to visible slots + // (How did I not think of this before? Why did I go to the trouble of overriding mergeItemStack? And + // how did that fix ever work in the first place?!) + int spellSlots = ((IWorkbenchItem)centreStack.getItem()).getSpellSlotCount(centreStack); + if(spellSlots > 0){ + return new int[]{0, spellSlots - 1}; + } + } + + }else if(getSlot(CRYSTAL_SLOT).isItemValid(stack)){ + return new int[]{CRYSTAL_SLOT, CRYSTAL_SLOT}; + + }else if(getSlot(CENTRE_SLOT).isItemValid(stack)){ + return new int[]{CENTRE_SLOT, CENTRE_SLOT}; + + }else if(getSlot(UPGRADE_SLOT).isItemValid(stack)){ + return new int[]{UPGRADE_SLOT, UPGRADE_SLOT}; + } + + return null; // It won't fit! + } + + @Override + public ItemStack slotClick(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player){ + + // -999 is used for slots in the player inventory + if(slotId > 0 && getSlot(slotId) instanceof SlotBookList){ + + ItemStack stack = player.inventory.getItemStack(); + + if(!stack.isEmpty() && !getBookshelfSlots().isEmpty()){ + mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, getBookshelfSlots().get(getBookshelfSlots().size() - 1).slotNumber + 1, false); + return stack; } } - // Only returns false if none of the slots given are enabled and empty - return false; + + return super.slotClick(slotId, dragType, clickTypeIn, player); } /** @@ -272,4 +364,136 @@ public class ContainerArcaneWorkbench extends Container { } } + /** Scrolls to the given row number. */ + public void scrollTo(int row){ + this.scroll = row; + } + + /** Sets the sorting type to the given type, or toggles the sort direction if it is already that type. */ + public void setSortType(SortType sortType){ + + if(this.sortType == sortType){ + this.sortDescending = !this.sortDescending; + }else{ + this.sortType = sortType; + this.sortDescending = false; + } + + updateActiveBookshelfSlots(); + } + + /** Returns the current sorting type. */ + public SortType getSortType(){ + return sortType; + } + + /** Returns true if the current sorting is in descending order, false otherwise. */ + public boolean isSortDescending(){ + return sortDescending; + } + + /** Sets the search text for the bookshelf slots. */ + public void setSearchText(@Nonnull String searchText){ + this.searchText = searchText; + this.scrollTo(0); + updateActiveBookshelfSlots(); + } + + /** Returns all bookshelf slots currently linked to this container, including empty ones. The returned list + * is a copy of the internal virtual slot list, with any invalid slots removed. */ + public List getBookshelfSlots(){ + List validSlots = new ArrayList<>(bookshelfSlots); + validSlots.removeIf(s -> !s.isValid()); + return validSlots; + } + + /** Updates the active bookshelf slots with the current search term and sorting. Client-side only! */ + public void updateActiveBookshelfSlots(){ + activeBookshelfSlots = bookshelfSlots.stream().filter(s -> s.isValid() && !s.getStack().isEmpty() + // Slot 0 is a convenient way of testing if the item is a valid spell book + && this.getSlot(0).isItemValid(s.getStack()) + && Spell.byMetadata(s.getStack().getMetadata()).matches(searchText)) + // TODO: This doesn't account for non-spell book items at the moment + .sorted(Comparator.comparing(s -> Spell.byMetadata(s.getStack().getMetadata()), + sortDescending ? sortType.comparator.reversed() : sortType.comparator)) + .collect(Collectors.toList()); + } + + /** Returns all the {@link VirtualSlot}s that are currently active, sorted according to the current sort order. A + * virtual slot is active if it is not empty and its contents match the current search term (if any). */ + public List getActiveBookshelfSlots(){ + return activeBookshelfSlots; + } + + /** Returns all the {@link VirtualSlot}s that are currently visible on screen (and hence have an associated 'real' + * slot), accounting for search and scrolling, and sorted according to the current sort order. */ + public List getVisibleBookshelfSlots(){ + List activeSlots = getActiveBookshelfSlots(); + return activeSlots.subList(BOOKSHELF_SLOTS_X * scroll, activeSlots.size()); + } + + // The following operations are expensive so should not be done every tick! + // N.B. If we drop the requirement of it working with any container it could potentially be a lot easier since + // we then always have control over the bookshelf classes + + /** Called on initialisation, and whenever a bookshelf is added or removed. */ + private void refreshBookshelfSlots(){ + + this.inventorySlots.removeAll(bookshelfSlots); + // TESTME: May need to do this for inventoryItemStacks (probably not though, seems like MC handles it) + bookshelfSlots.clear(); + + for(IInventory bookshelf : findNearbyBookshelves()){ + for(int i=0; i findNearbyBookshelves(){ + + List bookshelves = new ArrayList<>(); + + int searchRadius = 4; // TODO: Config option for this + + for(int x = -searchRadius; x <= searchRadius; x++){ + for(int y = -searchRadius; y <= searchRadius; y++){ + for(int z = -searchRadius; z <= searchRadius; z++){ + + BlockPos pos = this.tileentity.getPos().add(x, y, z); + // TODO: Config option for allowed containers + if(this.tileentity.getWorld().getBlockState(pos).getBlock() instanceof BlockBookshelf){ + TileEntity te = this.tileentity.getWorld().getTileEntity(pos); + if(te instanceof IInventory && te != this.tileentity) bookshelves.add((IInventory)te); + } + + } + } + } + + return bookshelves; + + } + + public enum SortType { + + TIER("tier", Comparator.naturalOrder()), + ELEMENT("element", Comparator.comparing(Spell::getElement).thenComparing(Spell::getTier)), + ALPHABETICAL("alphabetical", Comparator.comparing(Spell::getUnlocalisedName)); + + public String name; + public Comparator comparator; + + SortType(String name, Comparator comparator){ + this.name = name; + this.comparator = comparator; + } + + } } diff --git a/src/main/java/electroblob/wizardry/inventory/SlotBookList.java b/src/main/java/electroblob/wizardry/inventory/SlotBookList.java new file mode 100644 index 00000000..8d7696bd --- /dev/null +++ b/src/main/java/electroblob/wizardry/inventory/SlotBookList.java @@ -0,0 +1,70 @@ +package electroblob.wizardry.inventory; + +import electroblob.wizardry.item.ItemSpellBook; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; + +import java.util.List; + +public class SlotBookList extends SlotItemClassList { + + private final ContainerArcaneWorkbench container; + /** The position in the book list this slot occupies (in other words: the slot index, but offset so the first book + * list slot starts at 0). */ + private final int listIndex; + + public SlotBookList(IInventory inventory, int index, int x, int y, ContainerArcaneWorkbench container, int listIndex){ + super(inventory, index, x, y, 64, ItemSpellBook.class); + this.container = container; + this.listIndex = listIndex; + } + + /** Returns the {@link VirtualSlot} this slot currently accesses, or null if it does not currently have one. */ + public VirtualSlot getDelegate(){ + return hasDelegate() ? container.getVisibleBookshelfSlots().get(listIndex) : null; + } + + /** Returns true if this slot currently accesses a {@link VirtualSlot}, false otherwise. */ + public boolean hasDelegate(){ + return listIndex < container.getVisibleBookshelfSlots().size(); + } + + @Override + public ItemStack getStack(){ + return hasDelegate() ? getDelegate().getStack() : ItemStack.EMPTY; // Delegate item lookup to virtual slot + } + + // This doesn't depend on the client-side search and sorting stuff so it can be done in here + @Override + public boolean isItemValid(ItemStack stack){ + // It's valid if there is a virtual slot that will accept it + return container.getBookshelfSlots().stream().anyMatch(s -> s.isItemValid(stack)); + } + + // Actual item interaction is delegated client-side before it ever gets here (in the GUI class) + // Therefore we only need to handle empty slot clicks + + @Override + public ItemStack decrStackSize(int amount){ + return ItemStack.EMPTY; + } + + @Override + public boolean canTakeStack(EntityPlayer player){ + return false; + } + + @Override + public ItemStack onTake(EntityPlayer player, ItemStack stack){ + return stack; + } + + @Override + public void putStack(ItemStack stack){ + // Can't handle this here either because it gets tangled up with the 348-line behemoth that is + // Container#slotClick, which has all sorts of side-effects + // Instead, we need to delegate client-side, as before + } + +} diff --git a/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java b/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java new file mode 100644 index 00000000..cf806246 --- /dev/null +++ b/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java @@ -0,0 +1,79 @@ +package electroblob.wizardry.inventory; + +import electroblob.wizardry.tileentity.TileEntityBookshelf; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntity; + +/** + * A {@code VirtualSlot} represents a slot in an inventory other than the one that is currently open. Like regular slots, + * they hold a single {@link net.minecraft.item.ItemStack}, but unlike regular slots, they are not drawn to the screen + * and cannot be interacted with directly. A virtual slot is effectively a reference to a specific slot in some other + * inventory that allows the details of that inventory to be abstracted away from the current container. + *

+ * N.B. For normal slots, {@code slotIndex == slotNumber} (index is for the inventory, number is for the container). + * Importantly, for virtual slots this is not the case, since the {@code Container} they belong to is not the + * one associated with the virtual slot's {@code IInventory}. + */ +public class VirtualSlot extends Slot { + + private final TileEntity tileEntity; + + public VirtualSlot(IInventory inventory, int index){ + super(inventory, index, -999, -999); + if(!(inventory instanceof TileEntity)) throw new IllegalArgumentException("Inventory must be a tile entity!"); + this.tileEntity = (TileEntity)inventory; + } + + @Override + public boolean isEnabled(){ + return false; // Virtual slots are never displayed + } + + // We don't really want to be updating the bookshelves every single tick (let alone every frame!), so we're going + // to have to do some 'assuming things stay the same until told otherwise'. This means it's possible that virtual + // slots will remain even when their containers are gone, so we need a failsafe for when that happens. + + /** Returns true if this slot is valid, i.e. the tile entity still exists. */ + public boolean isValid(){ + return !tileEntity.isInvalid(); + } + + // Normally the container decides if a stack is valid, but that's not going to work here + @Override + public boolean isItemValid(ItemStack stack){ + // getSlotIndex() is required here, NOT slotNumber (which is only populated when the slot is added to a + // container - the javadoc is wrong, it has nothing to do with inventories) + return isValid() && inventory.isItemValidForSlot(getSlotIndex(), stack); + } + + @Override + public boolean canTakeStack(EntityPlayer playerIn){ + return isValid() && super.canTakeStack(playerIn); + } + + @Override + public ItemStack onTake(EntityPlayer player, ItemStack stack){ + return isValid() ? super.onTake(player, stack) : ItemStack.EMPTY; + } + + @Override + public ItemStack getStack(){ + return isValid() ? super.getStack() : ItemStack.EMPTY; + } + + @Override + public void putStack(ItemStack stack){ + if(isValid() && inventory instanceof TileEntityBookshelf) ((TileEntityBookshelf)inventory).sync(); + if(isValid()) super.putStack(stack); + } + + @Override + public ItemStack decrStackSize(int amount){ + if(isValid() && inventory instanceof TileEntityBookshelf) ((TileEntityBookshelf)inventory).sync(); + return isValid() ? super.decrStackSize(amount) : ItemStack.EMPTY; + } + +} diff --git a/src/main/java/electroblob/wizardry/spell/Spell.java b/src/main/java/electroblob/wizardry/spell/Spell.java index 863bd31c..e817980f 100644 --- a/src/main/java/electroblob/wizardry/spell/Spell.java +++ b/src/main/java/electroblob/wizardry/spell/Spell.java @@ -117,6 +117,11 @@ public abstract class Spell extends IForgeRegistryEntry.Impl implements C public static final String SPLASH_EFFECT_DURATION = "splash_effect_duration"; public static final String SPLASH_EFFECT_STRENGTH = "splash_effect_strength"; + // TODO: Translations for these? + public static final String TIER_MATCH_PREFIX = "tier="; + public static final String ELEMENT_MATCH_PREFIX = "element="; + public static final String TYPE_MATCH_PREFIX = "type="; + /** Forge registry-based replacement for the internal spells list. */ public static IForgeRegistry registry; @@ -675,6 +680,30 @@ public abstract class Spell extends IForgeRegistryEntry.Impl implements C return net.minecraft.client.resources.I18n.format(getDescriptionTranslationKey()); } + /** + * Returns whether this spell matches the given string. Client-side only! A spell matches a particular string + * if any of the following conditions are true:

+ * - The spell's localised name contains the given string
+ * - The string starts with "tier:", and the localised name of the spell's tier contains the given string
+ * - The string starts with "element:", and the localised name of the spell's element contains the given string
+ * - The string starts with "type:", and the localised name of the spell's type contains the given string

+ * Matches are not case-sensitive. + * @param text The string to tested + * @return True if this spell matches the given string, false otherwise. + */ + public boolean matches(@Nonnull String text){ + + if(text.startsWith(TIER_MATCH_PREFIX)){ + return getTier().getDisplayName().toLowerCase(Locale.ROOT).contains(text.substring(TIER_MATCH_PREFIX.length())); + }else if(text.startsWith(ELEMENT_MATCH_PREFIX)){ + return getElement().getDisplayName().toLowerCase(Locale.ROOT).contains(text.substring(ELEMENT_MATCH_PREFIX.length())); + }else if(text.startsWith(TYPE_MATCH_PREFIX)){ + return getType().getDisplayName().toLowerCase(Locale.ROOT).contains(text.substring(TYPE_MATCH_PREFIX.length())); + }else{ + return getDisplayName().toLowerCase(Locale.ROOT).contains(text); + } + } + // ============================================ Sound methods ============================================== /** diff --git a/src/main/resources/assets/ebwizardry/lang/en_gb.lang b/src/main/resources/assets/ebwizardry/lang/en_gb.lang index 0ad6e088..c51e35cf 100644 --- a/src/main/resources/assets/ebwizardry/lang/en_gb.lang +++ b/src/main/resources/assets/ebwizardry/lang/en_gb.lang @@ -524,6 +524,11 @@ container.ebwizardry\:arcane_workbench.apply=Apply container.ebwizardry\:arcane_workbench.mana=Mana\: container.ebwizardry\:arcane_workbench.upgrades=Applied Upgrades\: +container.ebwizardry\:arcane_workbench.bookshelves=Bookshelves +container.ebwizardry\:arcane_workbench.sort_tier=Sort by Tier +container.ebwizardry\:arcane_workbench.sort_element=Sort by Element +container.ebwizardry\:arcane_workbench.sort_alphabetical=Sort Alphabetically + container.ebwizardry\:bookshelf=Bookshelf tier.novice=Novice diff --git a/src/main/resources/assets/ebwizardry/lang/en_us.lang b/src/main/resources/assets/ebwizardry/lang/en_us.lang index 6649b52c..6dee3005 100644 --- a/src/main/resources/assets/ebwizardry/lang/en_us.lang +++ b/src/main/resources/assets/ebwizardry/lang/en_us.lang @@ -524,6 +524,11 @@ container.ebwizardry\:arcane_workbench.apply=Apply container.ebwizardry\:arcane_workbench.mana=Mana\: container.ebwizardry\:arcane_workbench.upgrades=Applied Upgrades\: +container.ebwizardry\:arcane_workbench.bookshelves=Bookshelves +container.ebwizardry\:arcane_workbench.sort_tier=Sort by Tier +container.ebwizardry\:arcane_workbench.sort_element=Sort by Element +container.ebwizardry\:arcane_workbench.sort_alphabetical=Sort Alphabetically + container.ebwizardry\:bookshelf=Bookshelf tier.novice=Novice diff --git a/src/main/resources/assets/ebwizardry/textures/gui/arcane_workbench.png b/src/main/resources/assets/ebwizardry/textures/gui/arcane_workbench.png index 814ea32d..786a491b 100644 Binary files a/src/main/resources/assets/ebwizardry/textures/gui/arcane_workbench.png and b/src/main/resources/assets/ebwizardry/textures/gui/arcane_workbench.png differ