From da8f6ac9cb76f6b604871c0c935538953e9d740c Mon Sep 17 00:00:00 2001 From: IntegratedQuantum Date: Fri, 31 Mar 2023 20:37:39 +0200 Subject: [PATCH] Add inventory crafting. --- assets/cubyz/recipes/wood_recipes | 21 +-- assets/cubyz/ui/inventory/crafting_arrow.png | Bin 0 -> 588 bytes .../ui/inventory/crafting_result_slot.png | Bin 0 -> 696 bytes assets/cubyz/ui/inventory/immutable_slot.png | Bin 0 -> 696 bytes src/assets.zig | 165 ++++------------- src/gui/components/CraftingResultSlot.zig | 110 +++++++++++ src/gui/components/Icon.zig | 48 +++++ src/gui/components/ImmutableItemSlot.zig | 70 +++++++ src/gui/gui.zig | 24 ++- src/gui/gui_component.zig | 8 +- src/gui/windows/_windowlist.zig | 1 + src/gui/windows/inventory.zig | 12 +- src/gui/windows/inventory_crafting.zig | 173 ++++++++++++++++++ src/items.zig | 102 ++++++++++- 14 files changed, 580 insertions(+), 154 deletions(-) create mode 100644 assets/cubyz/ui/inventory/crafting_arrow.png create mode 100644 assets/cubyz/ui/inventory/crafting_result_slot.png create mode 100644 assets/cubyz/ui/inventory/immutable_slot.png create mode 100644 src/gui/components/CraftingResultSlot.zig create mode 100644 src/gui/components/Icon.zig create mode 100644 src/gui/components/ImmutableItemSlot.zig create mode 100644 src/gui/windows/inventory_crafting.zig diff --git a/assets/cubyz/recipes/wood_recipes b/assets/cubyz/recipes/wood_recipes index a3294c30..f573c626 100644 --- a/assets/cubyz/recipes/wood_recipes +++ b/assets/cubyz/recipes/wood_recipes @@ -1,33 +1,20 @@ L = cubyz:oak_log T = cubyz:oak_top P = cubyz:oak_planks -S = cubyz:stick C = cubyz:coal -shapeless L result 4*P -shapeless T result 3*P -shaped -P P -P P +4*P result cubyz:workbench -shaped -P -P -result 4*S - -shaped C -S +P result 8*cubyz:torch -shaped -S P S -S P S -result 6*cubyz:oak_fence \ No newline at end of file +P +result 2*cubyz:oak_fence \ No newline at end of file diff --git a/assets/cubyz/ui/inventory/crafting_arrow.png b/assets/cubyz/ui/inventory/crafting_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..7d138cc1059ed509af848e11c0600992ff5d3f34 GIT binary patch literal 588 zcmV-S0<-;zP)EX>4Tx04R}tkv&MmKpe$iTcs+M4t5X`%ut;yh!t_vDionYs1;guFuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0scmXsb<#%plX(p zP9}tGZdC}rB7hNu(TljmOnpuiQ}7&L_we!cF2=LG&;2?2mAuISpFljzbi*RvAfDc| zbk6(45mu5E;&bA0gDyz?$aUG}H_kbWYY7*5n`d(!Ey()lA#h$5l0nOqkMnX zWrgz=XSGset$XqphVt6VGS_JiBZ);UL4*JqHIz|-g&3_GDJIgipYZSxIew8`GP%lN z%5sa{}}00006VoOIv00000008+zyMF)x010qNS#tmY z4#NNd4#NS*Z>VGd000McNliru=K~)P4hxDx54ZpT07*$iK~y-)V_+EafDxC(#fuji z7#J8BPR$Pg&%nUIkUHfCR<%qta{&y@&e8af%tv<#^<02%6CnU|$uI~579!FC15MMx aFaQA0R~ZzUWyM$k0000xlq%tsQ zOst(~>v7mY=4gEI(nX@|++7+eEBwTc=*w*7a$O;i)fy)2vu^%{C5oPfg(5NBe04fu zvN3Z?OAAC<4)P0xi>teeYksWvT`?uQxX#mei>@` zfy@5ag;qtMlV`3xSJC7<|FM)`KK{Lu&FSBF<}bFMY%2LcxHElbUc?dUBbR4SJ6Ad1 z;s{q!+mAUFR~B#!*%!qY{_HKjm)Mxz){~Mn&%#CLS#(5ZVwge5BhN<#ZdWzpJ$BA{ zxaVl3ZfL~gd4F0DuGzNfOzEuVjzmH42?|b*tgcfM6rEPPb#=15b+`S)+!wDj-D#bY zgr!(O8`sT`&HoR+&pUhP$d1_}Jxp))xBWHbVJT~@y1H$Df9~!&rXTYgwb#DXzuqv@ z|I%t--5QoJIn3hMeSL2->y#M7JcmajBdb&7*rU@{`;_^J56`;lGv>(t4;;T8F+4bXQkx1 zuGT*G%#FO=ng1l8mh9Ve@6@5*8h#!LgM`r}J|S~HA9gyios?)fd+_L*Jd3eW=#p00i_>zopr0RP7mkN^Mx literal 0 HcmV?d00001 diff --git a/assets/cubyz/ui/inventory/immutable_slot.png b/assets/cubyz/ui/inventory/immutable_slot.png new file mode 100644 index 0000000000000000000000000000000000000000..f129f9b253eca82ca831153121c679a28f811253 GIT binary patch literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT1|$ph9xlq%tsQ zOst(~>v7mY=4gEI(nX@|++7+eEBwTc=*w*7a$O;i)fy)2vu^%{C5oPfg(5NBe04fu zvN3Z?OAAC<4)P0xi>teeYksWvT`?uQxX#mei>@` zfy@5ag;qtMlV`3xSJC7<|FM)`KK{Lu&FSBF<}bFMY%2LcxHElbUc?dUBbR4SJ6Ad1 z;s{q!+mAUFR~B#!*%!qY{_HKjm)Mxz){~Mn&%#CLS#(5ZVwge5BhN<#ZdWzpJ$BA{ zxaVl3ZfL~gd4F0DuGzNfOzEuVjzmH42?|b*tgcfM6rEPPb#=15b+`S)+!wDj-D#bY zgr!(O8`sT`&HoR+&pUhP$d1_}Jxp))xBWHbVJT~@y1H$Df9~!&rXTYgwb#DXzuqv@ z|I%t--5QoJIn3hMeSL2->y#M7JcmajBdb&7*rU@{`;_^J56`;lGv>(t4;;T8F+4bXQkx1 zuGT*G%#FO=ng1l8mh9Ve@6@5*8h#!LgM`r}J|S~HA9gyios?)fd+_L*Jd3eW=#p00i_>zopr0RP7mkN^Mx literal 0 HcmV?d00001 diff --git a/src/assets.zig b/src/assets.zig index da996cf2..ea2e243f 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -42,8 +42,30 @@ pub fn readAllJsonFilesInAddons(externalAllocator: Allocator, addons: std.ArrayL } } } +/// Reads text files recursively from all subfolders. +pub fn readAllFilesInAddons(externalAllocator: Allocator, addons: std.ArrayList(std.fs.Dir), subPath: []const u8, output: *std.ArrayList([]const u8)) !void { + for(addons.items) |addon| { + var dir: std.fs.IterableDir = addon.openIterableDir(subPath, .{}) catch |err| { + if(err == error.FileNotFound) continue; + return err; + }; + defer dir.close(); -pub fn readAssets(externalAllocator: Allocator, assetPath: []const u8, blocks: *std.StringHashMap(JsonElement), items: *std.StringHashMap(JsonElement), biomes: *std.StringHashMap(JsonElement)) !void { + var walker = try dir.walk(main.threadAllocator); + defer walker.deinit(); + + while(try walker.next()) |entry| { + if(entry.kind == .File) { + var file = try dir.dir.openFile(entry.path, .{}); + defer file.close(); + const string = try file.readToEndAlloc(externalAllocator, std.math.maxInt(usize)); + try output.append(string); + } + } + } +} + +pub fn readAssets(externalAllocator: Allocator, assetPath: []const u8, blocks: *std.StringHashMap(JsonElement), items: *std.StringHashMap(JsonElement), biomes: *std.StringHashMap(JsonElement), recipes: *std.ArrayList([]const u8)) !void { var addons = std.ArrayList(std.fs.Dir).init(main.threadAllocator); defer addons.deinit(); var addonNames = std.ArrayList([]const u8).init(main.threadAllocator); @@ -68,6 +90,7 @@ pub fn readAssets(externalAllocator: Allocator, assetPath: []const u8, blocks: * try readAllJsonFilesInAddons(externalAllocator, addons, addonNames, "blocks", blocks); try readAllJsonFilesInAddons(externalAllocator, addons, addonNames, "items", items); try readAllJsonFilesInAddons(externalAllocator, addons, addonNames, "biomes", biomes); + try readAllFilesInAddons(externalAllocator, addons, "recipes", recipes); } pub fn init() !void { @@ -78,7 +101,7 @@ pub fn init() !void { commonBiomes = std.StringHashMap(JsonElement).init(arenaAllocator); commonRecipes = std.ArrayList([]const u8).init(arenaAllocator); - try readAssets(arenaAllocator, "assets/", &commonBlocks, &commonItems, &commonBiomes); + try readAssets(arenaAllocator, "assets/", &commonBlocks, &commonItems, &commonBiomes, &commonRecipes); } fn registerItem(assetFolder: []const u8, id: []const u8, json: JsonElement) !*items_zig.BaseItem { @@ -120,6 +143,10 @@ fn registerBlock(assetFolder: []const u8, id: []const u8, json: JsonElement) !vo // } } +fn registerRecipesFromFile(file: []const u8) !void { + try items_zig.registerRecipes(file); +} + pub const BlockPalette = struct { palette: std.ArrayList([]const u8), pub fn init(allocator: Allocator, json: JsonElement) !*BlockPalette { @@ -181,8 +208,11 @@ pub fn loadWorldAssets(assetFolder: []const u8, palette: *BlockPalette) !void { defer items.clearAndFree(); var biomes = try commonBiomes.cloneWithAllocator(main.threadAllocator); defer biomes.clearAndFree(); + var recipes = std.ArrayList([]const u8).init(main.threadAllocator); + try recipes.appendSlice(commonRecipes.items); + defer recipes.clearAndFree(); - try readAssets(arenaAllocator, assetFolder, &blocks, &items, &biomes); + try readAssets(arenaAllocator, assetFolder, &blocks, &items, &biomes, &recipes); // blocks: var block: u32 = 0; @@ -218,6 +248,10 @@ pub fn loadWorldAssets(assetFolder: []const u8, palette: *BlockPalette) !void { // block drops: try blocks_zig.registerBlockDrops(blocks); + for(recipes.items) |recipe| { + try registerRecipesFromFile(recipe); + } + // public void registerBlocks(Registry registries, NoIDRegistry oreRegistry, BlockPalette palette) { // HashMap perWorldBlocks = new HashMap<>(commonBlocks); // readAllJsonObjects("blocks", (json, id) -> { @@ -406,129 +440,4 @@ pub fn deinit() void { // missingDropsItem.clear(); // missingDropsAmount.clear(); // } -// -// public void readRecipes(ArrayList recipesList) { -// SimpleList lines = new SimpleList<>(new String[1024]); -// for (File addon : addons) { -// File recipes = new File(addon, "recipes"); -// if (recipes.exists()) { -// for (File file : recipes.listFiles()) { -// if (file.isDirectory()) continue; -// lines.clear(); -// try { -// BufferedReader buf = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8)); -// String line; -// while ((line = buf.readLine())!= null) { -// line = line.replaceAll("//.*", ""); // Ignore comments with "//". -// line = line.trim(); // Remove whitespaces before and after the word starts. -// if (line.isEmpty()) continue; -// lines.add(line); -// } -// buf.close(); -// } catch(IOException e) { -// Logger.error(e); -// } -// recipesList.add(lines.toArray()); -// } -// } -// } -// } -// -// private void registerRecipe(String[] recipe, NoIDRegistry recipeRegistry, Registry itemRegistry) { -// HashMap shortCuts = new HashMap<>(); -// ArrayList items = new ArrayList<>(); -// IntSimpleList itemsPerRow = new IntSimpleList(8); -// boolean shaped = false; -// boolean startedRecipe = false; -// for(int i = 0; i < recipe.length; i++) { -// String line = recipe[i]; -// // shortcuts: -// if (line.contains("=")) { -// String[] parts = line.split("="); -// Item item = itemRegistry.getByID(parts[1].replaceAll("\\s", "")); -// if (item == null) { -// Logger.warning("Skipping unknown item \"" + parts[1].replaceAll("\\s", "") + "\" in recipe parsing."); -// } else { -// shortCuts.put(parts[0].replaceAll("\\s", ""), itemRegistry.getByID(parts[1].replaceAll("\\s", ""))); // Remove all whitespaces, wherever they might be. Not necessarily the most robust way, but it should work. -// } -// } else if (line.startsWith("shaped")) { -// // Start of a shaped pattern -// shaped = true; -// startedRecipe = true; -// items.clear(); -// itemsPerRow.clear(); -// } else if (line.startsWith("shapeless")) { -// // Start of a shapeless pattern -// shaped = false; -// startedRecipe = true; -// items.clear(); -// itemsPerRow.clear(); -// } else if (line.startsWith("result") && startedRecipe && !itemsPerRow.isEmpty()) { -// // Parse the result, which is made up of `amount*shortcut`. -// startedRecipe = false; -// String result = line.substring(6).replaceAll("\\s", ""); // Remove "result" and all space-likes. -// int number = 1; -// if (result.contains("*")) { -// String[] parts = result.split("\\*"); -// result = parts[1]; -// number = Integer.parseInt(parts[0]); -// } -// Item item; -// if (shortCuts.containsKey(result)) { -// item = shortCuts.get(result); -// } else { -// item = itemRegistry.getByID(result); -// } -// if (item == null) { -// Logger.warning("Skipping recipe with unknown item \"" + result + "\" in recipe parsing."); -// } else { -// if (shaped) { -// int x = CubyzMath.max(itemsPerRow); -// int y = itemsPerRow.size; -// Item[] array = new Item[x*y]; -// int index = 0; -// for(int iy = 0; iy < itemsPerRow.size; iy++) { -// for(int ix = 0; ix < itemsPerRow.array[iy]; ix++) { -// array[iy*x + ix] = items.get(index); -// index++; -// } -// } -// recipeRegistry.register(new Recipe(x, y, array, number, item)); -// } else { -// recipeRegistry.register(new Recipe(items.toArray(new Item[0]), number, item)); -// } -// } -// } else if (startedRecipe) { -// // Parse the actual recipe: -// String[] words = line.split("\\s+"); // Split into sections that are divided by any number of whitespace characters. -// itemsPerRow.add(words.length); -// for(int j = 0; j < words.length; j++) { -// Item item; -// if (words[j].equals("0")) { -// item = null; -// } else if (shortCuts.containsKey(words[j])) { -// item = shortCuts.get(words[j]); -// } else { -// item = itemRegistry.getByID(words[j]); -// if (item == null) { -// startedRecipe = false; // Skip unknown recipes. -// Logger.warning("Skipping recipe with unknown item \"" + words[j] + "\" in recipe parsing."); -// } -// } -// items.add(item); -// } -// } -// } -// } -// -// public void registerRecipes(NoIDRegistry recipeRegistry, Registry itemRegistry) { -// for(String[] recipe : commonRecipes) { -// registerRecipe(recipe, recipeRegistry, itemRegistry); -// } -// ArrayList worldSpecificRecipes = new ArrayList<>(); -// readRecipes(worldSpecificRecipes); -// for(String[] recipe : worldSpecificRecipes) { -// registerRecipe(recipe, recipeRegistry, itemRegistry); -// } -// } //} \ No newline at end of file diff --git a/src/gui/components/CraftingResultSlot.zig b/src/gui/components/CraftingResultSlot.zig new file mode 100644 index 00000000..c0bafed8 --- /dev/null +++ b/src/gui/components/CraftingResultSlot.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const ItemStack = main.items.ItemStack; +const graphics = main.graphics; +const draw = graphics.draw; +const Texture = graphics.Texture; +const TextBuffer = graphics.TextBuffer; +const vec = main.vec; +const Vec2f = vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; + +const CraftingResultSlot = @This(); + +var texture: Texture = undefined; +const border: f32 = 3; + +pos: Vec2f, +size: Vec2f = .{32 + 2*border, 32 + 2*border}, +itemStack: ItemStack, +text: TextBuffer, +textSize: Vec2f = .{0, 0}, +onTake: *const fn(usize) bool, +userData: usize, +hovered: bool = false, +pressed: bool = false, + +pub fn __init() !void { + texture = try Texture.initFromFile("assets/cubyz/ui/inventory/crafting_result_slot.png"); +} + +pub fn __deinit() void { + texture.deinit(); +} + +pub fn init(pos: Vec2f, itemStack: ItemStack, onTake: *const fn(usize) bool, userData: usize) Allocator.Error!*CraftingResultSlot { + std.debug.assert(itemStack.item != null); + const self = try gui.allocator.create(CraftingResultSlot); + var buf: [16]u8 = undefined; + self.* = CraftingResultSlot { + .itemStack = itemStack, + .pos = pos, + .text = try TextBuffer.init(gui.allocator, std.fmt.bufPrint(&buf, "{}", .{self.itemStack.amount}) catch "∞", .{}, false, .right), + .onTake = onTake, + .userData = userData, + }; + self.textSize = try self.text.calculateLineBreaks(8, self.size[0] - 2*border); + return self; +} + +pub fn deinit(self: *const CraftingResultSlot) void { + self.text.deinit(); + gui.allocator.destroy(self); +} + +pub fn toComponent(self: *CraftingResultSlot) GuiComponent { + return GuiComponent{ + .craftingResultSlot = self + }; +} + +pub fn updateHovered(self: *CraftingResultSlot, _: Vec2f) void { + self.hovered = true; + gui.hoveredCraftingSlot = self; +} + +pub fn mainButtonPressed(self: *CraftingResultSlot, _: Vec2f) void { + self.pressed = true; +} + +pub fn mainButtonReleased(self: *CraftingResultSlot, mousePosition: Vec2f) void { + if(self.pressed) { + self.pressed = false; + if(GuiComponent.contains(self.pos, self.size, mousePosition)) { + if(gui.inventory.carriedItemStack.item == null or std.meta.eql(self.itemStack.item, gui.inventory.carriedItemStack.item)) { + if(std.math.add(u16, gui.inventory.carriedItemStack.amount, self.itemStack.amount) catch null) |nextAmount| if(nextAmount <= self.itemStack.item.?.stackSize()) { + if(self.onTake(self.userData)) { + gui.inventory.carriedItemStack.item = self.itemStack.item; + gui.inventory.carriedItemStack.amount += self.itemStack.amount; + } + }; + } + } + } +} + +pub fn render(self: *CraftingResultSlot, _: Vec2f) !void { + draw.setColor(0xffffffff); + texture.bindTo(0); + draw.boundImage(self.pos, self.size); + if(self.itemStack.item) |item| { + const itemTexture = try item.getTexture(); + itemTexture.bindTo(0); + draw.boundImage(self.pos + @splat(2, border), self.size - @splat(2, 2*border)); + if(self.itemStack.amount != 1) { + try self.text.render(self.pos[0] + self.size[0] - self.textSize[0] - border, self.pos[1] + self.size[1] - self.textSize[1] - border, 8); + } + } + if(self.pressed) { + draw.setColor(0x80808080); + draw.rect(self.pos, self.size); + } else if(self.hovered) { + self.hovered = false; + draw.setColor(0x300000ff); + draw.rect(self.pos, self.size); + } +} \ No newline at end of file diff --git a/src/gui/components/Icon.zig b/src/gui/components/Icon.zig new file mode 100644 index 00000000..6834b532 --- /dev/null +++ b/src/gui/components/Icon.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const graphics = main.graphics; +const draw = graphics.draw; +const Texture = graphics.Texture; +const vec = main.vec; +const Vec2f = vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; + +const Icon = @This(); + +const fontSize: f32 = 16; + +pos: Vec2f, +size: Vec2f, +texture: Texture, + +pub fn init(pos: Vec2f, size: Vec2f, texture: Texture) Allocator.Error!*Icon { + const self = try gui.allocator.create(Icon); + self.* = Icon { + .texture = texture, + .pos = pos, + .size = size, + }; + return self; +} + +pub fn deinit(self: *const Icon) void { + gui.allocator.destroy(self); +} + +pub fn toComponent(self: *Icon) GuiComponent { + return GuiComponent { + .icon = self + }; +} + +pub fn updateTexture(self: *Icon, newTexture: Texture) !void { + self.texture = newTexture; +} + +pub fn render(self: *Icon, _: Vec2f) !void { + self.texture.render(self.pos, self.size); +} \ No newline at end of file diff --git a/src/gui/components/ImmutableItemSlot.zig b/src/gui/components/ImmutableItemSlot.zig new file mode 100644 index 00000000..52354a2d --- /dev/null +++ b/src/gui/components/ImmutableItemSlot.zig @@ -0,0 +1,70 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const BaseItem = main.items.BaseItem; +const graphics = main.graphics; +const draw = graphics.draw; +const Texture = graphics.Texture; +const TextBuffer = graphics.TextBuffer; +const vec = main.vec; +const Vec2f = vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; + +const ImmutableItemSlot = @This(); + +var texture: Texture = undefined; +const border: f32 = 3; + +pos: Vec2f, +size: Vec2f = .{32 + 2*border, 32 + 2*border}, +item: *BaseItem, +amount: u32, +text: TextBuffer, +textSize: Vec2f = .{0, 0}, + +pub fn __init() !void { + texture = try Texture.initFromFile("assets/cubyz/ui/inventory/immutable_slot.png"); +} + +pub fn __deinit() void { + texture.deinit(); +} + +pub fn init(pos: Vec2f, item: *BaseItem, amount: u32) Allocator.Error!*ImmutableItemSlot { + const self = try gui.allocator.create(ImmutableItemSlot); + var buf: [16]u8 = undefined; + self.* = ImmutableItemSlot { + .item = item, + .amount = amount, + .pos = pos, + .text = try TextBuffer.init(gui.allocator, std.fmt.bufPrint(&buf, "{}", .{amount}) catch "∞", .{}, false, .right), + }; + self.textSize = try self.text.calculateLineBreaks(8, self.size[0] - 2*border); + return self; +} + +pub fn deinit(self: *const ImmutableItemSlot) void { + self.text.deinit(); + gui.allocator.destroy(self); +} + +pub fn toComponent(self: *ImmutableItemSlot) GuiComponent { + return GuiComponent{ + .immutableItemSlot = self + }; +} + +pub fn render(self: *ImmutableItemSlot, _: Vec2f) !void { + draw.setColor(0xffffffff); + texture.bindTo(0); + draw.boundImage(self.pos, self.size); + const itemTexture = try self.item.getTexture(); + itemTexture.bindTo(0); + draw.boundImage(self.pos + @splat(2, border), self.size - @splat(2, 2*border)); + if(self.amount != 1) { + try self.text.render(self.pos[0] + self.size[0] - self.textSize[0] - border, self.pos[1] + self.size[1] - self.textSize[1] - border, 8); + } +} \ No newline at end of file diff --git a/src/gui/gui.zig b/src/gui/gui.zig index e039b814..0ce42ed6 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -11,6 +11,8 @@ const Vec2f = vec.Vec2f; const Button = @import("components/Button.zig"); const CheckBox = @import("components/CheckBox.zig"); +const CraftingResultSlot = @import("components/CraftingResultSlot.zig"); +const ImmutableItemSlot = @import("components/ImmutableItemSlot.zig"); const ItemSlot = @import("components/ItemSlot.zig"); const ScrollBar = @import("components/ScrollBar.zig"); const Slider = @import("components/Slider.zig"); @@ -25,12 +27,14 @@ var hudWindows: std.ArrayList(*GuiWindow) = undefined; pub var openWindows: std.ArrayList(*GuiWindow) = undefined; pub var selectedWindow: ?*GuiWindow = null; // TODO: Make private. pub var selectedTextInput: ?*TextInput = null; +var hoveredAWindow: bool = false; pub var allocator: Allocator = undefined; pub var scale: f32 = undefined; pub var hoveredItemSlot: ?*ItemSlot = null; +pub var hoveredCraftingSlot: ?*CraftingResultSlot = null; pub fn init(_allocator: Allocator) !void { allocator = _allocator; @@ -53,6 +57,8 @@ pub fn init(_allocator: Allocator) !void { try GuiWindow.__init(); try Button.__init(); try CheckBox.__init(); + try CraftingResultSlot.__init(); + try ImmutableItemSlot.__init(); try ItemSlot.__init(); try ScrollBar.__init(); try Slider.__init(); @@ -74,6 +80,8 @@ pub fn deinit() void { GuiWindow.__deinit(); Button.__deinit(); CheckBox.__deinit(); + CraftingResultSlot.__deinit(); + ImmutableItemSlot.__deinit(); ItemSlot.__deinit(); ScrollBar.__deinit(); Slider.__deinit(); @@ -352,7 +360,12 @@ pub fn mainButtonPressed() void { inventory.update() catch |err| { std.log.err("Encountered error while updating inventory: {s}", .{@errorName(err)}); }; - if(inventory.carriedItemStack.amount != 0) return; + if(inventory.carriedItemStack.amount != 0) { + if(hoveredCraftingSlot) |hovered| { + hovered.mainButtonPressed(undefined); + } + return; + } selectedWindow = null; selectedTextInput = null; var selectedI: usize = 0; @@ -423,17 +436,20 @@ pub fn updateWindowPositions() void { pub fn updateAndRenderGui() !void { const mousePos = main.Window.getMousePosition()/@splat(2, scale); + hoveredAWindow = false; if(!main.Window.grabbed) { if(selectedWindow) |selected| { try selected.updateSelected(mousePos); } hoveredItemSlot = null; + hoveredCraftingSlot = null; var i: usize = openWindows.items.len; while(i != 0) { i -= 1; const window: *GuiWindow = openWindows.items[i]; if(GuiComponent.contains(window.pos, window.size, mousePos)) { try window.updateHovered(mousePos); + hoveredAWindow = true; break; } } @@ -450,9 +466,9 @@ pub fn updateAndRenderGui() !void { try inventory.render(mousePos); } -const inventory = struct { +pub const inventory = struct { const ItemStack = main.items.ItemStack; - var carriedItemStack: ItemStack = .{.item = null, .amount = 0}; + pub var carriedItemStack: ItemStack = .{.item = null, .amount = 0}; var carriedItemSlot: *ItemSlot = undefined; var deliveredItemStacks: std.ArrayList(*ItemStack) = undefined; var deliveredItemStacksOldAmount: std.ArrayList(u16) = undefined; @@ -547,7 +563,7 @@ const inventory = struct { } } } - } else { + } else if(!hoveredAWindow) { if(leftClick or carriedItemStack.amount == 1) { main.network.Protocols.genericUpdate.itemStackDrop(main.game.world.?.conn, carriedItemStack, vec.floatCast(f32, main.game.Player.getPosBlocking()), main.game.camera.direction, 20) catch |err| { std.log.err("Error while dropping itemStack: {s}", .{@errorName(err)}); diff --git a/src/gui/gui_component.zig b/src/gui/gui_component.zig index 2f2de31f..e4daf35c 100644 --- a/src/gui/gui_component.zig +++ b/src/gui/gui_component.zig @@ -8,19 +8,25 @@ pub const GuiComponent = union(enum) { pub const Button = @import("components/Button.zig"); pub const CheckBox = @import("components/CheckBox.zig"); + pub const CraftingResultSlot = @import("components/CraftingResultSlot.zig"); pub const HorizontalList = @import("components/HorizontalList.zig"); + pub const Icon = @import("components/Icon.zig"); + pub const ImmutableItemSlot = @import("components/ImmutableItemSlot.zig"); pub const ItemSlot = @import("components/ItemSlot.zig"); pub const Label = @import("components/Label.zig"); pub const MutexComponent = @import("components/MutexComponent.zig"); - pub const Slider = @import("components/Slider.zig"); pub const ScrollBar = @import("components/ScrollBar.zig"); + pub const Slider = @import("components/Slider.zig"); pub const TextInput = @import("components/TextInput.zig"); pub const VerticalList = @import("components/VerticalList.zig"); button: *Button, checkBox: *CheckBox, + craftingResultSlot: *CraftingResultSlot, horizontalList: *HorizontalList, + icon: *Icon, + immutableItemSlot: *ImmutableItemSlot, itemSlot: *ItemSlot, label: *Label, mutexComponent: *MutexComponent, diff --git a/src/gui/windows/_windowlist.zig b/src/gui/windows/_windowlist.zig index 378086c9..ed05fb71 100644 --- a/src/gui/windows/_windowlist.zig +++ b/src/gui/windows/_windowlist.zig @@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig"); pub const healthbar = @import("healthbar.zig"); pub const hotbar = @import("hotbar.zig"); pub const inventory = @import("inventory.zig"); +pub const inventory_crafting = @import("inventory_crafting.zig"); pub const main = @import("main.zig"); pub const multiplayer = @import("multiplayer.zig"); pub const settings = @import("settings.zig"); diff --git a/src/gui/windows/inventory.zig b/src/gui/windows/inventory.zig index e21e6d96..04cfe053 100644 --- a/src/gui/windows/inventory.zig +++ b/src/gui/windows/inventory.zig @@ -8,6 +8,7 @@ const Vec2f = main.vec.Vec2f; const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; +const Button = GuiComponent.Button; const HorizontalList = GuiComponent.HorizontalList; const VerticalList = GuiComponent.VerticalList; const ItemSlot = GuiComponent.ItemSlot; @@ -19,15 +20,20 @@ pub var window = GuiWindow { }; const padding: f32 = 8; - pub fn onOpen() Allocator.Error!void { var list = try VerticalList.init(.{padding, padding + 16}, 300, 0); - // TODO: Crafting. + // Some miscellanious slots and buttons: + // TODO: armor slots, backpack slot + stack-based backpack inventory, other items maybe? + { + var row = try HorizontalList.init(); + try row.add(try Button.init(.{0, 0}, 64, "Crafting", gui.openWindowFunction("cubyz:inventory_crafting"))); // TODO: Replace the text with an icon + try list.add(row); + } // Inventory: for(1..4) |y| { var row = try HorizontalList.init(); for(0..8) |x| { - try row.add(try ItemSlot.init(.{0, 0}, &Player.inventory__SEND_CHANGES_TO_SERVER.items[y*8 + x])); + try row.add(try ItemSlot.init(.{0, 0}, &Player.inventory__SEND_CHANGES_TO_SERVER.items[y*8 + x])); // TODO: Update server } try list.add(row); } diff --git a/src/gui/windows/inventory_crafting.zig b/src/gui/windows/inventory_crafting.zig new file mode 100644 index 00000000..83c2d482 --- /dev/null +++ b/src/gui/windows/inventory_crafting.zig @@ -0,0 +1,173 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const items = main.items; +const BaseItem = items.BaseItem; +const ItemStack = items.ItemStack; +const Player = main.game.Player; +const Texture = main.graphics.Texture; +const Vec2f = main.vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; +const GuiWindow = gui.GuiWindow; +const Button = GuiComponent.Button; +const HorizontalList = GuiComponent.HorizontalList; +const VerticalList = GuiComponent.VerticalList; +const Icon = GuiComponent.Icon; +const CraftingResultSlot = GuiComponent.CraftingResultSlot; +const ImmutableItemSlot = GuiComponent.ImmutableItemSlot; + +pub var window = GuiWindow { + .contentSize = Vec2f{64*8, 64*4}, + .title = "Crafting", + .id = "cubyz:inventory_crafting", +}; + +const padding: f32 = 8; + +var availableItems: std.ArrayList(*BaseItem) = undefined; +var itemAmount: std.ArrayList(u32) = undefined; + +var arrowTexture: Texture = undefined; +var recipeResult: ItemStack = undefined; + +pub fn init() !void { + arrowTexture = try Texture.initFromFile("assets/cubyz/ui/inventory/crafting_arrow.png"); +} + +pub fn deinit() void { + arrowTexture.deinit(); +} + +fn addItemStackToAvailable(itemStack: ItemStack) Allocator.Error!void { + if(itemStack.item) |item| { + if(item == .baseItem) { + const baseItem = item.baseItem; + for(availableItems.items, 0..) |alreadyPresent, i| { + if(baseItem == alreadyPresent) { + itemAmount.items[i] += itemStack.amount; + return; + } + } + try availableItems.append(baseItem); + try itemAmount.append(itemStack.amount); + } + } +} + +fn onTake(recipeIndex: usize) bool { + const recipe = items.recipes()[recipeIndex]; + for(recipe.sourceItems, recipe.sourceAmounts) |item, _amount| { + var amount: u32 = _amount; + for(main.game.Player.inventory__SEND_CHANGES_TO_SERVER.items) |*itemStack| { + if(itemStack.item) |invItem| { + if(invItem == .baseItem and invItem.baseItem == item) { + if(amount >= itemStack.amount) { + amount -= itemStack.amount; + itemStack.clear(); + } else { + itemStack.amount -= @intCast(u16, amount); + amount = 0; + } + if(amount == 0) break; + } + } + } + if(amount != 0) { + std.log.warn("Congratulations, you just managed to cheat {}*{s}, thanks to my lazy coding. Have fun with that :D", .{amount, item.id}); + } + } + return true; +} + +fn findAvailableRecipes(list: *VerticalList) Allocator.Error!bool { + const oldAmounts = try main.threadAllocator.dupe(u32, itemAmount.items); + defer main.threadAllocator.free(oldAmounts); + for(itemAmount.items) |*amount| { + amount.* = 0; + } + // Figure out what items are available in the inventory: + for(main.game.Player.inventory__SEND_CHANGES_TO_SERVER.items) |itemStack| { + try addItemStackToAvailable(itemStack); + } + try addItemStackToAvailable(gui.inventory.carriedItemStack); + if(std.mem.eql(u32, oldAmounts, itemAmount.items)) return false; + // Remove no longer present items: + var i: u32 = 0; + while(i < availableItems.items.len) : (i += 1) { + if(itemAmount.items[i] == 0) { + _ = itemAmount.swapRemove(i); + _ = availableItems.swapRemove(i); + } + } + // Find all recipes the player can make: + outer: for(items.recipes(), 0..) |*recipe, recipeIndex| { + middle: for(recipe.sourceItems, recipe.sourceAmounts) |sourceItem, sourceAmount| { + for(availableItems.items, itemAmount.items) |availableItem, availableAmount| { + if(availableItem == sourceItem and availableAmount >= sourceAmount) { + continue :middle; + } + } + continue :outer; // Ingredient not found. + } + // All ingredients found: Add it to the list. + var rowList = try HorizontalList.init(); + const maxColumns: u32 = 4; + const itemsPerColumn = recipe.sourceItems.len/maxColumns; + const remainder = recipe.sourceItems.len%maxColumns; + i = 0; + for(0..maxColumns) |col| { + var itemsThisColumn = itemsPerColumn; + if(col < remainder) itemsThisColumn += 1; + var columnList = try VerticalList.init(.{0, 0}, std.math.inf(f32), 0); + for(0..itemsThisColumn) |_| { + try columnList.add(try ImmutableItemSlot.init(.{0, 0}, recipe.sourceItems[i], recipe.sourceAmounts[i])); + i += 1; + } + columnList.finish(.center); + try rowList.add(columnList); + } + try rowList.add(try Icon.init(.{8, 0}, .{32, 32}, arrowTexture)); + const itemSlot = try CraftingResultSlot.init(.{8, 0}, recipe.resultItem, &onTake, recipeIndex); + try rowList.add(itemSlot); + rowList.finish(.{0, 0}, .center); + try list.add(rowList); + } + return true; +} + +fn refresh() Allocator.Error!void { + var list = try VerticalList.init(.{padding, padding + 16}, 300, 8); + if(!try findAvailableRecipes(list)) { + list.deinit(); + return; + } + if(window.rootComponent) |*comp| { + comp.deinit(); + } + list.finish(.center); + window.rootComponent = list.toComponent(); + window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @splat(2, padding); + gui.updateWindowPositions(); +} + +pub fn onOpen() Allocator.Error!void { + availableItems = std.ArrayList(*BaseItem).init(main.globalAllocator); + itemAmount = std.ArrayList(u32).init(main.globalAllocator); + try refresh(); +} + +pub fn onClose() void { + if(window.rootComponent) |*comp| { + comp.deinit(); + window.rootComponent = null; + } + availableItems.deinit(); + itemAmount.deinit(); +} + +pub fn update() Allocator.Error!void { + try refresh(); +} \ No newline at end of file diff --git a/src/items.zig b/src/items.zig index 7a3664e3..da783974 100644 --- a/src/items.zig +++ b/src/items.zig @@ -79,6 +79,17 @@ pub const BaseItem = struct { block: ?u16, foodValue: f32, // TODO: Effects. + var unobtainable = BaseItem { + .image = graphics.Image.defaultImage, + .texture = null, + .id = "unobtainable", + .name = "unobtainable", + .stackSize = 0, + .material = null, + .block = null, + .foodValue = 0, + }; + fn init(self: *BaseItem, allocator: Allocator, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, json: JsonElement) !void { self.id = try allocator.dupe(u8, id); if(texturePath.len == 0) { @@ -113,7 +124,7 @@ pub const BaseItem = struct { return hash; } - fn getTexture(self: *BaseItem) !graphics.Texture { + pub fn getTexture(self: *BaseItem) !graphics.Texture { if(self.texture == null) { if(self.block) |blockType| { const c = graphics.c; @@ -1344,18 +1355,31 @@ pub const Inventory = struct { } }; +const Recipe = struct { + sourceItems: []*BaseItem, + sourceAmounts: []u32, + resultItem: ItemStack, +}; + var arena: std.heap.ArenaAllocator = undefined; var reverseIndices: std.StringHashMap(*BaseItem) = undefined; var itemList: [65536]BaseItem = undefined; var itemListSize: u16 = 0; +var recipeList: std.ArrayList(Recipe) = undefined; + pub fn iterator() std.StringHashMap(*BaseItem).ValueIterator { return reverseIndices.valueIterator(); } +pub fn recipes() []Recipe { + return recipeList.items; +} + pub fn globalInit() void { arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); reverseIndices = std.StringHashMap(*BaseItem).init(arena.allocator()); + recipeList = std.ArrayList(Recipe).init(arena.allocator()); itemListSize = 0; } @@ -1371,8 +1395,83 @@ pub fn register(_: []const u8, texturePath: []const u8, replacementTexturePath: return newItem; } +pub fn registerRecipes(file: []const u8) !void { + var shortcuts = std.StringHashMap(*BaseItem).init(main.threadAllocator); + defer shortcuts.deinit(); + defer { + var keyIterator = shortcuts.keyIterator(); + while(keyIterator.next()) |key| { + main.threadAllocator.free(key.*); + } + } + var items = std.ArrayList(*BaseItem).init(main.threadAllocator); + defer items.deinit(); + var itemAmounts = std.ArrayList(u32).init(main.threadAllocator); + defer itemAmounts.deinit(); + var string = std.ArrayList(u8).init(main.threadAllocator); + defer string.deinit(); + var lines = std.mem.split(u8, file, "\n"); + while(lines.next()) |line| { + // shortcuts: + if(std.mem.containsAtLeast(u8, line, 1, "=")) { + var parts = std.mem.split(u8, line, "="); + for(parts.first()) |char| { + if(std.ascii.isWhitespace(char)) continue; // TODO: Unicode whitespaces + try string.append(char); + } + const shortcut = try string.toOwnedSlice(); + const id = std.mem.trim(u8, parts.rest(), &std.ascii.whitespace); // TODO: Unicode whitespaces + const item = getByID(id) orelse shortcuts.get(id) orelse &BaseItem.unobtainable; + try shortcuts.put(shortcut, item); + } else if(std.mem.startsWith(u8, line, "result") and items.items.len != 0) { + defer items.clearAndFree(); + defer itemAmounts.clearAndFree(); + var id = line["result".len..]; + var amount: u16 = 1; + if(std.mem.containsAtLeast(u8, id, 1, "*")) { + var parts = std.mem.split(u8, id, "*"); + const amountString = std.mem.trim(u8, parts.first(), &std.ascii.whitespace); // TODO: Unicode whitespaces + amount = std.fmt.parseInt(u16, amountString, 0) catch 1; + id = parts.rest(); + } + id = std.mem.trim(u8, id, &std.ascii.whitespace); // TODO: Unicode whitespaces + const item = getByID(id) orelse shortcuts.get(id) orelse continue; + const recipe = Recipe { + .sourceItems = try arena.allocator().dupe(*BaseItem, items.items), + .sourceAmounts = try arena.allocator().dupe(u32, itemAmounts.items), + .resultItem = ItemStack{.item = Item{.baseItem = item}, .amount = amount}, + }; + try recipeList.append(recipe); + } else { + var ingredients = std.mem.split(u8, line, ","); + outer: while(ingredients.next()) |ingredient| { + var id = ingredient; + var amount: u16 = 1; + if(std.mem.containsAtLeast(u8, id, 1, "*")) { + var parts = std.mem.split(u8, id, "*"); + const amountString = std.mem.trim(u8, parts.first(), &std.ascii.whitespace); // TODO: Unicode whitespaces + amount = std.fmt.parseInt(u16, amountString, 0) catch 1; + id = parts.rest(); + } + id = std.mem.trim(u8, id, &std.ascii.whitespace); // TODO: Unicode whitespaces + const item = getByID(id) orelse shortcuts.get(id) orelse continue; + // Resolve duplicates: + for(items.items, 0..) |presentItem, i| { + if(presentItem == item) { + itemAmounts.items[i] += amount; + continue :outer; + } + } + try items.append(item); + try itemAmounts.append(amount); + } + } + } +} + pub fn reset() void { reverseIndices.clearAndFree(); + recipeList.clearAndFree(); itemListSize = 0; // TODO: Use arena.reset() instead. arena.deinit(); @@ -1381,6 +1480,7 @@ pub fn reset() void { pub fn deinit() void { reverseIndices.clearAndFree(); + recipeList.clearAndFree(); arena.deinit(); }