Skip to content

Commit a595d5c

Browse files
feat: builders workbench added and recipes added
1 parent f42074a commit a595d5c

16 files changed

Lines changed: 759 additions & 34 deletions

src/client/java/com/tcm/MineTale/MineTaleClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
1010
import com.tcm.MineTale.block.workbenches.screen.ArmorersWorkbenchScreen;
11+
import com.tcm.MineTale.block.workbenches.screen.BuildersWorkbenchScreen;
1112
import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen;
1213
import com.tcm.MineTale.block.workbenches.screen.FarmersWorkbenchScreen;
1314
import com.tcm.MineTale.registry.ModBlocks;
@@ -38,6 +39,7 @@ public void onInitializeClient() {
3839
MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new);
3940
MenuScreens.register(ModMenuTypes.ARMORERS_WORKBENCH_MENU, ArmorersWorkbenchScreen::new);
4041
MenuScreens.register(ModMenuTypes.FARMERS_WORKBENCH_MENU, FarmersWorkbenchScreen::new);
42+
MenuScreens.register(ModMenuTypes.BUILDERS_WORKBENCH_MENU, BuildersWorkbenchScreen::new);
4143

4244
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1, ChunkSectionLayer.CUTOUT);
4345
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2, ChunkSectionLayer.CUTOUT);
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package com.tcm.MineTale.block.workbenches.screen;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Optional;
7+
8+
import com.tcm.MineTale.MineTale;
9+
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
10+
import com.tcm.MineTale.block.workbenches.menu.BuildersWorkbenchMenu;
11+
import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor;
12+
import com.tcm.MineTale.network.CraftRequestPayload;
13+
import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent;
14+
import com.tcm.MineTale.registry.ModBlocks;
15+
import com.tcm.MineTale.registry.ModRecipeDisplay;
16+
import com.tcm.MineTale.registry.ModRecipes;
17+
18+
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
19+
import net.minecraft.client.ClientRecipeBook;
20+
import net.minecraft.client.gui.GuiGraphics;
21+
import net.minecraft.client.gui.components.Button;
22+
import net.minecraft.client.gui.navigation.ScreenPosition;
23+
import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen;
24+
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
25+
import net.minecraft.client.renderer.RenderPipelines;
26+
import net.minecraft.core.Holder;
27+
import net.minecraft.resources.Identifier;
28+
import net.minecraft.world.entity.player.Inventory;
29+
import net.minecraft.world.entity.player.Player;
30+
import net.minecraft.world.item.Item;
31+
import net.minecraft.world.item.ItemStack;
32+
import net.minecraft.world.item.crafting.Ingredient;
33+
import net.minecraft.world.item.crafting.display.RecipeDisplayEntry;
34+
import net.minecraft.world.item.crafting.display.RecipeDisplayId;
35+
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
36+
import net.minecraft.network.chat.Component;
37+
38+
public class BuildersWorkbenchScreen extends AbstractRecipeBookScreen<BuildersWorkbenchMenu> {
39+
private static final Identifier TEXTURE =
40+
Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png");
41+
42+
private final MineTaleRecipeBookComponent mineTaleRecipeBook;
43+
44+
private RecipeDisplayId lastKnownSelectedId = null;
45+
46+
private Button craftOneBtn;
47+
private Button craftTenBtn;
48+
private Button craftAllBtn;
49+
50+
/**
51+
* Initialize a workbench GUI screen using the provided container menu, player inventory, and title.
52+
*
53+
* @param menu the menu supplying slots and synchronized state for this screen
54+
* @param inventory the player's inventory to display and interact with
55+
* @param title the title component shown at the top of the screen
56+
*/
57+
public BuildersWorkbenchScreen(BuildersWorkbenchMenu menu, Inventory inventory, Component title) {
58+
this(menu, inventory, title, createRecipeBookComponent(menu));
59+
}
60+
61+
/**
62+
* Creates a BuildersWorkbenchScreen bound to the given menu, player inventory, title, and recipe book component.
63+
*
64+
* @param menu the menu backing this screen
65+
* @param inventory the player's inventory shown in the screen
66+
* @param title the screen title component
67+
* @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes in this screen
68+
*/
69+
private BuildersWorkbenchScreen(BuildersWorkbenchMenu menu, Inventory inventory, Component title, MineTaleRecipeBookComponent recipeBook) {
70+
super(menu, recipeBook, inventory, title);
71+
this.mineTaleRecipeBook = recipeBook;
72+
}
73+
74+
/**
75+
* Create a MineTaleRecipeBookComponent configured for the workbench screen.
76+
*
77+
* @param menu the workbench menu used to initialize the recipe book component
78+
* @return a MineTaleRecipeBookComponent containing the workbench tab and associated recipe category
79+
*/
80+
private static MineTaleRecipeBookComponent createRecipeBookComponent(BuildersWorkbenchMenu menu) {
81+
ItemStack tabIcon = new ItemStack(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem());
82+
83+
List<RecipeBookComponent.TabInfo> tabs = List.of(
84+
new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.BUILDERS_SEARCH)
85+
);
86+
87+
return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.BUILDERS_TYPE);
88+
}
89+
90+
/**
91+
* Configure the screen's GUI dimensions and initialize widgets.
92+
*
93+
* Sets the layout size (imageWidth = 176, imageHeight = 166), delegates remaining
94+
* layout initialization to the superclass, and creates the three craft buttons
95+
* ("1", "10", "All") wired to their respective handlers.
96+
*/
97+
@Override
98+
protected void init() {
99+
// Important: Set your GUI size before super.init()
100+
this.imageWidth = 176;
101+
this.imageHeight = 166;
102+
103+
super.init();
104+
105+
int defaultLeft = this.leftPos + 90;
106+
int defaultTop = this.topPos + 25;
107+
108+
this.craftOneBtn = addRenderableWidget(Button.builder(Component.literal("Craft"), (button) -> {
109+
handleCraftRequest(1);
110+
}).bounds(defaultLeft, defaultTop, 75, 20).build());
111+
112+
this.craftTenBtn = addRenderableWidget(Button.builder(Component.literal("x10"), (button) -> {
113+
handleCraftRequest(10);
114+
}).bounds(defaultLeft, defaultTop + 22, 35, 20).build());
115+
116+
this.craftAllBtn = addRenderableWidget(Button.builder(Component.literal("All"), (button) -> {
117+
handleCraftRequest(-1); // -1 represents "All" logic
118+
}).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build());
119+
}
120+
121+
/**
122+
* Sends a crafting request for the currently selected recipe in the integrated recipe book.
123+
*
124+
* Locates the last recipe collection and last selected recipe ID from the recipe book component,
125+
* resolves the recipe's result item, and sends a CraftRequestPayload to the server containing that
126+
* item and the requested amount.
127+
*
128+
* @param amount the quantity to craft; use -1 to request crafting of the full available stack ("All")
129+
*/
130+
private void handleCraftRequest(int amount) {
131+
// Look at our "Memory" instead of the component
132+
if (this.lastKnownSelectedId != null) {
133+
ClientRecipeBook book = this.minecraft.player.getRecipeBook();
134+
RecipeDisplayEntry entry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId);
135+
136+
if (entry != null) {
137+
List<ItemStack> results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level));
138+
if (!results.isEmpty()) {
139+
System.out.println("Persistent Selection Success: " + results.get(0));
140+
ClientPlayNetworking.send(new CraftRequestPayload(results.get(0), amount));
141+
return;
142+
}
143+
}
144+
}
145+
System.out.println("Request failed: No recipe was ever selected!");
146+
}
147+
148+
/**
149+
* Draws the workbench GUI background texture at the screen's top-left corner.
150+
*
151+
* @param guiGraphics the graphics context used to draw GUI elements
152+
* @param f partial tick time for interpolation
153+
* @param i current mouse x coordinate relative to the window
154+
* @param j current mouse y coordinate relative to the window
155+
*/
156+
protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) {
157+
int k = this.leftPos;
158+
int l = this.topPos;
159+
guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256);
160+
}
161+
162+
@Override
163+
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
164+
renderBackground(graphics, mouseX, mouseY, delta);
165+
super.render(graphics, mouseX, mouseY, delta);
166+
167+
// 1. Get the current selection from the book
168+
RecipeDisplayId currentId = this.mineTaleRecipeBook.getSelectedRecipeId();
169+
170+
// 2. If it's NOT null, remember it!
171+
if (currentId != null) {
172+
this.lastKnownSelectedId = currentId;
173+
}
174+
175+
// 3. Use the remembered ID to find the entry for button activation
176+
RecipeDisplayEntry selectedEntry = null;
177+
if (this.lastKnownSelectedId != null && this.minecraft.level != null) {
178+
ClientRecipeBook book = this.minecraft.player.getRecipeBook();
179+
selectedEntry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId);
180+
}
181+
182+
// 2. Button Activation Logic
183+
if (selectedEntry != null) {
184+
// We use the entry directly. It contains the 15 ingredients needed!
185+
boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1);
186+
boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2);
187+
boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10);
188+
189+
this.craftOneBtn.active = canCraftOne;
190+
this.craftTenBtn.active = canCraftTen;
191+
this.craftAllBtn.active = canCraftMoreThanOne;
192+
} else {
193+
this.craftOneBtn.active = false;
194+
this.craftTenBtn.active = false;
195+
this.craftAllBtn.active = false;
196+
}
197+
198+
renderTooltip(graphics, mouseX, mouseY);
199+
}
200+
201+
/**
202+
* Determines whether the player has enough ingredients to craft the given recipe the specified number of times.
203+
*
204+
* @param player the player whose inventory (and networked nearby items) will be checked; may be null
205+
* @param entry the recipe display entry providing crafting requirements; may be null
206+
* @param craftCount the multiplier for required ingredient quantities (e.g., 1, 10, or -1 is not specially handled here)
207+
* @return `true` if the player has at least the required quantity of each ingredient multiplied by `craftCount`, `false` otherwise (also returns `false` if `player` or `entry` is null or the recipe has no requirements)
208+
*/
209+
private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) {
210+
if (player == null || entry == null) return false;
211+
212+
Optional<List<Ingredient>> reqs = entry.craftingRequirements();
213+
if (reqs.isEmpty()) return false;
214+
215+
// 1. Group ingredients by their underlying Item Holders.
216+
// Using List<Holder<Item>> as the key ensures structural equality (content-based hashing).
217+
Map<List<Holder<Item>>, Integer> aggregatedRequirements = new HashMap<>();
218+
Map<List<Holder<Item>>, Ingredient> holderToIngredient = new HashMap<>();
219+
220+
for (Ingredient ing : reqs.get()) {
221+
// Collect holders into a List to get a stable hashCode() and equals()
222+
@SuppressWarnings("deprecation")
223+
List<Holder<Item>> key = ing.items().toList();
224+
225+
// Aggregate the counts (how many of this specific ingredient set are required)
226+
aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1);
227+
228+
// Map the list back to the original ingredient for use in hasIngredientAmount
229+
holderToIngredient.putIfAbsent(key, ing);
230+
}
231+
232+
// 2. Check the player's inventory against the aggregated totals
233+
Inventory inv = player.getInventory();
234+
for (Map.Entry<List<Holder<Item>>, Integer> entryReq : aggregatedRequirements.entrySet()) {
235+
List<Holder<Item>> key = entryReq.getKey();
236+
int totalNeeded = entryReq.getValue() * craftCount;
237+
238+
// Retrieve the original Ingredient object associated with this list of holders
239+
Ingredient originalIng = holderToIngredient.get(key);
240+
241+
if (!hasIngredientAmount(inv, originalIng, totalNeeded)) {
242+
return false;
243+
}
244+
}
245+
246+
return true;
247+
}
248+
249+
private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) {
250+
System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "...");
251+
if (totalRequired <= 0) return true;
252+
253+
int found = 0;
254+
255+
// 1. Check Player Inventory
256+
for (int i = 0; i < inventory.getContainerSize(); i++) {
257+
ItemStack stack = inventory.getItem(i);
258+
if (!stack.isEmpty() && ingredient.test(stack)) {
259+
found += stack.getCount();
260+
}
261+
}
262+
263+
// 2. CHECK THE NETWORKED ITEMS FROM CHESTS
264+
// This is the list we sent via the packet!
265+
if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) {
266+
for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) {
267+
if (!stack.isEmpty() && ingredient.test(stack)) {
268+
found += stack.getCount();
269+
System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found);
270+
}
271+
}
272+
}
273+
274+
if (found >= totalRequired) {
275+
System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired);
276+
return true;
277+
}
278+
279+
System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired);
280+
return false;
281+
}
282+
283+
/**
284+
* Computes the on-screen position for the recipe book toggle button for this GUI.
285+
*
286+
* @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center
287+
*/
288+
@Override
289+
protected ScreenPosition getRecipeBookButtonPosition() {
290+
// 1. Calculate the start (left) of your workbench GUI
291+
int guiLeft = (this.width - this.imageWidth) / 2;
292+
293+
// 2. Calculate the top of your workbench GUI
294+
int guiTop = (this.height - this.imageHeight) / 2;
295+
296+
// 3. Standard Vanilla positioning:
297+
// Usually 5 pixels in from the left and 49 pixels up from the center
298+
return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49);
299+
}
300+
}

src/client/java/com/tcm/MineTale/block/workbenches/screen/FarmersWorkbenchScreen.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
1010
import com.tcm.MineTale.block.workbenches.menu.FarmersWorkbenchMenu;
1111
import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor;
12-
import com.tcm.MineTale.mixin.client.RecipeBookComponentAccessor;
1312
import com.tcm.MineTale.network.CraftRequestPayload;
1413
import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent;
1514
import com.tcm.MineTale.registry.ModBlocks;
@@ -23,7 +22,6 @@
2322
import net.minecraft.client.gui.navigation.ScreenPosition;
2423
import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen;
2524
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
26-
import net.minecraft.client.gui.screens.recipebook.RecipeCollection;
2725
import net.minecraft.client.renderer.RenderPipelines;
2826
import net.minecraft.core.Holder;
2927
import net.minecraft.resources.Identifier;

src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
1010
import com.tcm.MineTale.block.workbenches.menu.WorkbenchWorkbenchMenu;
1111
import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor;
12-
import com.tcm.MineTale.mixin.client.RecipeBookComponentAccessor;
1312
import com.tcm.MineTale.network.CraftRequestPayload;
1413
import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent;
1514
import com.tcm.MineTale.registry.ModBlocks;
@@ -23,7 +22,6 @@
2322
import net.minecraft.client.gui.navigation.ScreenPosition;
2423
import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen;
2524
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
26-
import net.minecraft.client.gui.screens.recipebook.RecipeCollection;
2725
import net.minecraft.client.renderer.RenderPipelines;
2826
import net.minecraft.core.Holder;
2927
import net.minecraft.resources.Identifier;
Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
package com.tcm.MineTale.datagen.recipes;
22

3+
import com.tcm.MineTale.datagen.builders.WorkbenchRecipeBuilder;
4+
import com.tcm.MineTale.registry.ModBlocks;
5+
import com.tcm.MineTale.registry.ModItems;
6+
import com.tcm.MineTale.registry.ModRecipeDisplay;
7+
import com.tcm.MineTale.registry.ModRecipes;
8+
39
import net.minecraft.core.HolderLookup;
410
import net.minecraft.data.recipes.RecipeOutput;
511
import net.minecraft.data.recipes.RecipeProvider;
612

713
public class BuilderRecipes {
814
public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, HolderLookup.Provider lookup) {
915

10-
// TODO: BUILDERS_WORKBENCH_BLOCK & ROPE Not Implemented
11-
// new WorkbenchRecipeBuilder(ModRecipes.BUILDER_TYPE, ModRecipes.BUILDER_SERIALIZER)
12-
// .input(ModItems.PLANT_FIBER)
13-
// .output(ModBlocks.ROPE.asItem())
14-
// .time(3)
15-
// .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem()))
16-
// .bookCategory(ModRecipeDisplay.BUILDER_SEARCH)
17-
// .save(exporter, "builders_workbench_rope");
16+
new WorkbenchRecipeBuilder(ModRecipes.BUILDERS_TYPE, ModRecipes.BUILDERS_SERIALIZER)
17+
.input(ModItems.PLANT_FIBER)
18+
.output(ModBlocks.ROPE)
19+
.time(3)
20+
.unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK))
21+
.bookCategory(ModRecipeDisplay.BUILDERS_SEARCH)
22+
.save(exporter, "builders_workbench_rope");
1823

19-
// TODO: BUILDERS_WORKBENCH_BLOCK & ROPE_DIAGONAL Not Implemented
20-
// new WorkbenchRecipeBuilder(ModRecipes.BUILDER_TYPE, ModRecipes.BUILDER_SERIALIZER)
21-
// .input(ModItems.PLANT_FIBER)
22-
// .output(ModBlocks.ROPE_DIAGONAL.asItem())
23-
// .time(3)
24-
// .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem()))
25-
// .bookCategory(ModRecipeDisplay.BUILDER_SEARCH)
26-
// .save(exporter, "builders_workbench_rope_diagonal");
24+
new WorkbenchRecipeBuilder(ModRecipes.BUILDERS_TYPE, ModRecipes.BUILDERS_SERIALIZER)
25+
.input(ModItems.PLANT_FIBER)
26+
.output(ModBlocks.ROPE_DIAGONAL)
27+
.time(3)
28+
.unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK))
29+
.bookCategory(ModRecipeDisplay.BUILDERS_SEARCH)
30+
.save(exporter, "builders_workbench_rope_diagonal");
2731
}
2832
}

0 commit comments

Comments
 (0)