Skip to content

Commit e98c120

Browse files
feat: add armorers workbench and allow to craft in workbench
1 parent ecff9c5 commit e98c120

14 files changed

Lines changed: 820 additions & 30 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
import java.util.List;
88

99
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
10+
import com.tcm.MineTale.block.workbenches.screen.ArmorersWorkbenchScreen;
1011
import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen;
12+
import com.tcm.MineTale.registry.ModBlocks;
1113
import com.tcm.MineTale.registry.ModMenuTypes;
1214

1315
import net.fabricmc.api.ClientModInitializer;
1416
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
17+
import net.fabricmc.fabric.api.client.rendering.v1.BlockRenderLayerMap;
1518
import net.minecraft.client.gui.screens.MenuScreens;
1619
import net.minecraft.client.gui.screens.Screen;
1720
import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener;
21+
import net.minecraft.client.renderer.chunk.ChunkSectionLayer;
1822
import net.minecraft.world.item.ItemStack;
1923

2024
public class MineTaleClient implements ClientModInitializer {
@@ -31,6 +35,10 @@ public void onInitializeClient() {
3135
MenuScreens.register(ModMenuTypes.FURNACE_WORKBENCH_MENU, FurnaceWorkbenchScreen::new);
3236
MenuScreens.register(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, CampfireWorkbenchScreen::new);
3337
MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new);
38+
MenuScreens.register(ModMenuTypes.ARMORERS_WORKBENCH_MENU, ArmorersWorkbenchScreen::new);
39+
40+
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1, ChunkSectionLayer.CUTOUT);
41+
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2, ChunkSectionLayer.CUTOUT);
3442

3543
ClientPlayNetworking.registerGlobalReceiver(ClientboundNearbyInventorySyncPacket.TYPE, (payload, context) -> {
3644
List<ItemStack> items = payload.items();
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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.ArmorersWorkbenchMenu;
11+
import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor;
12+
import com.tcm.MineTale.mixin.client.RecipeBookComponentAccessor;
13+
import com.tcm.MineTale.network.CraftRequestPayload;
14+
import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent;
15+
import com.tcm.MineTale.registry.ModBlocks;
16+
import com.tcm.MineTale.registry.ModRecipeDisplay;
17+
import com.tcm.MineTale.registry.ModRecipes;
18+
19+
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
20+
import net.minecraft.client.ClientRecipeBook;
21+
import net.minecraft.client.gui.GuiGraphics;
22+
import net.minecraft.client.gui.components.Button;
23+
import net.minecraft.client.gui.navigation.ScreenPosition;
24+
import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen;
25+
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
26+
import net.minecraft.client.gui.screens.recipebook.RecipeCollection;
27+
import net.minecraft.client.renderer.RenderPipelines;
28+
import net.minecraft.core.Holder;
29+
import net.minecraft.resources.Identifier;
30+
import net.minecraft.world.entity.player.Inventory;
31+
import net.minecraft.world.entity.player.Player;
32+
import net.minecraft.world.item.Item;
33+
import net.minecraft.world.item.ItemStack;
34+
import net.minecraft.world.item.crafting.Ingredient;
35+
import net.minecraft.world.item.crafting.display.RecipeDisplayEntry;
36+
import net.minecraft.world.item.crafting.display.RecipeDisplayId;
37+
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
38+
import net.minecraft.network.chat.Component;
39+
40+
public class ArmorersWorkbenchScreen extends AbstractRecipeBookScreen<ArmorersWorkbenchMenu> {
41+
private static final Identifier TEXTURE =
42+
Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png");
43+
44+
private final MineTaleRecipeBookComponent mineTaleRecipeBook;
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 ArmorersWorkbenchScreen(ArmorersWorkbenchMenu menu, Inventory inventory, Component title) {
58+
this(menu, inventory, title, createRecipeBookComponent(menu));
59+
}
60+
61+
/**
62+
* Creates a ArmorersWorkbenchScreen 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 ArmorersWorkbenchScreen(ArmorersWorkbenchMenu 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(ArmorersWorkbenchMenu menu) {
81+
ItemStack tabIcon = new ItemStack(ModBlocks.ARMORERS_WORKBENCH_BLOCK.asItem());
82+
83+
List<RecipeBookComponent.TabInfo> tabs = List.of(
84+
new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.WORKBENCH_SEARCH)
85+
);
86+
87+
return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.WORKBENCH_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+
131+
private void handleCraftRequest(int amount) {
132+
// 1. Cast the book component to the Accessor to get the selected data
133+
RecipeBookComponentAccessor accessor = (RecipeBookComponentAccessor) this.mineTaleRecipeBook;
134+
135+
RecipeCollection collection = accessor.getLastRecipeCollection();
136+
RecipeDisplayId displayId = accessor.getLastRecipe();
137+
138+
if (collection != null && displayId != null) {
139+
// 2. Find the visual entry
140+
for (RecipeDisplayEntry entry : collection.getSelectedRecipes(RecipeCollection.CraftableStatus.ANY)) {
141+
if (entry.id().equals(displayId)) {
142+
// 3. Resolve result for the packet
143+
List<ItemStack> results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level));
144+
145+
if (!results.isEmpty()) {
146+
ItemStack resultStack = results.get(0);
147+
148+
// 4. LOG FOR DEBUGGING
149+
System.out.println("Sending craft request for: " + resultStack + " amount: " + amount);
150+
151+
ClientPlayNetworking.send(new CraftRequestPayload(resultStack, amount));
152+
}
153+
break;
154+
}
155+
}
156+
} else {
157+
System.out.println("Request failed: Collection or DisplayID is null!");
158+
}
159+
}
160+
161+
/**
162+
* Draws the workbench GUI background texture at the screen's top-left corner.
163+
*
164+
* @param guiGraphics the graphics context used to draw GUI elements
165+
* @param f partial tick time for interpolation
166+
* @param i current mouse x coordinate relative to the window
167+
* @param j current mouse y coordinate relative to the window
168+
*/
169+
protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) {
170+
int k = this.leftPos;
171+
int l = this.topPos;
172+
guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256);
173+
}
174+
175+
@Override
176+
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
177+
renderBackground(graphics, mouseX, mouseY, delta);
178+
super.render(graphics, mouseX, mouseY, delta);
179+
180+
// Get the ID of the recipe clicked in the ghost-book
181+
RecipeDisplayId displayId = this.mineTaleRecipeBook.getSelectedRecipeId();
182+
RecipeDisplayEntry selectedEntry = null;
183+
184+
if (displayId != null && this.minecraft.level != null) {
185+
ClientRecipeBook book = this.minecraft.player.getRecipeBook();
186+
// Accessing the known recipes via your Accessor
187+
Map<RecipeDisplayId, RecipeDisplayEntry> knownRecipes = ((ClientRecipeBookAccessor) book).getKnown();
188+
selectedEntry = knownRecipes.get(displayId);
189+
}
190+
191+
// 2. Button Activation Logic
192+
if (selectedEntry != null) {
193+
// We use the entry directly. It contains the 15 ingredients needed!
194+
boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1);
195+
boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2);
196+
boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10);
197+
198+
this.craftOneBtn.active = canCraftOne;
199+
this.craftTenBtn.active = canCraftTen;
200+
this.craftAllBtn.active = canCraftMoreThanOne;
201+
} else {
202+
this.craftOneBtn.active = false;
203+
this.craftTenBtn.active = false;
204+
this.craftAllBtn.active = false;
205+
}
206+
207+
renderTooltip(graphics, mouseX, mouseY);
208+
}
209+
210+
/**
211+
* Determines whether the player has enough ingredients to craft the given recipe the specified number of times.
212+
*
213+
* @param player the player whose inventory (and networked nearby items) will be checked; may be null
214+
* @param entry the recipe display entry providing crafting requirements; may be null
215+
* @param craftCount the multiplier for required ingredient quantities (e.g., 1, 10, or -1 is not specially handled here)
216+
* @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)
217+
*/
218+
private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) {
219+
if (player == null || entry == null) return false;
220+
221+
Optional<List<Ingredient>> reqs = entry.craftingRequirements();
222+
if (reqs.isEmpty()) return false;
223+
224+
// 1. Group ingredients by their underlying Item Holders.
225+
// Using List<Holder<Item>> as the key ensures structural equality (content-based hashing).
226+
Map<List<Holder<Item>>, Integer> aggregatedRequirements = new HashMap<>();
227+
Map<List<Holder<Item>>, Ingredient> holderToIngredient = new HashMap<>();
228+
229+
for (Ingredient ing : reqs.get()) {
230+
// Collect holders into a List to get a stable hashCode() and equals()
231+
@SuppressWarnings("deprecation")
232+
List<Holder<Item>> key = ing.items().toList();
233+
234+
// Aggregate the counts (how many of this specific ingredient set are required)
235+
aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1);
236+
237+
// Map the list back to the original ingredient for use in hasIngredientAmount
238+
holderToIngredient.putIfAbsent(key, ing);
239+
}
240+
241+
// 2. Check the player's inventory against the aggregated totals
242+
Inventory inv = player.getInventory();
243+
for (Map.Entry<List<Holder<Item>>, Integer> entryReq : aggregatedRequirements.entrySet()) {
244+
List<Holder<Item>> key = entryReq.getKey();
245+
int totalNeeded = entryReq.getValue() * craftCount;
246+
247+
// Retrieve the original Ingredient object associated with this list of holders
248+
Ingredient originalIng = holderToIngredient.get(key);
249+
250+
if (!hasIngredientAmount(inv, originalIng, totalNeeded)) {
251+
return false;
252+
}
253+
}
254+
255+
return true;
256+
}
257+
258+
private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) {
259+
System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "...");
260+
if (totalRequired <= 0) return true;
261+
262+
int found = 0;
263+
264+
// 1. Check Player Inventory
265+
for (int i = 0; i < inventory.getContainerSize(); i++) {
266+
ItemStack stack = inventory.getItem(i);
267+
if (!stack.isEmpty() && ingredient.test(stack)) {
268+
found += stack.getCount();
269+
}
270+
}
271+
272+
// 2. CHECK THE NETWORKED ITEMS FROM CHESTS
273+
// This is the list we sent via the packet!
274+
if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) {
275+
for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) {
276+
if (!stack.isEmpty() && ingredient.test(stack)) {
277+
found += stack.getCount();
278+
System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found);
279+
}
280+
}
281+
}
282+
283+
if (found >= totalRequired) {
284+
System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired);
285+
return true;
286+
}
287+
288+
System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired);
289+
return false;
290+
}
291+
292+
/**
293+
* Computes the on-screen position for the recipe book toggle button for this GUI.
294+
*
295+
* @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center
296+
*/
297+
@Override
298+
protected ScreenPosition getRecipeBookButtonPosition() {
299+
// 1. Calculate the start (left) of your workbench GUI
300+
int guiLeft = (this.width - this.imageWidth) / 2;
301+
302+
// 2. Calculate the top of your workbench GUI
303+
int guiTop = (this.height - this.imageHeight) / 2;
304+
305+
// 3. Standard Vanilla positioning:
306+
// Usually 5 pixels in from the left and 49 pixels up from the center
307+
return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49);
308+
}
309+
}

src/client/java/com/tcm/MineTale/datagen/ModLangProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public void generateTranslations(HolderLookup.Provider wrapperLookup, Translatio
1818

1919
// --- BLOCKS ---
2020
translationBuilder.add("block.minetale.workbench_workbench_block", "Workbench");
21+
translationBuilder.add("block.minetale.armorers_workbench_block", "Armorer's Workbench");
2122
translationBuilder.add("block.minetale.furnace_workbench_block_t1", "Furnace Workbench - Tier One");
2223
translationBuilder.add("block.minetale.furnace_workbench_block_t2", "Furnace Workbench - Tier Two");
2324
translationBuilder.add("block.minetale.campfire_workbench_block", "Campfire Workbench");

0 commit comments

Comments
 (0)