diff --git a/src/main/java/anya/pizza/houseki/block/entity/custom/CrusherBlockEntity.java b/src/main/java/anya/pizza/houseki/block/entity/custom/CrusherBlockEntity.java index 5f153e30..3a5f1390 100644 --- a/src/main/java/anya/pizza/houseki/block/entity/custom/CrusherBlockEntity.java +++ b/src/main/java/anya/pizza/houseki/block/entity/custom/CrusherBlockEntity.java @@ -34,11 +34,12 @@ import java.util.Optional; public class CrusherBlockEntity extends BlockEntity implements ExtendedScreenHandlerFactory, ImplementedInventory { - private final DefaultedList inventory = DefaultedList.ofSize(3, ItemStack.EMPTY); + private final DefaultedList inventory = DefaultedList.ofSize(4, ItemStack.EMPTY); private static final int INPUT_SLOT = 0; private static final int FUEL_SLOT = 1; private static final int OUTPUT_SLOT = 2; + private static final int AUXILIARY_OUTPUT_SLOT = 3; protected final PropertyDelegate propertyDelegate; private int progress = 0; @@ -179,27 +180,79 @@ private void updateMaxProgress(World world) { .orElse(CrusherRecipe.DEFAULT_CRUSHING_TIME); } + /** + * Determines whether the crusher can perform a craft with the current input. + * + * Checks for a matching CrusherRecipe and verifies that the recipe's primary output and optional auxiliary output can be inserted into the output and auxiliary output slots respectively. + * + * @return `true` if a matching recipe exists and both outputs can be inserted into their target slots, `false` otherwise. + */ private boolean canCraft() { Optional> recipe = getCurrentRecipe(); if (recipe.isEmpty()) return false; - ItemStack output = recipe.get().value().getResult(null); - ItemStack outputSlot = inventory.get(OUTPUT_SLOT); - return (outputSlot.isEmpty() || outputSlot.isOf(output.getItem())) - && outputSlot.getCount() + output.getCount() <= outputSlot.getMaxCount(); + CrusherRecipe crusherRecipe = recipe.get().value(); + ItemStack output = crusherRecipe.getResult(null); + ItemStack auxiliary = crusherRecipe.auxiliaryOutput().orElse(ItemStack.EMPTY); + + return canInsertIntoSlot(OUTPUT_SLOT, output) && canInsertIntoSlot(AUXILIARY_OUTPUT_SLOT, auxiliary); + } + + /** + * Determine whether the given ItemStack can be placed into the specified inventory slot + * without violating item compatibility or stack size limits. + * + * @param slot index of the target slot in the block entity's inventory + * @param stack the ItemStack intended for insertion; an empty stack is considered insertable + * @return `true` if the slot can accept the stack (slot empty or same item/component and total count does not exceed the slot's max), `false` otherwise + */ + private boolean canInsertIntoSlot(int slot, ItemStack stack) { + if (stack.isEmpty()) return true; + ItemStack slotStack = inventory.get(slot); + int maxCount = slotStack.isEmpty() ? stack.getMaxCount() : slotStack.getMaxCount(); + return (slotStack.isEmpty() || ItemStack.areItemsAndComponentsEqual(slotStack, stack)) + && slotStack.getCount() + stack.getCount() <= maxCount; } + /** + * Executes the currently matched crusher recipe: adds the recipe's main output to the output slot, + * adds the optional auxiliary output to the auxiliary output slot if present, and consumes one input. + * + * If no matching recipe is available, no changes are made. + */ private void craftItem() { Optional> recipe = getCurrentRecipe(); if (recipe.isEmpty()) return; + CrusherRecipe crusherRecipe = recipe.get().value(); + + // Handle Main Output + insertOrIncrement(OUTPUT_SLOT, crusherRecipe.getResult(null).copy()); + + // Handle Auxiliary Output + crusherRecipe.auxiliaryOutput().ifPresent(stack -> { + insertOrIncrement(AUXILIARY_OUTPUT_SLOT, stack.copy()); + }); + inventory.get(INPUT_SLOT).decrement(1); - ItemStack outputSlot = inventory.get(OUTPUT_SLOT); - ItemStack result = recipe.get().value().output().copy(); - if (outputSlot.isEmpty()) { - inventory.set(OUTPUT_SLOT, result); + } + + /** + * Inserts the provided ItemStack into the specified inventory slot, merging with the existing stack if present. + * + * If `result` is empty this method has no effect. If the target slot is empty the `result` is placed there; + * otherwise the existing stack's count is increased by `result.getCount()`. + * + * @param slot the index of the target inventory slot + * @param result the ItemStack to insert or merge into the slot + */ + private void insertOrIncrement(int slot, ItemStack result) { + if (result.isEmpty()) return; + ItemStack slotStack = inventory.get(slot); + if (slotStack.isEmpty()) { + inventory.set(slot, result); } else { - outputSlot.increment(result.getCount()); + slotStack.increment(result.getCount()); } } @@ -208,11 +261,25 @@ private Optional> getCurrentRecipe() { } + /** + * Provide the indices of inventory slots that are accessible from the specified side. + * + * @param side the block face from which access is attempted + * @return an array of slot indices; for {@link Direction#DOWN} returns {OUTPUT_SLOT, AUXILIARY_OUTPUT_SLOT}, otherwise returns {INPUT_SLOT, FUEL_SLOT} + */ @Override public int[] getAvailableSlots(Direction side) { - return side == Direction.DOWN ? new int[]{OUTPUT_SLOT} : new int[]{INPUT_SLOT, FUEL_SLOT}; + return side == Direction.DOWN ? new int[]{OUTPUT_SLOT, AUXILIARY_OUTPUT_SLOT} : new int[]{INPUT_SLOT, FUEL_SLOT}; } + /** + * Determines whether the given ItemStack may be inserted into the specified inventory slot from the provided side. + * + * @param slot the target inventory slot index + * @param stack the ItemStack to insert + * @param side the side from which insertion is attempted; may be null for non-sided access + * @return `true` if insertion is allowed: fuel slot accepts items that provide fuel time, input slot accepts items that match a Crusher recipe; `false` otherwise. + */ @Override public boolean canInsert(int slot, ItemStack stack, @Nullable Direction side) { if (slot == FUEL_SLOT) return getFuelTime(stack) > 0; @@ -221,11 +288,24 @@ public boolean canInsert(int slot, ItemStack stack, @Nullable Direction side) { return false; } + /** + * Determines whether items may be extracted from the given slot from the specified side. + * + * @param slot the slot index being accessed + * @param stack the stack being extracted + * @param side the side of the block from which extraction is attempted + * @return `true` if the slot is the primary output slot or the auxiliary output slot, `false` otherwise + */ @Override public boolean canExtract(int slot, ItemStack stack, Direction side) { - return slot == OUTPUT_SLOT; + return slot == OUTPUT_SLOT || slot == AUXILIARY_OUTPUT_SLOT; } + /** + * Create a network packet containing this block entity's update data for the client. + * + * @return the update packet for synchronizing this block entity to clients, or {@code null} if no update is required + */ @Nullable @Override public Packet toUpdatePacket() { @@ -242,4 +322,4 @@ public void clear() { inventory.clear(); markDirty(); } -} +} \ No newline at end of file diff --git a/src/main/java/anya/pizza/houseki/recipe/CrusherRecipe.java b/src/main/java/anya/pizza/houseki/recipe/CrusherRecipe.java index 3da43deb..8469d789 100644 --- a/src/main/java/anya/pizza/houseki/recipe/CrusherRecipe.java +++ b/src/main/java/anya/pizza/houseki/recipe/CrusherRecipe.java @@ -1,5 +1,7 @@ package anya.pizza.houseki.recipe; +import java.util.Optional; + import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -15,9 +17,33 @@ import net.minecraft.util.collection.DefaultedList; import net.minecraft.world.World; -public record CrusherRecipe(Ingredient inputItem, ItemStack output, int crushingTime) implements Recipe { +public record CrusherRecipe(Ingredient inputItem, ItemStack output, int crushingTime, Optional auxiliaryOutput) implements Recipe { public static final int DEFAULT_CRUSHING_TIME = 200; + public CrusherRecipe { + if (auxiliaryOutput == null) { + auxiliaryOutput = Optional.empty(); + } + } + + // 2. Secondary Constructor (For DataGen/Old Recipes) + // This allows you to call: new CrusherRecipe(input, output, time) + /** + * Constructs a CrusherRecipe with the specified input, output, and crushing time, and with no auxiliary output. + * + * @param inputItem the ingredient consumed by the recipe + * @param output the primary result produced by the recipe + * @param crushingTime the time required to perform the crushing (in ticks) + */ + public CrusherRecipe(Ingredient inputItem, ItemStack output, int crushingTime) { + this(inputItem, output, crushingTime, Optional.empty()); + } + + /** + * Gets the recipe's ingredient list. + * + * @return a DefaultedList containing the single input ingredient for this recipe + */ @Override public DefaultedList getIngredients() { DefaultedList list = DefaultedList.of(); @@ -62,17 +88,29 @@ public RecipeType getType() { public static class Serializer implements RecipeSerializer { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( - Ingredient.DISALLOW_EMPTY_CODEC.fieldOf("ingredient").forGetter(CrusherRecipe::inputItem), - ItemStack.CODEC.fieldOf("result").forGetter(CrusherRecipe::output), - Codec.INT.optionalFieldOf("crushingTime",DEFAULT_CRUSHING_TIME).forGetter(CrusherRecipe::crushingTime) - ).apply(inst, CrusherRecipe::new)); + Ingredient.DISALLOW_EMPTY_CODEC.fieldOf("ingredient").forGetter(CrusherRecipe::inputItem), + ItemStack.CODEC.fieldOf("result").forGetter(CrusherRecipe::output), + Codec.INT.optionalFieldOf("crushingTime", DEFAULT_CRUSHING_TIME).forGetter(CrusherRecipe::crushingTime), + // Optional auxiliary output is now the 4th parameter + ItemStack.CODEC.optionalFieldOf("auxiliary_result", ItemStack.EMPTY) + .xmap(Optional::of, opt -> opt.orElse(ItemStack.EMPTY)) + .forGetter(CrusherRecipe::auxiliaryOutput) + ).apply(inst, CrusherRecipe::new)); + public static final PacketCodec STREAM_CODEC = - PacketCodec.tuple( - Ingredient.PACKET_CODEC, CrusherRecipe::inputItem, - ItemStack.PACKET_CODEC, CrusherRecipe::output, - PacketCodecs.INTEGER, CrusherRecipe::crushingTime, - CrusherRecipe::new); + PacketCodec.tuple( + Ingredient.PACKET_CODEC, CrusherRecipe::inputItem, + ItemStack.PACKET_CODEC, CrusherRecipe::output, + PacketCodecs.INTEGER, CrusherRecipe::crushingTime, + // Change this line to use OPTIONAL_PACKET_CODEC + PacketCodecs.optional(ItemStack.OPTIONAL_PACKET_CODEC), CrusherRecipe::auxiliaryOutput, + CrusherRecipe::new); + /** + * Returns the map-based codec for serializing and deserializing CrusherRecipe instances. + * + * @return the MapCodec that encodes and decodes CrusherRecipe objects + */ @Override public MapCodec codec() { return CODEC; @@ -84,4 +122,4 @@ public PacketCodec packetCodec() { } } -} +} \ No newline at end of file diff --git a/src/main/java/anya/pizza/houseki/screen/custom/CrusherScreenHandler.java b/src/main/java/anya/pizza/houseki/screen/custom/CrusherScreenHandler.java index 4951fe34..8a2f2dc1 100644 --- a/src/main/java/anya/pizza/houseki/screen/custom/CrusherScreenHandler.java +++ b/src/main/java/anya/pizza/houseki/screen/custom/CrusherScreenHandler.java @@ -24,9 +24,17 @@ public CrusherScreenHandler(int syncId, PlayerInventory inventory, BlockPos pos) this(syncId, inventory, inventory.player.getWorld().getBlockEntity(pos), new ArrayPropertyDelegate(5)); } + /** + * Creates a Crusher screen handler, initializes the crusher and player inventories, and attaches the provided property delegate for GUI state syncing. + * + * @param syncId window sync id assigned by the client/server + * @param playerInventory the player's inventory to populate player slots and hotbar + * @param blockEntity the block entity whose inventory backs this handler; must be an Inventory of size 4 and is used as a CrusherBlockEntity + * @param arrayPropertyDelegate the PropertyDelegate used to synchronize progress, fuel, and related GUI properties + */ public CrusherScreenHandler(int syncId, PlayerInventory playerInventory, BlockEntity blockEntity, PropertyDelegate arrayPropertyDelegate) { super(ModScreenHandlers.CRUSHER_SCREEN_HANDLER, syncId); - checkSize((Inventory) blockEntity, 3); + checkSize((Inventory) blockEntity, 4); this.inventory = (Inventory) blockEntity; this.propertyDelegate = arrayPropertyDelegate; this.blockEntity = (CrusherBlockEntity) blockEntity; @@ -38,6 +46,12 @@ public boolean canInsert(ItemStack stack) { return false; //Makes output slot read-only } }); + this.addSlot(new Slot(inventory, 3, 130, 30) { + @Override + public boolean canInsert(ItemStack stack) { + return false; //Makes output slot read-only + } + }); addPlayerInventory(playerInventory); addPlayerHotbar(playerInventory); @@ -123,4 +137,4 @@ private void addPlayerHotbar(PlayerInventory playerInventory) { public PropertyDelegate getPropertyDelegate() { return propertyDelegate; } -} +} \ No newline at end of file diff --git a/src/main/resources/data/houseki/recipe/crushed_bauxite_from_crushing_bauxite.json b/src/main/resources/data/houseki/recipe/crushed_bauxite_from_crushing_bauxite.json index c94e2681..7cd3b688 100644 --- a/src/main/resources/data/houseki/recipe/crushed_bauxite_from_crushing_bauxite.json +++ b/src/main/resources/data/houseki/recipe/crushed_bauxite_from_crushing_bauxite.json @@ -6,5 +6,9 @@ "result": { "id": "houseki:crushed_bauxite" }, + "auxiliary_result": { + "id": "minecraft:iron_nugget", + "count": 1 + }, "crushingTime": 250 } \ No newline at end of file