diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/AlchemistsWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/AlchemistsWorkbenchScreen.java new file mode 100644 index 0000000..e99997a --- /dev/null +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/AlchemistsWorkbenchScreen.java @@ -0,0 +1,313 @@ +package com.tcm.MineTale.block.workbenches.screen; + +import com.tcm.MineTale.MineTale; +import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; +import com.tcm.MineTale.block.workbenches.menu.AlchemistsWorkbenchMenu; +import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor; +import com.tcm.MineTale.network.CraftRequestPayload; +import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.registry.ModRecipes; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.navigation.ScreenPosition; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.Holder; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AlchemistsWorkbenchScreen extends AbstractRecipeBookScreen { + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png"); + + private final MineTaleRecipeBookComponent mineTaleRecipeBook; + + private RecipeDisplayId lastKnownSelectedId = null; + + private Button craftOneBtn; + private Button craftTenBtn; + private Button craftAllBtn; + + /** + * Initialize a workbench GUI screen using the provided container menu, player inventory, and title. + * + * @param menu the menu supplying slots and synchronized state for this screen + * @param inventory the player's inventory to display and interact with + * @param title the title component shown at the top of the screen + */ + public AlchemistsWorkbenchScreen(AlchemistsWorkbenchMenu menu, Inventory inventory, Component title) { + this(menu, inventory, title, createRecipeBookComponent(menu)); + } + + /** + * Initialises an AlchemistsWorkbenchScreen using the provided menu, player inventory, title and recipe book component. + * + * @param menu the backing AlchemistsWorkbenchMenu for this screen + * @param inventory the player's inventory to display + * @param title the screen title component + * @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes + */ + private AlchemistsWorkbenchScreen(AlchemistsWorkbenchMenu menu, Inventory inventory, Component title, MineTaleRecipeBookComponent recipeBook) { + super(menu, recipeBook, inventory, title); + this.mineTaleRecipeBook = recipeBook; + } + + /** + * Creates a MineTaleRecipeBookComponent configured for the alchemist's workbench. + * + * @param menu the workbench menu used to back the recipe book component + * @return a recipe-book component containing the workbench tab and associated recipe category + */ + private static MineTaleRecipeBookComponent createRecipeBookComponent(AlchemistsWorkbenchMenu menu) { + ItemStack tabIcon = new ItemStack(ModBlocks.ALCHEMISTS_WORKBENCH_BLOCK.asItem()); + + List tabs = List.of( + new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.ALCHEMISTS_SEARCH) + ); + + return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.ALCHEMISTS_TYPE); + } + + /** + * Initialises the workbench screen layout and interactive craft buttons. + * + * Sets the GUI image dimensions (must be set before calling superclass init), delegates remaining + * initialisation to the superclass, computes default positions and adds three craft buttons: + * "Craft" (request 1), "x10" (request 10) and "All" (request all; represented by -1). + */ + @Override + protected void init() { + // Important: Set your GUI size before super.init() + this.imageWidth = 176; + this.imageHeight = 166; + + super.init(); + + int defaultLeft = this.leftPos + 90; + int defaultTop = this.topPos + 25; + + this.craftOneBtn = addRenderableWidget(Button.builder(Component.translatable("gui.minetale.craftbtn"), (button) -> { + handleCraftRequest(1); + }).bounds(defaultLeft, defaultTop, 75, 20).build()); + + this.craftTenBtn = addRenderableWidget(Button.builder(Component.literal("x10"), (button) -> { + handleCraftRequest(10); + }).bounds(defaultLeft, defaultTop + 22, 35, 20).build()); + + this.craftAllBtn = addRenderableWidget(Button.builder(Component.translatable("gui.minetale.allbtn"), (button) -> { + handleCraftRequest(-1); // -1 represents "All" logic + }).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build()); + } + + /** + * Request crafting for the currently selected recipe from the integrated recipe book. + * + * If a recipe is selected, sends a CraftRequestPayload to the server for that recipe and the + * specified quantity. If no recipe is selected, no request is sent. + * + * @param amount the quantity to craft; use -1 to request crafting of the full available stack ("All") + */ + private void handleCraftRequest(int amount) { + // Look at our "Memory" instead of the component + if (this.lastKnownSelectedId != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + RecipeDisplayEntry entry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + + if (entry != null) { + List results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); + if (!results.isEmpty()) { + System.out.println("Persistent Selection Success: " + results.get(0)); + ClientPlayNetworking.send(new CraftRequestPayload(results.get(0), amount)); + return; + } + } + } + System.out.println("Request failed: No recipe was ever selected!"); + } + + /** + * Draws the workbench background texture at the screen's current GUI origin. + * + * @param guiGraphics the graphics context used to draw GUI elements + * @param f partial tick time for interpolation + * @param i current mouse x coordinate + * @param j current mouse y coordinate + */ + protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { + int k = this.leftPos; + int l = this.topPos; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); + } + + /** + * Render the screen, update the remembered recipe selection and enable or disable craft buttons based on available ingredients. + * + * Updates the stored recipe selection from the recipe book, resolves that selection against the client's known recipes when present, + * sets the craft buttons' active state according to whether the player has sufficient ingredients for counts of 1, 2 and 10, + * and renders the background, superclass UI and any tooltips. + */ + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics, mouseX, mouseY, delta); + super.render(graphics, mouseX, mouseY, delta); + + // 1. Get the current selection from the book + RecipeDisplayId currentId = this.mineTaleRecipeBook.getSelectedRecipeId(); + + // 2. If it's NOT null, remember it! + if (currentId != null) { + this.lastKnownSelectedId = currentId; + } + + // 3. Use the remembered ID to find the entry for button activation + RecipeDisplayEntry selectedEntry = null; + if (this.lastKnownSelectedId != null && this.minecraft.level != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + selectedEntry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + } + + // 2. Button Activation Logic + if (selectedEntry != null) { + // We use the entry directly. It contains the 15 ingredients needed! + boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1); + boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2); + boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10); + + this.craftOneBtn.active = canCraftOne; + this.craftTenBtn.active = canCraftTen; + this.craftAllBtn.active = canCraftMoreThanOne; + } else { + this.craftOneBtn.active = false; + this.craftTenBtn.active = false; + this.craftAllBtn.active = false; + } + + renderTooltip(graphics, mouseX, mouseY); + } + + /** + * Check whether a player has sufficient ingredients to craft a recipe a given number of times. + * + * @param player the player whose inventory and networked nearby items will be checked; may be null + * @param entry the recipe display entry providing crafting requirements; may be null + * @param craftCount the multiplier for required ingredient quantities + * @return `true` if the player has at least the required quantity of each ingredient multiplied by `craftCount`; `false` otherwise. Returns `false` if `player` or `entry` is null or if the recipe has no requirements. + */ + private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) { + if (player == null || entry == null) return false; + + Optional> reqs = entry.craftingRequirements(); + if (reqs.isEmpty()) return false; + + // 1. Group ingredients by their underlying Item Holders. + // Using List> as the key ensures structural equality (content-based hashing). + Map>, Integer> aggregatedRequirements = new HashMap<>(); + Map>, Ingredient> holderToIngredient = new HashMap<>(); + + for (Ingredient ing : reqs.get()) { + // Collect holders into a List to get a stable hashCode() and equals() + @SuppressWarnings("deprecation") + List> key = ing.items().toList(); + + // Aggregate the counts (how many of this specific ingredient set are required) + aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1); + + // Map the list back to the original ingredient for use in hasIngredientAmount + holderToIngredient.putIfAbsent(key, ing); + } + + // 2. Check the player's inventory against the aggregated totals + Inventory inv = player.getInventory(); + for (Map.Entry>, Integer> entryReq : aggregatedRequirements.entrySet()) { + List> key = entryReq.getKey(); + int totalNeeded = entryReq.getValue() * craftCount; + + // Retrieve the original Ingredient object associated with this list of holders + Ingredient originalIng = holderToIngredient.get(key); + + if (!hasIngredientAmount(inv, originalIng, totalNeeded)) { + return false; + } + } + + return true; + } + + /** + * Checks whether the given inventory and any nearby networked item sources contain at least the specified quantity of items that match the ingredient. + * + * @param inventory the inventory to search through (typically the player's inventory) + * @param ingredient the ingredient predicate used to test item stacks + * @param totalRequired the total number of matching items required + * @return `true` if the combined sources contain at least `totalRequired` matching items, `false` otherwise + */ + private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) { + System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "..."); + if (totalRequired <= 0) return true; + + int found = 0; + + // 1. Check Player Inventory + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + } + } + + // 2. CHECK THE NETWORKED ITEMS FROM CHESTS + // This is the list we sent via the packet! + if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) { + for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) { + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found); + } + } + } + + if (found >= totalRequired) { + System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired); + return true; + } + + System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired); + return false; + } + + /** + * Computes the on-screen position for the recipe book toggle button for this GUI. + * + * @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center + */ + @Override + protected ScreenPosition getRecipeBookButtonPosition() { + // 1. Calculate the start (left) of your workbench GUI + int guiLeft = (this.width - this.imageWidth) / 2; + + // 2. Calculate the top of your workbench GUI + int guiTop = (this.height - this.imageHeight) / 2; + + // 3. Standard Vanilla positioning: + // Usually 5 pixels in from the left and 49 pixels up from the center + return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49); + } +} \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java b/src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java index 867007b..4ec6f84 100644 --- a/src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java +++ b/src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java @@ -106,13 +106,13 @@ private void registerFurnaceWorkbench(BlockModelGenerators generator, Block bloc } /** - * Register item models for mod items that require custom or non-default models. - * - *

Registers models for shields, trimmable armour pieces, a handheld mace template, - * and various flat handheld item templates used for food and small items.

- * - * @param itemModelGenerators the generator used to create and register item models - */ + * Registers item models for mod items that require custom or non-default models. + * + *

Generates models for shields, trimmable armour pieces, a handheld mace template, + * and various flat handheld item templates used for food and small items.

+ * + * @param itemModelGenerators the generator used to create and register item models + */ @Override public void generateItemModels(ItemModelGenerators itemModelGenerators) { itemModelGenerators.generateShield(ModItems.COPPER_SHIELD); @@ -134,6 +134,9 @@ public void generateItemModels(ItemModelGenerators itemModelGenerators) { itemModelGenerators.generateFlatItem(ModItems.ONION, ModelTemplates.FLAT_HANDHELD_ITEM); itemModelGenerators.generateFlatItem(ModItems.WOOL_SCRAPS, ModelTemplates.FLAT_HANDHELD_ITEM); itemModelGenerators.generateFlatItem(ModItems.POOP, ModelTemplates.FLAT_HANDHELD_ITEM); + itemModelGenerators.generateFlatItem(ModItems.EMPTY_POTION_BOTTLE, ModelTemplates.FLAT_HANDHELD_ITEM); + itemModelGenerators.generateFlatItem(ModItems.ANTIDOTE, ModelTemplates.FLAT_HANDHELD_ITEM); + itemModelGenerators.generateFlatItem(ModItems.POPBERRY_BOMB, ModelTemplates.FLAT_HANDHELD_ITEM); } } \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/datagen/recipes/AlchemistRecipes.java b/src/client/java/com/tcm/MineTale/datagen/recipes/AlchemistRecipes.java index a60b868..b9444d7 100644 --- a/src/client/java/com/tcm/MineTale/datagen/recipes/AlchemistRecipes.java +++ b/src/client/java/com/tcm/MineTale/datagen/recipes/AlchemistRecipes.java @@ -11,35 +11,35 @@ public class AlchemistRecipes { /** - * Registers alchemist workbench recipes and writes them to the provided exporter. + * Generate and register alchemist workbench recipes and write them to the given exporter. * - * Uses the provider to determine unlock conditions and the lookup to resolve holders required - * during recipe generation. + * Creates the alchemist recipes (for example: antidote and popberry_bomb), using the provider + * to derive unlock conditions and the lookup to resolve registry holders required during generation. * - * @param provider the RecipeProvider used to query registered items/blocks for unlock conditions + * @param provider the RecipeProvider used to determine unlock conditions (e.g. presence of blocks/items) * @param exporter the RecipeOutput target to which generated recipes will be saved * @param lookup the HolderLookup.Provider used to resolve registry holders during generation */ public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, HolderLookup.Provider lookup) { - //new WorkbenchRecipeBuilder(ModRecipes.ALCHEMIST_TYPE, ModRecipes.ALCHEMIST_SERIALIZER) - // .input(ModItems.EMPTY_POTION_BOTTLE) - // .input(ModItems.PLANT_FIBER, 5) - // .input(ModItems.ESSENCE_OF_LIFE, 2) - // .input(ModItems.VENOM_SACK, 1) - // .output(ModItems.ANTIDOTE) - // .time(1) - // .unlockedBy("has_alchemist_workbench", provider.has(ModBlocks.ALCHEMIST_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.ALCHEMIST_SEARCH) - // .save(exporter, "ANTIDOTE"); + new WorkbenchRecipeBuilder(ModRecipes.ALCHEMISTS_TYPE, ModRecipes.ALCHEMISTS_SERIALIZER) + .input(ModItems.EMPTY_POTION_BOTTLE) + .input(ModItems.PLANT_FIBER, 5) + .input(ModItems.ESSENCE_OF_LIFE, 2) + .input(ModItems.VENOM_SAC, 1) + .output(ModItems.ANTIDOTE) + .time(1) + .unlockedBy("has_alchemists_workbench", provider.has(ModBlocks.ALCHEMISTS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.ALCHEMISTS_SEARCH) + .save(exporter, "antidote"); - // new WorkbenchRecipeBuilder(ModRecipes.ALCHEMIST_TYPE, ModRecipes.ALCHEMIST_SERIALIZER) - // .input(ModItems.WILD_BERRY, 6) - // .input(ModItems.BOOM_POWDER, 2) - // .input(ModItems.PLANT_FIBER, 4) - // .output(ModItems.POPBERRY_BOMB, 2) - // .time(0.5) - // .unlockedBy("has_alchemist_workbench", provider.has(ModBlocks.ALCHEMIST_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.ALCHEMIST_SEARCH) - // .save(exporter, "POPBERRY_BOMB"); + new WorkbenchRecipeBuilder(ModRecipes.ALCHEMISTS_TYPE, ModRecipes.ALCHEMISTS_SERIALIZER) + .input(ModItems.WILD_BERRY, 6) + .input(ModItems.BOOM_POWDER, 2) + .input(ModItems.PLANT_FIBER, 4) + .output(ModItems.POPBERRY_BOMB) + .time(0.5f) + .unlockedBy("has_alchemists_workbench", provider.has(ModBlocks.ALCHEMISTS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.ALCHEMISTS_SEARCH) + .save(exporter, "popberry_bomb"); } } diff --git a/src/client/java/com/tcm/MineTale/datagen/recipes/BlacksmithRecipes.java b/src/client/java/com/tcm/MineTale/datagen/recipes/BlacksmithRecipes.java index d82edd8..173c4bc 100644 --- a/src/client/java/com/tcm/MineTale/datagen/recipes/BlacksmithRecipes.java +++ b/src/client/java/com/tcm/MineTale/datagen/recipes/BlacksmithRecipes.java @@ -13,13 +13,17 @@ public class BlacksmithRecipes { /** - * Registers blacksmith workbench crafting recipes into the given exporter. + * Register blacksmith workbench crafting recipes into the supplied exporter. * - * Adds the copper_axe, copper_mace and copper_sword recipes (and leaves additional recipe templates commented). + * Adds concrete recipes for copper_axe, copper_battleaxe, copper_daggers, crude_longsword, + * copper_longsword, copper_mace, copper_shortbow and copper_sword. Each recipe specifies + * ingredient sources (items or tag references resolved via the provided lookup), the output, + * crafting time, an unlock condition requiring the blacksmiths workbench, a book category, + * and is saved to the exporter under a recipe key. * - * @param provider recipe provider used to build unlock conditions - * @param exporter target used to save generated recipes - * @param lookup holder lookup provider used to resolve tag-based ingredient references + * @param provider recipe provider used to construct unlock conditions + * @param exporter target used to save generated recipes + * @param lookup holder lookup provider used to resolve tag-based ingredient references */ public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, HolderLookup.Provider lookup) { new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) @@ -32,45 +36,45 @@ public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) .save(exporter, "copper_axe"); - //new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) - // .input(Items.COPPER_INGOT, 6) - // .input(ItemTags.LOGS, lookup, 6) - // .input(ModItems.PLANT_FIBER, 4) - // .output(ModItems.COPPER_BATTLEAXE) - // .time(3) - // .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) - // .save(exporter, "copper_battleaxe"); + new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) + .input(Items.COPPER_INGOT, 6) + .input(ItemTags.LOGS, lookup, 6) + .input(ModItems.PLANT_FIBER, 4) + .output(ModItems.COPPER_BATTLEAXE) + .time(3) + .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) + .save(exporter, "copper_battleaxe"); - // new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) - // .input(Items.COPPER_INGOT, 4) - // .input(ItemTags.LOGS, lookup, 3) - // .input(ModItems.PLANT_FIBER, 3) - // .output(ModItems.COPPER_DAGGERS) - // .time(3) - // .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) - // .save(exporter, "copper_daggers"); + new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) + .input(Items.COPPER_INGOT, 4) + .input(ItemTags.LOGS, lookup, 3) + .input(ModItems.PLANT_FIBER, 3) + .output(ModItems.COPPER_DAGGERS) + .time(3) + .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) + .save(exporter, "copper_daggers"); - // new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) - // .input(ItemTags.STONE_TOOL_MATERIALS, lookup, 6) - // .input(ItemTags.LOGS, lookup, 2) - // .input(ModItems.PLANT_FIBER, 4) - // .output(ModItems.CRUDE_LONGSWORD) - // .time(3) - // .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) - // .save(exporter, "crude_longsword"); + new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) + .input(ItemTags.STONE_TOOL_MATERIALS, lookup, 6) + .input(ItemTags.LOGS, lookup, 2) + .input(ModItems.PLANT_FIBER, 4) + .output(ModItems.CRUDE_LONGSWORD) + .time(3) + .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) + .save(exporter, "crude_longsword"); - // new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) - // .input(Items.COPPER_INGOT, 6) - // .input(ItemTags.LOGS, lookup, 2) - // .input(ModItems.PLANT_FIBER, 4) - // .output(ModItems.COPPER_LONGSWORD) - // .time(3) - // .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) - // .save(exporter, "copper_longsword"); + new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) + .input(Items.COPPER_INGOT, 6) + .input(ItemTags.LOGS, lookup, 2) + .input(ModItems.PLANT_FIBER, 4) + .output(ModItems.COPPER_LONGSWORD) + .time(3) + .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) + .save(exporter, "copper_longsword"); new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) .input(Items.COPPER_INGOT, 6) @@ -82,15 +86,15 @@ public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) .save(exporter, "copper_mace"); - // new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) - // .input(Items.COPPER_INGOT, 4) - // .input(ItemTags.LOGS, lookup, 4) - // .input(ModItems.PLANT_FIBER, 6) - // .output(ModItems.COPPER_SHORTBOW) - // .time(3) - // .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) - // .save(exporter, "copper_shortbow"); + new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) + .input(Items.COPPER_INGOT, 4) + .input(ItemTags.LOGS, lookup, 4) + .input(ModItems.PLANT_FIBER, 6) + .output(ModItems.COPPER_SHORTBOW) + .time(3) + .unlockedBy("has_blacksmiths_workbench", provider.has(ModBlocks.BLACKSMITHS_WORKBENCH_BLOCK.asItem())) + .bookCategory(ModRecipeDisplay.BLACKSMITHS_SEARCH) + .save(exporter, "copper_shortbow"); new WorkbenchRecipeBuilder(ModRecipes.BLACKSMITHS_TYPE, ModRecipes.BLACKSMITHS_SERIALIZER) .input(Items.COPPER_INGOT, 4) diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/AlchemistsWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/AlchemistsWorkbench.java new file mode 100644 index 0000000..24a2e5e --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/AlchemistsWorkbench.java @@ -0,0 +1,94 @@ +package com.tcm.MineTale.block.workbenches; + +import com.mojang.serialization.MapCodec; +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.block.workbenches.entity.AlchemistsWorkbenchEntity; +import com.tcm.MineTale.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public class AlchemistsWorkbench extends AbstractWorkbench { + public static final boolean IS_WIDE = true; + public static final boolean IS_TALL = false; + + public static final MapCodec CODEC = simpleCodec(AlchemistsWorkbench::new); + + /** + * Initialises an AlchemistsWorkbench configured to use the mod's ALCHEMISTS_WORKBENCH_BE block entity. + * + * @param properties block properties for the workbench + */ + public AlchemistsWorkbench(Properties properties) { + // Hardcode the supplier and sounds here if they never change + super(properties, () -> ModBlockEntities.ALCHEMISTS_WORKBENCH_BE, IS_WIDE, IS_TALL, 1); + } + + /** + * Create a new AlchemistsWorkbench configured with the given block properties and block-entity type supplier. + * + * @param properties block properties to apply to this workbench + * @param supplier supplier providing the BlockEntityType for the AlchemistsWorkbenchEntity + */ + public AlchemistsWorkbench(Properties properties, Supplier> supplier) { + super(properties, supplier, IS_WIDE, IS_TALL, 1); + } + + /** + * Provides a ticker for workbench block entities when the supplied block entity type matches this block's entity type. + * + * @param type the block entity type to match against this block's workbench entity type + * @return a BlockEntityTicker that updates matching workbench block entities, or {@code null} if the types do not match + */ + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + // This connects the Level's ticking system to your static tick method + return createTickerHelper(type, ModBlockEntities.ALCHEMISTS_WORKBENCH_BE, AbstractWorkbenchEntity::tick); + } + + /** + * MapCodec for serialising and deserialising this workbench. + * + * @return the MapCodec used to serialise and deserialise AlchemistsWorkbench instances + */ + @Override + protected MapCodec codec() { + return CODEC; + } + + /** + * Specifies that this block is rendered using its block model. + * + * @return RenderShape.MODEL to render the block using its JSON/model representation. + */ + @Override + public RenderShape getRenderShape(BlockState state) { + // BaseEntityBlock defaults to INVISIBLE. + // We set it to MODEL so the JSON model is rendered. + return RenderShape.MODEL; + } + + private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 16, 16); + + /** + * The block's collision and interaction shape: a full 1×1×1 voxel (x 0–16, y 0–16, z 0–16). + * + * @return the voxel shape used for collision and interaction + */ + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/AlchemistsWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AlchemistsWorkbenchEntity.java new file mode 100644 index 0000000..231af5d --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AlchemistsWorkbenchEntity.java @@ -0,0 +1,173 @@ +package com.tcm.MineTale.block.workbenches.entity; + +import com.mojang.serialization.Codec; +import com.tcm.MineTale.block.workbenches.menu.AlchemistsWorkbenchMenu; +import com.tcm.MineTale.recipe.WorkbenchRecipe; +import com.tcm.MineTale.registry.ModBlockEntities; +import com.tcm.MineTale.registry.ModRecipes; +import com.tcm.MineTale.util.Constants; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class AlchemistsWorkbenchEntity extends AbstractWorkbenchEntity { + protected final ContainerData data = new ContainerData() { + /** + * Provide a data value for container UI synchronization by index. + * + * @param index the data index to query + * @return `0` for all indices + */ + @Override + public int get(int index) { + return switch (index) { + default -> 0; + }; + } + + /** + * No-op setter; client-side attempts to set workbench data are ignored because state is server-driven. + */ + @Override + public void set(int index, int value) { + // Not required on WorkbenchEntity + } + + /** + * The number of data values exposed by this ContainerData. + * + * @return the number of data entries (4) + */ + @Override + public int getCount() { + return 4; + } + }; + + /** + * Creates a new AlchemistsWorkbenchEntity at the given world position with the specified block state. + * + * Initialises the workbench's tier to 1 and enables pulling from nearby inventories. + * + * @param blockPos the world position of this block entity + * @param blockState the block state for this block entity + */ + public AlchemistsWorkbenchEntity(BlockPos blockPos, BlockState blockState) { + super(ModBlockEntities.ALCHEMISTS_WORKBENCH_BE, blockPos, blockState); + + this.tier = 1; + this.canPullFromNearby = true; + } + + /** + * Persist this workbench's state to the given ValueOutput. + * + * Stores "WorkbenchTier" (int), "ScanRadius" (double), and the full inventory under "Inventory" + * using type-safe Codecs. + * + * @param valueOutput the writer used to serialize this entity's fields + */ + @Override + protected void saveAdditional(ValueOutput valueOutput) { + super.saveAdditional(valueOutput); + // store() uses Codecs for type safety + valueOutput.store("WorkbenchTier", Codec.INT, this.tier); + valueOutput.store("ScanRadius", Codec.DOUBLE, this.scanRadius); + + // Convert the SimpleContainer to a List of ItemStacks for the Codec + // Or use the built-in NBT helper if your framework supports it + List stacks = new ArrayList<>(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + stacks.add(inventory.getItem(i)); + } + + // CHANGE: Use OPTIONAL_CODEC instead of CODEC + valueOutput.store("Inventory", ItemStack.OPTIONAL_CODEC.listOf(), stacks); + } + + /** + * Restore this workbench's persisted state, applying defaults for missing values. + * + * Reads and restores three keys from the provided input: + * - "WorkbenchTier": stored as an int into {@code tier}, defaults to {@code 1} if absent; + * - "ScanRadius": stored as a double into {@code scanRadius}, defaults to {@code 0.0} if absent; + * - "Inventory": read as a list of {@code ItemStack} and copies items into the internal inventory up to its capacity. + * + * @param valueInput source of persisted values + */ + @Override + protected void loadAdditional(ValueInput valueInput) { + super.loadAdditional(valueInput); + // read() returns an Optional + this.tier = valueInput.read("WorkbenchTier", Codec.INT).orElse(1); + this.scanRadius = valueInput.read("ScanRadius", Codec.DOUBLE).orElse(0.0); + + // Read the inventory list back + valueInput.read("Inventory", ItemStack.OPTIONAL_CODEC.listOf()).ifPresent(stacks -> { + for (int i = 0; i < stacks.size() && i < inventory.getContainerSize(); i++) { + inventory.setItem(i, stacks.get(i)); + } + }); + } + + /** + * Create the server-side container menu for this workbench and synchronise nearby state to the opening player. + * + * @param syncId the window id for client–server menu synchronization + * @param playerInventory the opening player's inventory + * @param player the player who opened the menu; if a ServerPlayer, nearby state will be synced to them before the menu is returned + * @return the container menu bound to this workbench's inventory and synced data + */ + @Override + public @Nullable AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) { + // 1. Trigger the sync to the client before returning the menu + if (player instanceof ServerPlayer serverPlayer) { + this.syncNearbyToPlayer(serverPlayer); + } + + // // 2. Return the menu as usual + return new AlchemistsWorkbenchMenu(syncId, playerInventory, this.data, this); + } + + /** + * Identifies the recipe type used to find and match recipes for this workbench. + * + * @return the RecipeType for workbench recipes + */ + @Override + public RecipeType getWorkbenchRecipeType() { + return ModRecipes.ALCHEMISTS_TYPE; + } + + /** + * Determines whether the workbench currently has fuel available. + * + * Checks that the entity is in a loaded level and that the configured fuel slot contains an item. + * + * @return `true` if the entity is in a loaded level and the fuel slot contains an item, `false` otherwise. + */ + @Override + protected boolean hasFuel() { + if (this.level == null) return false; + + // Check if block is lit + // BlockState state = this.level.getBlockState(this.worldPosition); + // boolean isLit = state.hasProperty(BlockStateProperties.LIT) && state.getValue(BlockStateProperties.LIT); + + boolean hasFuelItem = !this.getItem(Constants.FUEL_SLOT).isEmpty(); + + return hasFuelItem; + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/AlchemistsWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/AlchemistsWorkbenchMenu.java new file mode 100644 index 0000000..f5b6c9e --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/AlchemistsWorkbenchMenu.java @@ -0,0 +1,130 @@ +package com.tcm.MineTale.block.workbenches.menu; + +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.recipe.WorkbenchRecipeInput; +import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.RecipeBookType; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +public class AlchemistsWorkbenchMenu extends AbstractWorkbenchContainerMenu { + // No internal inventory needed anymore, but we pass an empty container to the super + private static final int EMPTY_SIZE = 0; + private static final int DATA_SIZE = 0; + + @Nullable + private final AbstractWorkbenchEntity blockEntity; + private final Inventory playerInventory; + + /** + * Creates a client-side menu instance when the workbench UI is opened. + * + * @param syncId the synchronization id used to match this menu with the server + * @param playerInventory the player's inventory bound to this menu + */ + public AlchemistsWorkbenchMenu(int syncId, Inventory playerInventory) { + this(syncId, playerInventory, new SimpleContainerData(EMPTY_SIZE), null); + } + + /** + * Initialise a workbench menu bound to the given player inventory and optional block entity. + * + * Uses an empty internal container (size 0) and the class's data size for synchronisation of numeric state. + * + * @param syncId the synchronisation id for this menu + * @param playerInventory the player's inventory used for slot access and recipe-book integration + * @param data container data used to synchronise numeric state between server and client + * @param blockEntity nullable block entity this menu is bound to, or {@code null} if not bound + */ + public AlchemistsWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { + // Note: The order of arguments depends on your AbstractWorkbenchContainerMenu, + // but the 'expectedSize' parameter MUST be 0. + super( + ModMenuTypes.ALCHEMISTS_WORKBENCH_MENU, + syncId, + new SimpleContainer(EMPTY_SIZE), + data, + DATA_SIZE, + playerInventory, + EMPTY_SIZE, + EMPTY_SIZE, + ModRecipes.ALCHEMISTS_TYPE + ); + this.blockEntity = blockEntity; + this.playerInventory = playerInventory; + } + + /** + * Accesses the block entity bound to this menu, if present. + * + * @return the bound AbstractWorkbenchEntity, or {@code null} if this menu is not bound to a block entity + */ + @Override + public @Nullable AbstractWorkbenchEntity getBlockEntity() { + return this.blockEntity; + } + + /** + * Aggregate item stacks relevant for crafting lookups into the provided StackedItemContents. + * + * This includes stacks from the bound player's inventory, any stacks present in this menu's + * internal container slots, and nearby item stacks supplied by the network when available. + * + * @param contents the StackedItemContents to populate with accounted stacks + */ + @Override + public void fillCraftSlotsStackedContents(StackedItemContents contents) { + // 1. Account for items in the player's pockets + this.playerInventory.fillStackedContents(contents); + + // 2. Account for items sitting in the Workbench slots (if any) + for (int i = 0; i < this.container.getContainerSize(); i++) { + contents.accountStack(this.container.getItem(i)); + } + + // 3. THE FIX: Use the list provided by the Packet (Networked Items) + // We stop calling be.getNearbyInventories() here because it returns empty on Client + List nearbyItems = this.getNetworkedNearbyItems(); + + if (!nearbyItems.isEmpty() && this.playerInventory.player.level().isClientSide()) { + System.out.println("DEBUG: Recipe Book is now accounting for " + nearbyItems.size() + " stacks from the packet!"); + } + + for (ItemStack stack : nearbyItems) { + contents.accountStack(stack); + } + } + + /** + * Selects the crafting recipe-book category for this menu. + * + * @return {@code RecipeBookType.CRAFTING} + */ + @Override + public RecipeBookType getRecipeBookType() { + // This keeps the Crafting-style recipe book available on the UI + return RecipeBookType.CRAFTING; + } + + /** + * Provide an empty recipe input for this menu's crafting UI. + * + * There are no internal crafting slots for this menu; crafting reads items from the player inventory when triggered. + * + * @return a WorkbenchRecipeInput with both input stacks set to ItemStack.EMPTY + */ + @Override + public WorkbenchRecipeInput createRecipeInput() { + // Since there are no slots, we return an empty input. + // The actual crafting logic will scan the player inventory directly when a button is clicked. + return new WorkbenchRecipeInput(ItemStack.EMPTY, ItemStack.EMPTY); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java index 8cb7d34..98d4903 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java +++ b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java @@ -21,6 +21,7 @@ public class ModRecipes { public static final RecipeType BUILDERS_TYPE = createType("builders_recipe_type"); public static final RecipeType BLACKSMITHS_TYPE = createType("blacksmiths_recipe_type"); public static final RecipeType FURNITURE_TYPE = createType("furniture_recipe_type"); + public static final RecipeType ALCHEMISTS_TYPE = createType("alchemists_recipe_type"); // 2. Define the Serializers (The "How") // We pass the specific Type into the Serializer's constructor @@ -48,11 +49,14 @@ public class ModRecipes { public static final RecipeSerializer FURNITURE_SERIALIZER = new WorkbenchRecipe.Serializer(FURNITURE_TYPE); + public static final RecipeSerializer ALCHEMISTS_SERIALIZER = + new WorkbenchRecipe.Serializer(ALCHEMISTS_TYPE); + /** - * Registers the mod's recipe types and their serializers into Minecraft's built‑in registries under the mod namespace. + * Register the mod's custom recipe types and their serializers into Minecraft's built-in registries under the mod namespace. * - * This registers the following recipe types with their corresponding serializers: FURNACE_T1_TYPE, CAMPFIRE_TYPE, - * WORKBENCH_TYPE, ARMORERS_TYPE, FARMERS_TYPE, BUILDERS_TYPE, BLACKSMITHS_TYPE and FURNITURE_TYPE. + * Registers the following recipe types with their corresponding serializers: FURNACE_T1_TYPE, CAMPFIRE_TYPE, + * WORKBENCH_TYPE, ARMORERS_TYPE, FARMERS_TYPE, BUILDERS_TYPE, BLACKSMITHS_TYPE, FURNITURE_TYPE and ALCHEMISTS_TYPE. */ public static void initialize() { // Register the Furnace-flavored version @@ -72,6 +76,8 @@ public static void initialize() { register(BLACKSMITHS_TYPE, BLACKSMITHS_SERIALIZER); register(FURNITURE_TYPE, FURNITURE_SERIALIZER); + + register(ALCHEMISTS_TYPE, ALCHEMISTS_SERIALIZER); } /**