diff --git a/assets/cubyz/tools/axe.png b/assets/cubyz/tools/axe.png new file mode 100644 index 00000000..3d414ed3 Binary files /dev/null and b/assets/cubyz/tools/axe.png differ diff --git a/assets/cubyz/tools/axe.zig.zon b/assets/cubyz/tools/axe.zig.zon new file mode 100644 index 00000000..0e27a5e9 --- /dev/null +++ b/assets/cubyz/tools/axe.zig.zon @@ -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, + }, +} diff --git a/assets/cubyz/tools/pickaxe.png b/assets/cubyz/tools/pickaxe.png new file mode 100644 index 00000000..d9a23a46 Binary files /dev/null and b/assets/cubyz/tools/pickaxe.png differ diff --git a/assets/cubyz/tools/pickaxe.zig.zon b/assets/cubyz/tools/pickaxe.zig.zon new file mode 100644 index 00000000..aed9ead8 --- /dev/null +++ b/assets/cubyz/tools/pickaxe.zig.zon @@ -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, + }, +} diff --git a/assets/cubyz/tools/pickaxe_overlay.png b/assets/cubyz/tools/pickaxe_overlay.png new file mode 100644 index 00000000..5bee82ef Binary files /dev/null and b/assets/cubyz/tools/pickaxe_overlay.png differ diff --git a/assets/cubyz/tools/shover.png b/assets/cubyz/tools/shover.png new file mode 100644 index 00000000..34cb90f8 Binary files /dev/null and b/assets/cubyz/tools/shover.png differ diff --git a/assets/cubyz/tools/shover.zig.zon b/assets/cubyz/tools/shover.zig.zon new file mode 100644 index 00000000..de63decc --- /dev/null +++ b/assets/cubyz/tools/shover.zig.zon @@ -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, + }, +} diff --git a/assets/cubyz/tools/template.png b/assets/cubyz/tools/template.png new file mode 100644 index 00000000..13b4ad67 Binary files /dev/null and b/assets/cubyz/tools/template.png differ diff --git a/src/Inventory.zig b/src/Inventory.zig index 4825bfcf..bf464868 100644 --- a/src/Inventory.zig +++ b/src/Inventory.zig @@ -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; } } diff --git a/src/assets.zig b/src/assets.zig index 6ac3f8d0..08b747d8 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -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); diff --git a/src/gui/components/ItemSlot.zig b/src/gui/components/ItemSlot.zig index 273f3e1a..31ebd925 100644 --- a/src/gui/components/ItemSlot.zig +++ b/src/gui/components/ItemSlot.zig @@ -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| { diff --git a/src/gui/windows/workbench.zig b/src/gui/windows/workbench.zig index f6275785..5244c7cf 100644 --- a/src/gui/windows/workbench.zig +++ b/src/gui/windows/workbench.zig @@ -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); +} diff --git a/src/items.zig b/src/items.zig index 0368c536..cc9c114d 100644 --- a/src/items.zig +++ b/src/items.zig @@ -58,6 +58,25 @@ const Material = struct { // MARK: Material hash ^= hash >> 24; return hash; } + + fn getProperty(self: Material, prop: MaterialProperty) f32 { + switch(prop) { + inline else => |field| return @field(self, @tagName(field)), + } + } +}; + +const MaterialProperty = enum { + density, + resistance, + power, + + fn fromString(string: []const u8) MaterialProperty { + return std.meta.stringToEnum(MaterialProperty, string) orelse { + std.log.err("Couldn't find material property {s}. Replacing it with power", .{string}); + return .power; + }; + } }; @@ -141,225 +160,6 @@ pub const BaseItem = struct { // MARK: BaseItem ///Generates the texture of a Tool using the material information. const TextureGenerator = struct { // MARK: TextureGenerator - /// Used to translate between grid and pixel coordinates. - pub const GRID_CENTERS_X = [_]u8 { - 2, 5, 8, 11, 14, - 2, 5, 8, 11, 14, - 2, 5, 8, 11, 14, - 2, 5, 8, 11, 14, - 2, 5, 8, 11, 14, - }; - /// Used to translate between grid and pixel coordinates. - pub const GRID_CENTERS_Y = [_]u8 { - 2, 2, 2, 2, 2, - 5, 5, 5, 5, 5, - 8, 8, 8, 8, 8, - 11, 11, 11, 11, 11, - 14, 14, 14, 14, 14, - }; - - /// Contains the material(s) of a single pixel and tries to avoid multiple materials. - const PixelData = struct { - maxNeighbors: u8 = 0, - items: main.List(*const BaseItem), - pub fn init(allocator: NeverFailingAllocator) PixelData { - return PixelData { - .items = .init(allocator), - }; - } - pub fn deinit(self: *PixelData) void { - self.items.clearAndFree(); - } - pub fn add(self: *PixelData, item: *const BaseItem, neighbors: u8) void { - if(neighbors > self.maxNeighbors) { - self.maxNeighbors = neighbors; - self.items.clearRetainingCapacity(); - } - if(neighbors == self.maxNeighbors) { - self.items.append(item); - } - } - }; - - /// Counts the neighbors, while prioritizing direct neighbors over diagonals. - fn countNeighbors(relativeGrid: *[25]?*const BaseItem) u8 { - var neighbors: u8 = 0; - // direct neighbors count 1.5 times as much. - if(relativeGrid[7] != null) neighbors += 3; - if(relativeGrid[11] != null) neighbors += 3; - if(relativeGrid[13] != null) neighbors += 3; - if(relativeGrid[17] != null) neighbors += 3; - - if(relativeGrid[6] != null) neighbors += 2; - if(relativeGrid[8] != null) neighbors += 2; - if(relativeGrid[16] != null) neighbors += 2; - if(relativeGrid[18] != null) neighbors += 2; - - return neighbors; - } - - /// This part is responsible for associating each pixel with an item. - fn drawRegion(relativeGrid: *[25]?*const BaseItem, relativeNeighborCount: *[25]u8, x: u8, y: u8, pixels: *[16][16]PixelData) void { - if(relativeGrid[12]) |item| { - // Count diagonal and straight neighbors: - var diagonalNeighbors: u8 = 0; - var straightNeighbors: u8 = 0; - if(relativeGrid[7] != null) straightNeighbors += 1; - if(relativeGrid[11] != null) straightNeighbors += 1; - if(relativeGrid[13] != null) straightNeighbors += 1; - if(relativeGrid[17] != null) straightNeighbors += 1; - - if(relativeGrid[6] != null) diagonalNeighbors += 1; - if(relativeGrid[8] != null) diagonalNeighbors += 1; - if(relativeGrid[16] != null) diagonalNeighbors += 1; - if(relativeGrid[18] != null) diagonalNeighbors += 1; - - const neighbors = diagonalNeighbors + straightNeighbors; - - pixels[x + 1][y + 1].add(item, relativeNeighborCount[12]); - pixels[x + 1][y + 2].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 1].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 2].add(item, relativeNeighborCount[12]); - - // Checkout straight neighbors: - if(relativeGrid[7] != null) { - if(relativeNeighborCount[7] >= relativeNeighborCount[12]) { - pixels[x + 1][y].add(item, relativeNeighborCount[12]); - pixels[x + 2][y].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[1] != null and relativeGrid[16] == null and straightNeighbors <= 1) { - pixels[x + 2][y + 3].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[3] != null and relativeGrid[18] == null and straightNeighbors <= 1) { - pixels[x + 1][y + 3].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[11] != null) { - if(relativeNeighborCount[11] >= relativeNeighborCount[12]) { - pixels[x][y + 1].add(item, relativeNeighborCount[12]); - pixels[x][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[5] != null and relativeGrid[8] == null and straightNeighbors <= 1) { - pixels[x + 3][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[15] != null and relativeGrid[18] == null and straightNeighbors <= 1) { - pixels[x + 3][y + 1].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[13] != null) { - if(relativeNeighborCount[13] >= relativeNeighborCount[12]) { - pixels[x + 3][y + 1].add(item, relativeNeighborCount[12]); - pixels[x + 3][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[9] != null and relativeGrid[6] == null and straightNeighbors <= 1) { - pixels[x][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[19] != null and relativeGrid[16] == null and straightNeighbors <= 1) { - pixels[x][y + 1].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[17] != null) { - if(relativeNeighborCount[17] >= relativeNeighborCount[12]) { - pixels[x + 1][y + 3].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 3].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[21] != null and relativeGrid[6] == null and straightNeighbors <= 1) { - pixels[x + 2][y].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[23] != null and relativeGrid[8] == null and straightNeighbors <= 1) { - pixels[x + 1][y].add(item, relativeNeighborCount[12]); - } - } - - // Checkout diagonal neighbors: - if(relativeGrid[6] != null) { - if(relativeNeighborCount[6] >= relativeNeighborCount[12]) { - pixels[x][y].add(item, relativeNeighborCount[12]); - } - pixels[x + 1][y].add(item, relativeNeighborCount[12]); - pixels[x][y + 1].add(item, relativeNeighborCount[12]); - if(relativeGrid[1] != null and relativeGrid[7] == null and neighbors <= 2) { - pixels[x + 3][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[5] != null and relativeGrid[11] == null and neighbors <= 2) { - pixels[x + 2][y + 3].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[8] != null) { - if(relativeNeighborCount[8] >= relativeNeighborCount[12]) { - pixels[x + 3][y].add(item, relativeNeighborCount[12]); - } - pixels[x + 2][y].add(item, relativeNeighborCount[12]); - pixels[x + 3][y + 1].add(item, relativeNeighborCount[12]); - if(relativeGrid[3] != null and relativeGrid[7] == null and neighbors <= 2) { - pixels[x][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[9] != null and relativeGrid[13] == null and neighbors <= 2) { - pixels[x + 1][y + 3].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[16] != null) { - if(relativeNeighborCount[16] >= relativeNeighborCount[12]) { - pixels[x][y + 3].add(item, relativeNeighborCount[12]); - } - pixels[x][y + 2].add(item, relativeNeighborCount[12]); - pixels[x + 1][y + 3].add(item, relativeNeighborCount[12]); - if(relativeGrid[21] != null and relativeGrid[17] == null and neighbors <= 2) { - pixels[x + 3][y + 1].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[15] != null and relativeGrid[11] == null and neighbors <= 2) { - pixels[x + 2][y].add(item, relativeNeighborCount[12]); - } - } - if(relativeGrid[18] != null) { - if(relativeNeighborCount[18] >= relativeNeighborCount[12]) { - pixels[x + 3][y + 3].add(item, relativeNeighborCount[12]); - } - pixels[x + 2][y + 3].add(item, relativeNeighborCount[12]); - pixels[x + 3][y + 2].add(item, relativeNeighborCount[12]); - if(relativeGrid[23] != null and relativeGrid[17] == null and neighbors <= 2) { - pixels[x][y + 1].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[19] != null and relativeGrid[13] == null and neighbors <= 2) { - pixels[x + 1][y].add(item, relativeNeighborCount[12]); - } - } - - // Make stuff more round when there is many incoming connections: - if(diagonalNeighbors >= 3 or straightNeighbors == 4) { - pixels[x + 0][y + 1].add(item, relativeNeighborCount[12]); - pixels[x + 0][y + 2].add(item, relativeNeighborCount[12]); - pixels[x + 3][y + 1].add(item, relativeNeighborCount[12]); - pixels[x + 3][y + 2].add(item, relativeNeighborCount[12]); - pixels[x + 1][y + 0].add(item, relativeNeighborCount[12]); - pixels[x + 1][y + 3].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 0].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 3].add(item, relativeNeighborCount[12]); - // Check which of the neighbors was empty: - if(relativeGrid[6] == null) { - pixels[x + 0][y + 0].add(item, relativeNeighborCount[12]); - pixels[x + 2][y - 1].add(item, relativeNeighborCount[12]); - pixels[x - 1][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[8] == null) { - pixels[x + 3][y + 0].add(item, relativeNeighborCount[12]); - pixels[x + 1][y - 1].add(item, relativeNeighborCount[12]); - pixels[x + 4][y + 2].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[16] == null) { - pixels[x + 0][y + 3].add(item, relativeNeighborCount[12]); - pixels[x + 2][y + 4].add(item, relativeNeighborCount[12]); - pixels[x - 1][y + 1].add(item, relativeNeighborCount[12]); - } - if(relativeGrid[18] == null) { - pixels[x + 3][y + 3].add(item, relativeNeighborCount[12]); - pixels[x + 1][y + 4].add(item, relativeNeighborCount[12]); - pixels[x + 4][y + 1].add(item, relativeNeighborCount[12]); - } - } - } - } - fn generateHeightMap(itemGrid: *[16][16]?*const BaseItem, seed: *u64) [17][17]f32 { var heightMap: [17][17]f32 = undefined; var x: u8 = 0; @@ -409,17 +209,16 @@ const TextureGenerator = struct { // MARK: TextureGenerator pub fn generate(tool: *Tool) void { const img = tool.image; - var pixelMaterials: [16][16]PixelData = undefined; for(0..16) |x| { for(0..16) |y| { - pixelMaterials[x][y] = PixelData.init(main.stackAllocator); - } - } - - defer { // TODO: Maybe use an ArenaAllocator? - for(0..16) |x| { - for(0..16) |y| { - pixelMaterials[x][y].deinit(); + const source = tool.type.pixelSources[x][y]; + const sourceOverlay = tool.type.pixelSourcesOverlay[x][y]; + if(sourceOverlay < 25 and tool.craftingGrid[sourceOverlay] != null) { + tool.materialGrid[x][y] = tool.craftingGrid[sourceOverlay]; + } else if(source < 25) { + tool.materialGrid[x][y] = tool.craftingGrid[source]; + } else { + tool.materialGrid[x][y] = null; } } } @@ -427,74 +226,13 @@ const TextureGenerator = struct { // MARK: TextureGenerator var seed: u64 = tool.seed; random.scrambleSeed(&seed); - // Count all neighbors: - var neighborCount: [25]u8 = [_]u8{0} ** 25; - var x: u8 = 0; - while(x < 5) : (x += 1) { - var y: u8 = 0; - while(y < 5) : (y += 1) { - var offsetGrid: [25]?*const BaseItem = .{null} ** 25; - var dx: i32 = -2; - while(dx <= 2) : (dx += 1) { - var dy: i32 = -2; - while(dy <= 2) : (dy += 1) { - if(x + dx >= 0 and x + dx < 5 and y + dy >= 0 and y + dy < 5) { - const index: usize = @intCast(x + dx + 5*(y + dy)); - const offsetIndex: usize = @intCast(2 + dx + 5*(2 + dy)); - offsetGrid[offsetIndex] = tool.craftingGrid[index]; - } - } - } - const index = x + 5*y; - neighborCount[index] = countNeighbors(&offsetGrid); - } - } - - // Push all items from the regions on a 16×16 image grid. - x = 0; - while(x < 5) : (x += 1) { - var y: u8 = 0; - while(y < 5) : (y += 1) { - var offsetGrid: [25]?*const BaseItem = .{null} ** 25; - var offsetNeighborCount: [25]u8 = .{0} ** 25; - var dx: i32 = -2; - while(dx <= 2) : (dx += 1) { - var dy: i32 = -2; - while(dy <= 2) : (dy += 1) { - if(x + dx >= 0 and x + dx < 5 and y + dy >= 0 and y + dy < 5) { - const index: usize = @intCast(x + dx + 5*(y + dy)); - const offsetIndex: usize = @intCast(2 + dx + 5*(2 + dy)); - offsetGrid[offsetIndex] = tool.craftingGrid[index]; - offsetNeighborCount[offsetIndex] = neighborCount[index]; - } - } - } - const index = x + 5*y; - drawRegion(&offsetGrid, &offsetNeighborCount, GRID_CENTERS_X[index] - 2, GRID_CENTERS_Y[index] - 2, &pixelMaterials); - } - } - - var itemGrid = &tool.materialGrid; - x = 0; - while(x < 16) : (x += 1) { - var y: u8 = 0; - while(y < 16) : (y += 1) { - if(pixelMaterials[x][y].items.items.len != 0) { - // Choose a random material at conflict zones: - itemGrid[x][y] = pixelMaterials[x][y].items.items[random.nextIntBounded(u8, &seed, @as(u8, @intCast(pixelMaterials[x][y].items.items.len)))]; - } else { - itemGrid[x][y] = null; - } - } - } - // Generate a height map, which will be used for lighting calulations. - const heightMap = generateHeightMap(itemGrid, &seed); - x = 0; + const heightMap = generateHeightMap(&tool.materialGrid, &seed); + var x: u8 = 0; while(x < 16) : (x += 1) { var y: u8 = 0; while(y < 16) : (y += 1) { - if(itemGrid[x][y]) |item| { + if(tool.materialGrid[x][y]) |item| { if(item.material) |material| { // Calculate the lighting based on the nearest free space: const lightTL = heightMap[x][y] - heightMap[x + 1][y + 1]; @@ -515,350 +253,78 @@ const TextureGenerator = struct { // MARK: TextureGenerator /// Determines the physical properties of a tool to caclulate in-game parameters such as durability and speed. const ToolPhysics = struct { // MARK: ToolPhysics - /// Finds the handle of the tool. - /// Uses a quite simple algorithm: - /// It just simply takes the lowest, right-most 2×2 grid of filled pixels. - /// Returns whether the handle is good or not. - fn findHandle(tool: *Tool) bool { - // A handle is a piece of the tool that is normally on the bottom row and has at most one neighbor: - // Find the bottom row: - var y: u32 = 20; - outer: - while(y > 0) : (y -= 5) { - var x: u32 = 0; - while(x < 5) : (x += 5) { - if(tool.craftingGrid[y + x] != null) { - break :outer; - } - } - } - // Find a valid handle: - // Goes from right to left. - // TODO: Add left-hander setting that mirrors the x axis of the tools and the crafting grid - var x: u32 = 4; - while(true) { - if(tool.craftingGrid[y + x] != null) { - tool.handlePosition[0] = @as(f32, @floatFromInt(TextureGenerator.GRID_CENTERS_X[x + y])) - 0.5; - tool.handlePosition[1] = @as(f32, @floatFromInt(TextureGenerator.GRID_CENTERS_Y[x + y])) - 0.5; - // Count the neighbors to determine whether it's a good handle: - var neighbors: u32 = 0; - if(x != 0 and tool.craftingGrid[y + x - 1] != null) - neighbors += 1; - if(x != 4 and tool.craftingGrid[y + x + 1] != null) - neighbors += 1; - if(y != 0) { - if(tool.craftingGrid[y - 5 + x] != null) - neighbors += 1; - if(x != 0 and tool.craftingGrid[y - 5 + x - 1] != null) - neighbors += 1; - if(x != 4 and tool.craftingGrid[y - 5 + x + 1] != null) - neighbors += 1; - } - if(neighbors <= 1) { - return true; - } - } - if(x == 0) break; - x -= 1; - } - // No good handle was found on the bottom row. - return false; - } - - /// Determines the mass and moment of inertia of handle and center of mass. - fn determineInertia(tool: *Tool) void { - // Determines mass and center of mass: - var mass: f32 = 0; - var centerOfMass: Vec2f = Vec2f{0, 0}; - var x: u32 = 0; - while(x < 16) : (x += 1) { - var y: u32 = 0; - while(y < 16) : (y += 1) { - if(tool.materialGrid[x][y]) |item| { - if(item.material) |material| { - const localMass = material.density; - centerOfMass[0] += localMass*(@as(f32, @floatFromInt(x)) + 0.5); - centerOfMass[1] += localMass*(@as(f32, @floatFromInt(y)) + 0.5); - mass += localMass; - } - } - } - } - tool.centerOfMass = centerOfMass/@as(Vec2f, @splat(mass)); - tool.mass = mass; - - // Determines the moment of intertia relative to the center of mass: - var inertia: f32 = 0; - x = 0; - while(x < 16) : (x += 1) { - var y: u32 = 0; - while(y < 16) : (y += 1) { - if(tool.materialGrid[x][y]) |item| { - if(item.material) |material| { - const localMass = material.density; - const dx = @as(f32, @floatFromInt(x)) + 0.5 - tool.centerOfMass[0]; - const dy = @as(f32, @floatFromInt(y)) + 0.5 - tool.centerOfMass[1]; - inertia += localMass*(dx*dx + dy*dy); - } - } - } - } - tool.inertiaCenterOfMass = inertia; - // Using the parallel axis theorem the inertia relative to the handle can be derived: - tool.inertiaHandle = inertia + mass*vec.length(tool.centerOfMass - tool.handlePosition); - } - - /// Determines the sharpness of a point on the tool. - fn determineSharpness(tool: *Tool, point: *Vec3i, initialAngle: f32) void { - const center: Vec2f = tool.handlePosition - vec.normalize(tool.centerOfMass - tool.handlePosition)*@as(Vec2f, @splat(16)); // Going 16 pixels away from the handle to simulate arm length. - // A region is smooth if there is a lot of pixel within similar angle/distance: - const originalAngle = std.math.atan2(@as(f32, @floatFromInt(point.*[1])) + 0.5 - center[1], @as(f32, @floatFromInt(point.*[0])) + 0.5 - center[0]) - initialAngle; - const originalDistance = @cos(originalAngle)*vec.length(center - Vec2f{@as(f32, @floatFromInt(point.*[0])) + 0.5, @as(f32, @floatFromInt(point.*[1])) + 0.5}); - var numOfSmoothPixels: u31 = 0; - var x: f32 = 0; - while(x < 16) : (x += 1) { - var y: f32 = 0; - while(y < 16) : (y += 1) { - const angle = std.math.atan2(y + 0.5 - center[1], x + 0.5 - center[0]) - initialAngle; - const distance = @cos(angle)*vec.length(center - Vec2f{x + 0.5, y + 0.5}); - const deltaAngle = @abs(angle - originalAngle); - const deltaDist = @abs(distance - originalDistance); - if(deltaAngle <= 0.2 and deltaDist <= 0.7) { - numOfSmoothPixels += 1; - } - } - } - point.*[2] = numOfSmoothPixels; - } - - /// Determines where the tool would collide with the terrain. - /// Also evaluates the smoothness of the collision point and stores it in the z component. - fn determineCollisionPoints(tool: *Tool, leftCollisionPoint: *Vec3i, rightCollisionPoint: *Vec3i, frontCollisionPoint: *Vec3i, factor: f32) void { - // For finding that point the center of rotation is assumed to be 1 arm(16 pixel) begind the handle. - // Additionally the handle is assumed to go towards the center of mass. - const center: Vec2f = tool.handlePosition - vec.normalize(tool.centerOfMass - tool.handlePosition)*@as(Vec2f, @splat(factor)); // Going some distance away from the handle to simulate arm length. - // Angle of the handle. - const initialAngle = std.math.atan2(tool.handlePosition[1] - center[1], tool.handlePosition[0] - center[0]); - var leftCollisionAngle: f32 = 0; - var rightCollisionAngle: f32 = 0; - var frontCollisionDistance: f32 = 0; - var x: u8 = 0; - while(x < 16) : (x += 1) { - var y: u8 = 0; - while(y < 16) : (y += 1) { - if(tool.materialGrid[x][y] == null) continue; - const x_float: f32 = @floatFromInt(x); - const y_float: f32 = @floatFromInt(y); - const angle = std.math.atan2(y_float + 0.5 - center[1], x_float + 0.5 - center[0]) - initialAngle; - const distance = @cos(angle)*vec.length(center - Vec2f{x_float + 0.5, y_float + 0.5}); - if(angle < leftCollisionAngle) { - leftCollisionAngle = angle; - leftCollisionPoint.* = Vec3i{x, y, 0}; - } - if(angle > rightCollisionAngle) { - rightCollisionAngle = angle; - rightCollisionPoint.* = Vec3i{x, y, 0}; - } - if(distance > frontCollisionDistance) { - frontCollisionDistance = distance; - frontCollisionPoint.* = Vec3i{x, y, 0}; - } - } - } - - // sharpness is hard. - determineSharpness(tool, leftCollisionPoint, initialAngle); - determineSharpness(tool, rightCollisionPoint, initialAngle); - determineSharpness(tool, frontCollisionPoint, initialAngle); - } - - fn calculateDurability(tool: *Tool) void { - // Doesn't do much besides summing up the durability of all it's parts: - var durability: f32 = 0; - for(0..16) |x| { - for(0..16) |y| { - if(tool.materialGrid[x][y]) |item| { - if(item.material) |material| { - durability += material.resistance; - } - } - } - } - // Smaller tools are faster to swing. To balance that smaller tools get a lower durability. - tool.maxDurability = @intFromFloat(@max(1, std.math.pow(f32, durability/4, 1.5))); - tool.durability = tool.maxDurability; - } - - /// Determines how hard the tool hits the ground. - fn calculateImpactEnergy(tool: *Tool, collisionPoint: Vec3i) f32 { - // Fun fact: Without gravity the impact energy is independent of the mass of the pickaxe(E = ∫ F⃗ ds⃗), but only on the length of the handle. - var impactEnergy: f32 = vec.length(tool.centerOfMass - tool.handlePosition); - - // But when the pickaxe does get heavier 2 things happen: - // 1. The player needs to lift a bigger weight, so the tool speed gets reduced(calculated elsewhere). - // 2. When travelling down the tool also gets additional energy from gravity, so the force is increased by m·g. - impactEnergy *= tool.materialGrid[@intCast(collisionPoint[0])][@intCast(collisionPoint[1])].?.material.?.power + tool.mass/256; - - return impactEnergy; // TODO: Balancing - } - - /// Determines how good a pickaxe this side of the tool would make. - fn evaluatePickaxePower(tool: *Tool, collisionPointLower: Vec3i, collisionPointUpper: Vec3i) f32 { - // Pickaxes are used for breaking up rocks. This requires a high energy in a small area. - // So a tool is a good pickaxe, if it delivers a energy force and if it has a sharp tip. - - // A sharp tip has less than two neighbors: - var neighborsLower: u32 = 0; - var x: i32 = -1; - while(x < 2) : (x += 1) { - var y: i32 = -1; - while(y <= 2) : (y += 1) { - if(x + collisionPointLower[0] >= 0 and x + collisionPointLower[0] < 16) { - if(y + collisionPointLower[1] >= 0 and y + collisionPointLower[1] < 16) { - if(tool.materialGrid[@intCast(x + collisionPointLower[0])][@intCast(y + collisionPointLower[1])] != null) - neighborsLower += 1; - } - } - } - } - var neighborsUpper: u32 = 0; - var dirUpper: Vec2i = Vec2i{0, 0}; - x = -1; - while(x < 2) : (x += 1) { - var y: i32 = -1; - while(y <= 2) : (y += 1) { - if(x + collisionPointUpper[0] >= 0 and x + collisionPointUpper[0] < 16) { - if(y + collisionPointUpper[1] >= 0 and y + collisionPointUpper[1] < 16) { - if(tool.materialGrid[@intCast(x + collisionPointUpper[0])][@intCast(y + collisionPointUpper[1])] != null) { - neighborsUpper += 1; - dirUpper[0] += x; - dirUpper[1] += y; - } - } - } - } - } - if(neighborsLower > 3 and neighborsUpper > 3) return 0; - - // A pickaxe never points upwards: - if(neighborsUpper == 3 and dirUpper[1] == 2) { - return 0; - } - - return calculateImpactEnergy(tool, collisionPointLower); - } - - /// Determines how good an axe this side of the tool would make. - fn evaluateAxePower(tool: *Tool, collisionPointLower: Vec3i, collisionPointUpper: Vec3i) f32 { - // Axes are used for breaking up wood. This requires a larger area (= smooth tip) rather than a sharp tip. - const collisionPointLowerFloat = Vec2f{@floatFromInt(collisionPointLower[0]), @floatFromInt(collisionPointLower[1])}; - const collisionPointUpperFloat = Vec2f{@floatFromInt(collisionPointUpper[0]), @floatFromInt(collisionPointUpper[1])}; - const areaFactor = 0.25 + vec.length(collisionPointLowerFloat - collisionPointUpperFloat)/4; - - return areaFactor*calculateImpactEnergy(tool, collisionPointLower)/8; - } - - /// Determines how good a shovel this side of the tool would make. - fn evaluateShovelPower(tool: *Tool, collisionPoint: Vec3i) f32 { - // Shovels require a large area to put all the sand on. - // For the sake of simplicity I just assume that every part of the tool can contain sand and that sand piles up in a pyramidial shape. - var sandPiles: [16][16]u8 = [_][16]u8{[_]u8{0} ** 16} ** 16; - const Entry = struct { - x: u8, - y: u8, - }; - var stack = main.List(Entry).init(main.stackAllocator); - defer stack.deinit(); - // Uses a simple flood-fill algorithm equivalent to light calculation. - var x: u8 = 0; - while(x < 16) : (x += 1) { - var y: u8 = 0; - while(y < 16) : (y += 1) { - sandPiles[x][y] = std.math.maxInt(u8); - if(tool.materialGrid[x][y] == null) { - sandPiles[x][y] = 0; - stack.append(Entry{.x=x, .y=y}); - } else if(x == 0 or x == 15 or y == 0 or y == 15) { - sandPiles[x][y] = 1; - stack.append(Entry{.x=x, .y=y}); - } - } - } - while(stack.popOrNull()) |entry| { - x = entry.x; - const y = entry.y; - if(x != 0 and y != 0 and tool.materialGrid[x - 1][y - 1] != null) { - if(sandPiles[x - 1][y - 1] > sandPiles[x][y] + 1) { - sandPiles[x - 1][y - 1] = sandPiles[x][y] + 1; - stack.append(Entry{.x=x-1, .y=y-1}); - } - } - if(x != 0 and y != 15 and tool.materialGrid[x - 1][y + 1] != null) { - if(sandPiles[x - 1][y + 1] > sandPiles[x][y] + 1) { - sandPiles[x - 1][y + 1] = sandPiles[x][y] + 1; - stack.append(Entry{.x=x-1, .y=y+1}); - } - } - if(x != 15 and y != 0 and tool.materialGrid[x + 1][y - 1] != null) { - if(sandPiles[x + 1][y - 1] > sandPiles[x][y] + 1) { - sandPiles[x + 1][y - 1] = sandPiles[x][y] + 1; - stack.append(Entry{.x=x+1, .y=y-1}); - } - } - if(x != 15 and y != 15 and tool.materialGrid[x + 1][y + 1] != null) { - if(sandPiles[x + 1][y + 1] > sandPiles[x][y] + 1) { - sandPiles[x + 1][y + 1] = sandPiles[x][y] + 1; - stack.append(Entry{.x=x+1, .y=y+1}); - } - } - } - // Count the volume: - var volume: f32 = 0; - x = 0; - while(x < 16) : (x += 1) { - var y: u8 = 0; - while(y < 16) : (y += 1) { - volume += @floatFromInt(sandPiles[x][y]); - } - } - volume /= 256; // TODO: Balancing - return volume*calculateImpactEnergy(tool, collisionPoint); - } - - /// Determines all the basic properties of the tool. pub fn evaluateTool(tool: *Tool) void { - const hasGoodHandle = findHandle(tool); - calculateDurability(tool); - determineInertia(tool); - var leftCollisionPointLower = Vec3i{0, 0, 0}; - var rightCollisionPointLower = Vec3i{0, 0, 0}; - var frontCollisionPointLower = Vec3i{0, 0, 0}; - var leftCollisionPointUpper = Vec3i{0, 0, 0}; - var rightCollisionPointUpper = Vec3i{0, 0, 0}; - var frontCollisionPointUpper = Vec3i{0, 0, 0}; - determineCollisionPoints(tool, &leftCollisionPointLower, &rightCollisionPointLower, &frontCollisionPointLower, 16); - determineCollisionPoints(tool, &rightCollisionPointUpper, &leftCollisionPointUpper, &frontCollisionPointUpper, -20); - - const leftPP = evaluatePickaxePower(tool, leftCollisionPointLower, leftCollisionPointUpper); - const rightPP = evaluatePickaxePower(tool, rightCollisionPointLower, rightCollisionPointUpper); - tool.pickaxePower = @max(leftPP, rightPP); // TODO: Adjust the swing direction. - - const leftAP = evaluateAxePower(tool, leftCollisionPointLower, leftCollisionPointUpper); - const rightAP = evaluateAxePower(tool, rightCollisionPointLower, rightCollisionPointUpper); - tool.axePower = @max(leftAP, rightAP); // TODO: Adjust the swing direction. - - tool.shovelPower = evaluateShovelPower(tool, frontCollisionPointLower); - - // It takes longer to swing a heavy tool. - tool.swingTime = (tool.mass + tool.inertiaHandle/8)/256; // TODO: Balancing - - if(hasGoodHandle) { // Good handles make tools easier to handle. - tool.swingTime /= 2.0; + inline for(comptime std.meta.fieldNames(ToolProperty)) |name| { + @field(tool, name) = 0; } + for(0..25) |i| { + const material = (tool.craftingGrid[i] orelse continue).material orelse continue; + for(tool.type.slotInfos[i].parameters) |set| { + tool.getProperty(set.destination).* += set.factor*set.functionType.eval(material.getProperty(set.source) + set.additionConstant); + } + } + tool.durability = @max(1, std.math.lossyCast(u32, tool.maxDurability)); + } +}; - // TODO: Swords and throwing weapons. +const SlotInfo = struct {// MARK: SlotInfo + parameters: []ParameterSet = &.{}, + disabled: bool = false, + optional: bool = false, +}; +const ParameterSet = struct { + source: MaterialProperty, + destination: ToolProperty, + factor: f32, + additionConstant: f32, + functionType: FunctionType, +}; + +const FunctionType = enum { + linear, + square, + squareRoot, + exp2, + log2, + + fn eval(self: FunctionType, val: f32) f32 { + switch(self) { + .linear => return val, + .square => return val*val, + .squareRoot => return @sqrt(val), + .exp2 => return @exp2(val), + .log2 => return @log2(val), + } + } + + fn fromString(string: []const u8) FunctionType { + return std.meta.stringToEnum(FunctionType, string) orelse { + std.log.err("Couldn't find function type {s}. Replacing it with linear", .{string}); + return .linear; + }; + } +}; + +pub const ToolType = struct { // MARK: ToolType + id: []const u8, + blockClass: main.blocks.BlockClass, + slotInfos: [25]SlotInfo, + pixelSources: [16][16]u8, + pixelSourcesOverlay: [16][16]u8, +}; + +const ToolProperty = enum { + power, + maxDurability, + swingTime, + + fn fromString(string: []const u8) ToolProperty { + return std.meta.stringToEnum(ToolProperty, string) orelse { + std.log.err("Couldn't find tool property {s}. Replacing it with power", .{string}); + return .power; + }; } }; @@ -869,18 +335,12 @@ pub const Tool = struct { // MARK: Tool image: graphics.Image, texture: ?graphics.Texture, seed: u32, + type: *const ToolType, - /// Reduction factor to block breaking time. - pickaxePower: f32, - /// Reduction factor to block breaking time. - axePower: f32, - /// Reduction factor to block breaking time. - shovelPower: f32, - /// TODO: damage - damage: f32 = 1, + power: f32, durability: u32, - maxDurability: u32, + maxDurability: f32, /// How long it takes to swing the tool in seconds. swingTime: f32, @@ -923,10 +383,8 @@ pub const Tool = struct { // MARK: Tool .image = graphics.Image.init(main.globalAllocator, self.image.width, self.image.height), .texture = null, .seed = self.seed, - .pickaxePower = self.pickaxePower, - .axePower = self.axePower, - .shovelPower = self.shovelPower, - .damage = self.damage, + .type = self.type, + .power = self.power, .durability = self.durability, .maxDurability = self.maxDurability, .swingTime = self.swingTime, @@ -941,10 +399,11 @@ pub const Tool = struct { // MARK: Tool } - pub fn initFromCraftingGrid(craftingGrid: [25]?*const BaseItem, seed: u32) *Tool { + pub fn initFromCraftingGrid(craftingGrid: [25]?*const BaseItem, seed: u32, typ: *const ToolType) *Tool { const self = init(); self.seed = seed; self.craftingGrid = craftingGrid; + self.type = typ; // Produce the tool and its textures: // The material grid, which comes from texture generation, is needed on both server and client, to generate the tool properties. TextureGenerator.generate(self); @@ -953,8 +412,11 @@ pub const Tool = struct { // MARK: Tool } pub fn initFromZon(zon: ZonElement) *Tool { - const self = initFromCraftingGrid(extractItemsFromZon(zon.getChild("grid")), zon.get(u32, "seed", 0)); - self.durability = zon.get(u32, "durability", self.maxDurability); + const self = initFromCraftingGrid(extractItemsFromZon(zon.getChild("grid")), zon.get(u32, "seed", 0), getToolTypeByID(zon.get([]const u8, "type", "cubyz:pickaxe")) orelse blk: { + std.log.err("Couldn't find tool with type {s}. Replacing it with cubyz:pickaxe", .{zon.get([]const u8, "type", "cubyz:pickaxe")}); + break :blk getToolTypeByID("cubyz:pickaxe") orelse @panic("cubyz:pickaxe tool not found. Did you load the game with the correct assets?"); + }); + self.durability = zon.get(u32, "durability", std.math.lossyCast(u32, self.maxDurability)); return self; } @@ -979,6 +441,7 @@ pub const Tool = struct { // MARK: Tool zonObject.put("grid", zonArray); zonObject.put("durability", self.durability); zonObject.put("seed", self.seed); + zonObject.put("type", self.type.id); return zonObject; } @@ -992,6 +455,12 @@ pub const Tool = struct { // MARK: Tool return hash; } + fn getProperty(self: *Tool, prop: ToolProperty) *f32 { + switch(prop) { + inline else => |field| return &@field(self, @tagName(field)), + } + } + fn getTexture(self: *Tool) graphics.Texture { if(self.texture == null) { self.texture = graphics.Texture.init(); @@ -1003,33 +472,24 @@ pub const Tool = struct { // MARK: Tool fn getTooltip(self: *Tool) []const u8 { self.tooltip.clearRetainingCapacity(); self.tooltip.writer().print( + \\{s} \\Time to swing: {d:.2} s - \\Pickaxe power: {} % - \\Axe power: {} % - \\Shover power: {} % + \\Power: {} % \\Durability: {}/{} , .{ + self.type.id, self.swingTime, - @as(i32, @intFromFloat(100*self.pickaxePower)), - @as(i32, @intFromFloat(100*self.axePower)), - @as(i32, @intFromFloat(100*self.shovelPower)), - self.durability, self.maxDurability, + @as(i32, @intFromFloat(100*self.power)), + self.durability, std.math.lossyCast(u32, self.maxDurability), } ) catch unreachable; return self.tooltip.items; } pub fn getPowerByBlockClass(self: *Tool, blockClass: blocks.BlockClass) f32 { - return switch(blockClass) { - .fluid => 0, - .leaf => 1, - .sand => self.shovelPower, - .stone => self.pickaxePower, - .unbreakable => 0, - .wood => self.axePower, - .air => 0, - }; + if(blockClass == self.type.blockClass) return self.power; + return 0; } pub fn onUseReturnBroken(self: *Tool) bool { @@ -1191,12 +651,17 @@ pub const Recipe = struct { // MARK: Recipe }; var arena: main.utils.NeverFailingArenaAllocator = undefined; +var toolTypes: std.StringHashMap(ToolType) = undefined; var reverseIndices: std.StringHashMap(*BaseItem) = undefined; pub var itemList: [65536]BaseItem = undefined; pub var itemListSize: u16 = 0; var recipeList: main.List(Recipe) = undefined; +pub fn toolTypeIterator() std.StringHashMap(ToolType).ValueIterator { + return toolTypes.valueIterator(); +} + pub fn iterator() std.StringHashMap(*BaseItem).ValueIterator { return reverseIndices.valueIterator(); } @@ -1207,6 +672,7 @@ pub fn recipes() []Recipe { pub fn globalInit() void { arena = .init(main.globalAllocator); + toolTypes = .init(arena.allocator().allocator); reverseIndices = .init(arena.allocator().allocator); recipeList = .init(arena.allocator()); itemListSize = 0; @@ -1225,6 +691,89 @@ pub fn register(_: []const u8, texturePath: []const u8, replacementTexturePath: return newItem; } +fn loadPixelSources(assetFolder: []const u8, id: []const u8, layerPostfix: []const u8, pixelSources: *[16][16]u8) void { + var split = std.mem.splitScalar(u8, id, ':'); + const mod = split.first(); + const tool = split.rest(); + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/tools/{s}{s}.png", .{assetFolder, mod, tool, layerPostfix}) catch unreachable; + defer main.stackAllocator.free(path); + const image = main.graphics.Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + if(err != error.FileNotFound) { + std.log.err("Error while reading tool image '{s}': {s}", .{path, @errorName(err)}); + } + const replacementPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/tools/{s}{s}.png", .{mod, tool, layerPostfix}) catch unreachable; + defer main.stackAllocator.free(replacementPath); + break :blk main.graphics.Image.readFromFile(main.stackAllocator, replacementPath) catch |err2| { + if(layerPostfix.len == 0 or err2 != error.FileNotFound) + std.log.err("Error while reading tool image. Tried '{s}' and '{s}': {s}", .{path, replacementPath, @errorName(err2)}); + break :blk main.graphics.Image.emptyImage; + }; + }; + defer image.deinit(main.stackAllocator); + if((image.width != 16 or image.height != 16) and image.imageData.ptr != main.graphics.Image.emptyImage.imageData.ptr) { + std.log.err("Truncating image for {s} with incorrect dimensions. Should be 16×16.", .{id}); + } + for(0..16) |x| { + for(0..16) |y| { + const color = if(image.width != 0 and image.height != 0) image.getRGB(@min(image.width - 1, x), image.height - 1 - @min(image.height - 1, y)) else main.graphics.Color{.r = 0, .g = 0, .b = 0, .a = 0}; + pixelSources[x][y] = blk: { + if(color.a == 0) break :blk 255; + const xPos = color.r/52; + const yPos = color.b/52; + break :blk xPos + 5*yPos; + }; + } + } +} + +pub fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void { + std.log.info("Registering tool type {s}", .{id}); + if(toolTypes.contains(id)) { + std.log.err("Registered tool type with id {s} twice!", .{id}); + } + var slotTypes = std.StringHashMap(SlotInfo).init(main.stackAllocator.allocator); + defer slotTypes.deinit(); + slotTypes.put("none", .{.disabled = true}) catch unreachable; + for(zon.getChild("slotTypes").toSlice()) |typ| { + const name = typ.get([]const u8, "name", "huh?"); + var parameterSets = main.List(ParameterSet).init(arena.allocator()); + for(typ.getChild("parameterSets").toSlice()) |set| { + parameterSets.append(.{ + .source = MaterialProperty.fromString(set.get([]const u8, "source", "not specified")), + .destination = ToolProperty.fromString(set.get([]const u8, "destination", "not specified")), + .factor = set.get(f32, "factor", 1), + .additionConstant = set.get(f32, "additionConstant", 0), + .functionType = FunctionType.fromString(set.get([]const u8, "functionType", "linear")), + }); + } + slotTypes.put(name, .{ + .parameters = parameterSets.toOwnedSlice(), + .optional = typ.get(bool, "optional", false), + }) catch unreachable; + } + var slotInfos: [25]SlotInfo = undefined; + const slotTypesZon = zon.getChild("slots"); + for(0..25) |i| { + const slotTypeId = slotTypesZon.getAtIndex([]const u8, i, "none"); + slotInfos[i] = slotTypes.get(slotTypeId) orelse blk: { + std.log.err("Could not find slot type {s}. It must be specified in the same file.", .{slotTypeId}); + break :blk .{.disabled = true}; + }; + } + var pixelSources: [16][16]u8 = undefined; + loadPixelSources(assetFolder, id, "", &pixelSources); + var pixelSourcesOverlay: [16][16]u8 = undefined; + loadPixelSources(assetFolder, id, "_overlay", &pixelSourcesOverlay); + const idDupe = arena.allocator().dupe(u8, id); + toolTypes.put(idDupe, .{ + .id = idDupe, + .blockClass = std.meta.stringToEnum(main.blocks.BlockClass, zon.get([]const u8, "blockClass", "none")) orelse .air, + .slotInfos = slotInfos, + .pixelSources = pixelSources, + .pixelSourcesOverlay = pixelSourcesOverlay, + }) catch unreachable; +} + fn parseRecipeItem(zon: ZonElement) !ItemStack { var id = zon.as([]const u8, ""); id = std.mem.trim(u8, id, &std.ascii.whitespace); @@ -1267,6 +816,7 @@ pub fn registerRecipes(zon: ZonElement) void { } pub fn reset() void { + toolTypes.clearAndFree(); reverseIndices.clearAndFree(); for(recipeList.items) |recipe| { if(recipe.cachedInventory) |inv| { @@ -1279,6 +829,7 @@ pub fn reset() void { } pub fn deinit() void { + toolTypes.clearAndFree(); reverseIndices.clearAndFree(); for(recipeList.items) |recipe| { if(recipe.cachedInventory) |inv| { @@ -1298,3 +849,12 @@ pub fn getByID(id: []const u8) ?*BaseItem { return null; } } + +pub fn getToolTypeByID(id: []const u8) ?*const ToolType { + if(toolTypes.getPtr(id)) |result| { + return result; + } else { + std.log.err("Couldn't find item {s}.", .{id}); + return null; + } +}