Add bookshelf interface to arcane workbench
Implements the main functionality: - Access bookshelves from within the workbench - Search by spell name, tier, element and type - Sort by tier, element or alphabetically, ascending or descending - Three-way shift-clicking support - Automatic return of books to bookshelves when wand is removed
This commit is contained in:
@@ -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.
|
||||
* <p></p>
|
||||
* The virtual slot system works as follows:<p></p>
|
||||
* - The container has two types of slots: {@link SlotBookList} and {@link VirtualSlot}.<br>
|
||||
* - The {@code SlotBookList}s are the ones that actually get displayed. They essentially delegate all their
|
||||
* functions to the relevant {@code VirtualSlot}.<br>
|
||||
* - 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()}.<br>
|
||||
* - When a stack is <i>taken</i> 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.<br>
|
||||
* - When a stack is <i>inserted</i> 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.<br>
|
||||
* - 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 <i>then</i> 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<VirtualSlot> bookshelfSlots = new ArrayList<>();
|
||||
private List<VirtualSlot> 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 <i>will</i> 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 <b>all</b> 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<VirtualSlot> getBookshelfSlots(){
|
||||
List<VirtualSlot> 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 <i>active</i> if it is not empty and its contents match the current search term (if any). */
|
||||
public List<VirtualSlot> 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<VirtualSlot> getVisibleBookshelfSlots(){
|
||||
List<VirtualSlot> 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<bookshelf.getSizeInventory(); i++){
|
||||
VirtualSlot slot = new VirtualSlot(bookshelf, i); // This sets the slot INDEX (for the INVENTORY)
|
||||
bookshelfSlots.add(slot);
|
||||
this.addSlotToContainer(slot); // This sets the slot NUMBER (for the CONTAINER)
|
||||
}
|
||||
}
|
||||
|
||||
if(tileentity.getWorld().isRemote) updateActiveBookshelfSlots();
|
||||
|
||||
}
|
||||
|
||||
/** Returns a list of nearby tile entities that have inventories. */
|
||||
public List<IInventory> findNearbyBookshelves(){
|
||||
|
||||
List<IInventory> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user