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 super Spell> comparator;
+
+ SortType(String name, Comparator super Spell> 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