Revamp the procedural crafting system. (#974)

* Add a button in the workbench.

This can be used to toggle the thing.

* Do all the tool type reading and registering stuff

* The button now toggles

* Send the tool type to the server when opening the workbench inventory.

Clicking the button will now just close and reopen the inventory.

* Unify the tool powers (no more shovers for you).

The tool type is no determined by the button

* Add the new system for calculating the tool properties based on per-slot parameters.

* Some fixes for the previous commit

* Allow disabling slots, or making them mandatory

* The return of the shover, and other tools. Also fixed some bugs.

* Tool texturing can now be configured using an actual texture, where each color corresponds to one slot.

Also added temporary textures for the current tools.

* Remove all the unused tool code

* Hide disabled slots and change color of optional slots.

* Add support for overlay tool textures and add an example texture for the pickaxe.

* Avoid needless error outputs when no image was loaded.
This commit is contained in:
IntegratedQuantum 2025-02-19 16:43:17 +01:00 committed by GitHub
parent ca0a37cabb
commit 3d81ef9d3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 740 additions and 688 deletions

BIN
assets/cubyz/tools/axe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,126 @@
.{
.blockClass = .wood,
.slotTypes = .{
.{
.name = .handle,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .center,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 50,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .density,
.destination = .power,
.factor = 0.2,
},
.{
.source = .power,
.destination = .power,
.functionType = .squareRoot,
.factor = 0.1,
},
},
},
.{
.name = .blade,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .density,
.destination = .power,
.factor = 0.2,
},
.{
.source = .power,
.destination = .power,
.factor = 0.7,
},
},
},
.{
.name = .gemSlot,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 2,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .backbone,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 2,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .blode,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
},
.slots = .{
.gemSlot, .backbone, .none, .none, .none,
.blode, .handle, .backbone, .none, .none,
.blade, .center, .handle, .backbone, .none,
.none, .blade, .blode, .handle, .none,
.none, .none, .none, .none, .handle,
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,136 @@
.{
.blockClass = .stone,
.slotTypes = .{
.{
.name = .handle,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .center,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 50,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .density,
.destination = .power,
.factor = 0.2,
},
.{
.source = .power,
.destination = .power,
.functionType = .squareRoot,
.factor = 0.1,
},
},
},
.{
.name = .bridge,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 10,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .density,
.destination = .power,
.factor = 0.2,
},
.{
.source = .power,
.destination = .power,
.functionType = .squareRoot,
.factor = 0.1,
},
},
},
.{
.name = .tip,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .density,
.destination = .power,
.factor = 0.2,
},
.{
.source = .power,
.destination = .power,
.factor = 0.7,
},
},
},
.{
.name = .gemSlot,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 2,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .binding,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
},
.slots = .{
.gemSlot, .binding, .bridge, .tip, .none,
.binding, .center, .binding, .none, .none,
.bridge, .binding, .handle, .none, .none,
.tip, .none, .none, .handle, .none,
.none, .none, .none, .none, .handle,
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,141 @@
.{
.blockClass = .sand,
.slotTypes = .{
.{
.name = .handle,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .center,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 60,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .power,
.destination = .power,
.functionType = .squareRoot,
.factor = 0.1,
},
},
},
.{
.name = .blade,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .power,
.destination = .power,
.factor = 0.3,
},
},
},
.{
.name = .tip,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 20,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.02,
},
.{
.source = .power,
.destination = .power,
.factor = 0.6,
},
},
},
.{
.name = .tiptip,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 30,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
.{
.source = .power,
.destination = .power,
.factor = 0.7,
},
},
},
.{
.name = .edge,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 10,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
.{
.name = .back,
.optional = true,
.parameterSets = .{
.{
.source = .resistance,
.destination = .maxDurability,
.factor = 2,
},
.{
.source = .density,
.destination = .swingTime,
.factor = 0.1,
},
},
},
},
.slots = .{
.tiptip, .tip, .edge, .none, .none,
.tip, .blade, .blade, .edge, .none,
.edge, .blade, .center, .back, .none,
.none, .edge, .back, .handle, .none,
.none, .none, .none, .none, .handle,
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

View File

@ -1065,7 +1065,7 @@ pub const Command = struct { // MARK: Command
fn serialize(self: Open, data: *main.List(u8)) void {
std.mem.writeInt(u32, data.addMany(4)[0..4], self.inv.id, .big);
std.mem.writeInt(usize, data.addMany(8)[0..8], self.inv._items.len, .big);
data.append(@intFromEnum(self.inv.type));
data.append(@intFromEnum(std.meta.activeTag(self.inv.type)));
data.append(@intFromEnum(self.source));
switch (self.source) {
.playerInventory, .hand => |val| {
@ -1084,6 +1084,12 @@ pub const Command = struct { // MARK: Command
.sharedTestingInventory, .other => {},
.alreadyFreed => unreachable,
}
switch(self.inv.type) {
.normal, .creative, .crafting => {},
.workbench => {
data.appendSlice(self.inv.type.workbench.id);
},
}
}
fn deserialize(data: []const u8, side: Side, user: ?*main.server.User) !Open {
@ -1091,7 +1097,7 @@ pub const Command = struct { // MARK: Command
if(side != .server or user == null) return error.Invalid;
const id = std.mem.readInt(u32, data[0..4], .big);
const len = std.mem.readInt(usize, data[4..12], .big);
const typ: Inventory.Type = @enumFromInt(data[12]);
const typeEnum: TypeEnum = @enumFromInt(data[12]);
const sourceType: SourceType = @enumFromInt(data[13]);
const source: Source = switch(sourceType) {
.playerInventory => .{.playerInventory = std.mem.readInt(u32, data[14..18], .big)},
@ -1126,6 +1132,16 @@ pub const Command = struct { // MARK: Command
.other => .{.other = {}},
.alreadyFreed => unreachable,
};
const remainingLen: usize = switch(sourceType) {
.playerInventory, .hand => 18,
.sharedTestingInventory, .other => 14,
.recipe => data.len,
.alreadyFreed => unreachable,
};
const typ: Type = switch(typeEnum) {
inline .normal, .creative, .crafting => |tag| tag,
.workbench => .{.workbench = main.items.getToolTypeByID(data[remainingLen..]) orelse return error.Invalid},
};
Sync.ServerSide.createInventory(user.?, id, len, typ, source);
return .{
.inv = Sync.ServerSide.getInventory(user.?, id) orelse return error.Invalid,
@ -1176,6 +1192,7 @@ pub const Command = struct { // MARK: Command
cmd.tryCraftingTo(allocator, self.source, self.dest, side, user);
return;
}
if(self.dest.inv.type == .workbench and self.dest.slot != 25 and self.dest.inv.type.workbench.slotInfos[self.dest.slot].disabled) return;
if(self.dest.inv.type == .workbench and self.dest.slot == 25) {
if(self.source.ref().item == null and self.dest.ref().item != null) {
cmd.executeBaseOperation(allocator, .{.move = .{
@ -1233,7 +1250,7 @@ pub const Command = struct { // MARK: Command
std.debug.assert(self.source.inv.type == .normal);
if(self.dest.inv.type == .creative) return;
if(self.dest.inv.type == .crafting) return;
if(self.dest.inv.type == .workbench and self.dest.slot == 25) return;
if(self.dest.inv.type == .workbench and (self.dest.slot == 25 or self.dest.inv.type.workbench.slotInfos[self.dest.slot].disabled)) return;
if(self.dest.inv.type == .workbench and !canPutIntoWorkbench(self.source)) return;
const itemSource = self.source.ref().item orelse return;
if(self.dest.ref().item) |itemDest| {
@ -1288,6 +1305,7 @@ pub const Command = struct { // MARK: Command
cmd.tryCraftingTo(allocator, self.dest, self.source, side, user);
return;
}
if(self.source.inv.type == .workbench and self.source.slot != 25 and self.source.inv.type.workbench.slotInfos[self.source.slot].disabled) return;
if(self.source.inv.type == .workbench and self.source.slot == 25) {
if(self.dest.ref().item == null and self.source.ref().item != null) {
cmd.executeBaseOperation(allocator, .{.move = .{
@ -1359,6 +1377,7 @@ pub const Command = struct { // MARK: Command
}
return;
}
if(self.source.inv.type == .workbench and self.source.slot != 25 and self.source.inv.type.workbench.slotInfos[self.source.slot].disabled) return;
if(self.source.inv.type == .workbench and self.source.slot == 25) {
cmd.removeToolCraftingIngredients(allocator, self.source.inv, side);
}
@ -1395,7 +1414,7 @@ pub const Command = struct { // MARK: Command
amount: u16 = 0,
fn run(self: FillFromCreative, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, mode: Gamemode) error{serverFailure}!void {
if(self.dest.inv.type == .workbench and self.dest.slot == 25) return;
if(self.dest.inv.type == .workbench and (self.dest.slot == 25 or self.dest.inv.type.workbench.slotInfos[self.dest.slot].disabled)) return;
if(side == .server and user != null and mode != .creative) return;
if(side == .client and mode != .creative) return;
@ -1713,12 +1732,18 @@ const Source = union(SourceType) {
const Inventory = @This(); // MARK: Inventory
const Type = enum(u8) {
const TypeEnum = enum(u8) {
normal = 0,
creative = 1,
crafting = 2,
workbench = 3,
};
const Type = union(TypeEnum) {
normal: void,
creative: void,
crafting: void,
workbench: *const main.items.ToolType,
};
type: Type,
id: u32,
_items: []ItemStack,
@ -1771,16 +1796,18 @@ fn update(self: Inventory) void {
self._items[self._items.len - 1].deinit();
self._items[self._items.len - 1].clear();
var availableItems: [25]?*const BaseItem = undefined;
var nonEmpty: bool = false;
var hasAllMandatory: bool = true;
for(0..25) |i| {
if(self._items[i].item != null and self._items[i].item.? == .baseItem) {
availableItems[i] = self._items[i].item.?.baseItem;
nonEmpty = true;
} else {
if(!self.type.workbench.slotInfos[i].optional and !self.type.workbench.slotInfos[i].disabled)
hasAllMandatory = false;
availableItems[i] = null;
}
}
if(nonEmpty) {
if(hasAllMandatory) {
var hash = std.hash.Crc32.init();
for(availableItems) |item| {
if(item != null) {
@ -1789,7 +1816,7 @@ fn update(self: Inventory) void {
hash.update("none");
}
}
self._items[self._items.len - 1].item = Item{.tool = Tool.initFromCraftingGrid(availableItems, hash.final())};
self._items[self._items.len - 1].item = Item{.tool = Tool.initFromCraftingGrid(availableItems, hash.final(), self.type.workbench)};
self._items[self._items.len - 1].amount = 1;
}
}

View File

@ -12,6 +12,7 @@ var arenaAllocator: NeverFailingAllocator = undefined;
var commonBlocks: std.StringHashMap(ZonElement) = undefined;
var commonBiomes: std.StringHashMap(ZonElement) = undefined;
var commonItems: std.StringHashMap(ZonElement) = undefined;
var commonTools: std.StringHashMap(ZonElement) = undefined;
var commonRecipes: std.StringHashMap(ZonElement) = undefined;
var commonModels: std.StringHashMap([]const u8) = undefined;
@ -181,7 +182,7 @@ pub fn readAllObjFilesInAddonsHashmap(externalAllocator: NeverFailingAllocator,
}
}
pub fn readAssets(externalAllocator: NeverFailingAllocator, assetPath: []const u8, blocks: *std.StringHashMap(ZonElement), items: *std.StringHashMap(ZonElement), biomes: *std.StringHashMap(ZonElement), recipes: *std.StringHashMap(ZonElement), models: *std.StringHashMap([]const u8)) void {
pub fn readAssets(externalAllocator: NeverFailingAllocator, assetPath: []const u8, blocks: *std.StringHashMap(ZonElement), items: *std.StringHashMap(ZonElement), tools: *std.StringHashMap(ZonElement), biomes: *std.StringHashMap(ZonElement), recipes: *std.StringHashMap(ZonElement), models: *std.StringHashMap([]const u8)) void {
var addons = main.List(std.fs.Dir).init(main.stackAllocator);
defer addons.deinit();
var addonNames = main.List([]const u8).init(main.stackAllocator);
@ -214,6 +215,7 @@ pub fn readAssets(externalAllocator: NeverFailingAllocator, assetPath: []const u
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "blocks", true, blocks);
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "items", true, items);
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "tools", true, tools);
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "biomes", true, biomes);
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "recipes", false, recipes);
readAllObjFilesInAddonsHashmap(externalAllocator, addons, addonNames, "models", models);
@ -226,11 +228,12 @@ pub fn init() void {
arenaAllocator = arena.allocator();
commonBlocks = .init(arenaAllocator.allocator);
commonItems = .init(arenaAllocator.allocator);
commonTools = .init(arenaAllocator.allocator);
commonBiomes = .init(arenaAllocator.allocator);
commonRecipes = .init(arenaAllocator.allocator);
commonModels = .init(arenaAllocator.allocator);
readAssets(arenaAllocator, "assets/", &commonBlocks, &commonItems, &commonBiomes, &commonRecipes, &commonModels);
readAssets(arenaAllocator, "assets/", &commonBlocks, &commonItems, &commonTools, &commonBiomes, &commonRecipes, &commonModels);
}
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !*items_zig.BaseItem {
@ -247,6 +250,10 @@ fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !*item
return items_zig.register(assetFolder, texturePath, replacementTexturePath, id, zon);
}
fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void {
items_zig.registerTool(assetFolder, id, zon);
}
fn registerBlock(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
const block = blocks_zig.register(assetFolder, id, zon);
blocks_zig.meshes.register(assetFolder, id, zon);
@ -322,6 +329,8 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePal
defer blocks.clearAndFree();
var items = commonItems.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer items.clearAndFree();
var tools = commonTools.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer tools.clearAndFree();
var biomes = commonBiomes.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer biomes.clearAndFree();
var recipes = commonRecipes.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
@ -329,7 +338,7 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePal
var models = commonModels.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer models.clearAndFree();
readAssets(arenaAllocator, assetFolder, &blocks, &items, &biomes, &recipes, &models);
readAssets(arenaAllocator, assetFolder, &blocks, &items, &tools, &biomes, &recipes, &models);
errdefer unloadAssets();
var modelIterator = models.iterator();
@ -363,6 +372,12 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePal
_ = try registerItem(assetFolder, entry.key_ptr.*, entry.value_ptr.*);
}
// tools:
iterator = tools.iterator();
while(iterator.next()) |entry| {
registerTool(assetFolder, entry.key_ptr.*, entry.value_ptr.*);
}
// block drops:
blocks_zig.finishBlocks(blocks);

View File

@ -32,7 +32,7 @@ textSize: Vec2f = .{0, 0},
hovered: bool = false,
pressed: bool = false,
renderFrame: bool = true,
texture: Texture,
texture: ?Texture,
mode: Mode,
var defaultTexture: Texture = undefined;
@ -42,12 +42,14 @@ const TextureParamType = union(enum) {
default: void,
immutable: void,
craftingResult: void,
invisible: void,
custom: Texture,
fn value(self: TextureParamType) Texture {
fn value(self: TextureParamType) ?Texture {
return switch(self) {
.default => defaultTexture,
.immutable => immutableTexture,
.craftingResult => craftingResultTexture,
.invisible => null,
.custom => |t| t,
};
}
@ -128,8 +130,8 @@ pub fn mainButtonReleased(self: *ItemSlot, _: Vec2f) void {
pub fn render(self: *ItemSlot, _: Vec2f) void {
self.refreshText();
draw.setColor(0xffffffff);
if(self.renderFrame) {
self.texture.bindTo(0);
if(self.renderFrame and self.texture != null) {
self.texture.?.bindTo(0);
draw.boundImage(self.pos, self.size);
}
if(self.inventory.getItem(self.itemSlot)) |item| {

View File

@ -37,13 +37,24 @@ var inv: Inventory = undefined;
var craftingResult: *ItemSlot = undefined;
var seed: u32 = undefined;
var itemSlots: [25]*ItemSlot = undefined;
pub fn onOpen() void {
seed = @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())));
inv = Inventory.init(main.globalAllocator, 26, .workbench, .other);
var toolTypes: main.ListUnmanaged(*const main.items.ToolType) = .{};
var currentToolType: usize = 0;
var toolButton: *Button = undefined;
var needsUpdate: bool = false;
fn toggleTool(_: usize) void {
currentToolType += 1;
currentToolType %= toolTypes.items.len;
toolButton.child.label.updateText(toolTypes.items[currentToolType].id);
needsUpdate = true;
}
fn openInventory() void {
inv = Inventory.init(main.globalAllocator, 26, .{.workbench = toolTypes.items[currentToolType]}, .other);
const list = HorizontalList.init();
{ // crafting grid
const grid = VerticalList.init(.{0, 0}, 300, 0);
@ -52,7 +63,8 @@ pub fn onOpen() void {
const row = HorizontalList.init();
for(0..5) |x| {
const index = x + y*5;
const slot = ItemSlot.init(.{0, 0}, inv, @intCast(index), .default, .normal);
const slotInfo = toolTypes.items[currentToolType].slotInfos[index];
const slot = ItemSlot.init(.{0, 0}, inv, @intCast(index), if(slotInfo.disabled) .invisible else if(slotInfo.optional) .immutable else .default, if(slotInfo.disabled) .immutable else .normal);
itemSlots[index] = slot;
row.add(slot);
}
@ -61,15 +73,25 @@ pub fn onOpen() void {
grid.finish(.center);
list.add(grid);
}
list.add(Icon.init(.{8, 0}, .{32, 32}, inventory_crafting.arrowTexture, false));
list.add(ItemSlot.init(.{8, 0}, inv, 25, .craftingResult, .takeOnly));
const verticalThing = VerticalList.init(.{0, 0}, 300, padding);
toolButton = Button.initText(.{8, 0}, 116, toolTypes.items[currentToolType].id, .{.callback = &toggleTool});
verticalThing.add(toolButton);
const buttonHeight = verticalThing.size[1];
const craftingResultList = HorizontalList.init();
craftingResultList.add(Icon.init(.{0, 0}, .{32, 32}, inventory_crafting.arrowTexture, false));
craftingResultList.add(ItemSlot.init(.{8, 0}, inv, 25, .craftingResult, .takeOnly));
craftingResultList.finish(.{padding, padding}, .center);
verticalThing.add(craftingResultList);
verticalThing.size[1] += buttonHeight + 2*padding; // Centering the thing
verticalThing.finish(.center);
list.add(verticalThing);
list.finish(.{padding, padding + 16}, .center);
window.rootComponent = list.toComponent();
window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding));
gui.updateWindowPositions();
}
pub fn onClose() void {
fn closeInventory() void {
main.game.Player.inventory.depositOrDrop(inv);
inv.deinit(main.globalAllocator);
if(window.rootComponent) |*comp| {
@ -77,3 +99,26 @@ pub fn onClose() void {
window.rootComponent = null;
}
}
pub fn update() void {
if(needsUpdate) {
needsUpdate = false;
closeInventory();
openInventory();
}
}
pub fn onOpen() void {
currentToolType = 0;
var iterator = main.items.toolTypeIterator();
while(iterator.next()) |toolType| {
toolTypes.append(main.globalAllocator, toolType);
}
openInventory();
}
pub fn onClose() void {
closeInventory();
toolTypes.clearAndFree(main.globalAllocator);
}

File diff suppressed because it is too large Load Diff