Add inventory crafting.

This commit is contained in:
IntegratedQuantum 2023-03-31 20:37:39 +02:00
parent 9fdfa04734
commit da8f6ac9cb
14 changed files with 580 additions and 154 deletions

View File

@ -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
P
result 2*cubyz:oak_fence

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View File

@ -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<DataOrientedRegistry> registries, NoIDRegistry<Ore> oreRegistry, BlockPalette palette) {
// HashMap<Resource, JsonObject> perWorldBlocks = new HashMap<>(commonBlocks);
// readAllJsonObjects("blocks", (json, id) -> {
@ -406,129 +440,4 @@ pub fn deinit() void {
// missingDropsItem.clear();
// missingDropsAmount.clear();
// }
//
// public void readRecipes(ArrayList<String[]> recipesList) {
// SimpleList<String> 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<Recipe> recipeRegistry, Registry<Item> itemRegistry) {
// HashMap<String, Item> shortCuts = new HashMap<>();
// ArrayList<Item> 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<Recipe> recipeRegistry, Registry<Item> itemRegistry) {
// for(String[] recipe : commonRecipes) {
// registerRecipe(recipe, recipeRegistry, itemRegistry);
// }
// ArrayList<String[]> worldSpecificRecipes = new ArrayList<>();
// readRecipes(worldSpecificRecipes);
// for(String[] recipe : worldSpecificRecipes) {
// registerRecipe(recipe, recipeRegistry, itemRegistry);
// }
// }
//}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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)});

View File

@ -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,

View File

@ -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");

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}